diff --git a/static/js/editors.js b/static/js/editors.js index c132b2c..ba64b59 100644 --- a/static/js/editors.js +++ b/static/js/editors.js @@ -766,6 +766,7 @@ var editors = { foldGutter: true, cursorHeight: 1, path: data.path, + profile: 'xhtml', // define Emmet output profile extraKeys: { "Ctrl-\\": "autocompleteAnyWord", ".": "autocompleteAfterDot", @@ -803,6 +804,10 @@ var editors = { "Shift-Alt-J": "selectIdentifier" } }); + + if ("text/html" === data.mode) { + emmetCodeMirror(editor); + } editor.on('cursorActivity', function (cm) { $(".edit-exprinfo").remove(); diff --git a/static/js/lib/emmet.js b/static/js/lib/emmet.js new file mode 100644 index 0000000..cf0ea22 --- /dev/null +++ b/static/js/lib/emmet.js @@ -0,0 +1,44612 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o;"undefined"!=typeof window?o=window:"undefined"!=typeof global?o=global:"undefined"!=typeof self&&(o=self),o.emmetCodeMirror=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 2 && typeof pos !== "object") { + pos = { line: arguments[1], ch: arguments[2] }; + } + return cm.indexFromPos(pos); +} + +/** + * Converts charater index in text to CM’s internal object representation + * @param {CodeMirror} cm CodeMirror instance + * @param {Number} ix Character index in CM document + * @return {Object} + */ +function indexToPos(cm, ix) { + return cm.posFromIndex(ix); +} +},{"./emmet":2}],2:[function(require,module,exports){ +"use strict"; + +var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + +var emmet = _interopRequire(require("emmet")); + +require("emmet/bundles/snippets"); + +require("emmet/bundles/caniuse"); + +module.exports = emmet; +},{"emmet":39,"emmet/bundles/caniuse":3,"emmet/bundles/snippets":4}],3:[function(require,module,exports){ +/** + * Bundler, used in builder script to statically + * include optimized caniuse.json into bundle + */ +var ciu = require('../lib/assets/caniuse'); +var db = require('../lib/caniuse.json'); +ciu.load(db, true); +},{"../lib/assets/caniuse":23,"../lib/caniuse.json":35}],4:[function(require,module,exports){ +/** + * Bundler, used in builder script to statically + * include snippets.json into bundle + */ +var res = require('../lib/assets/resources'); +var snippets = require('../lib/snippets.json'); +res.setVocabulary(snippets, 'system'); + +},{"../lib/assets/resources":31,"../lib/snippets.json":67}],5:[function(require,module,exports){ +/** + * HTML pair matching (balancing) actions + * @constructor + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var htmlMatcher = require('../assets/htmlMatcher'); + var utils = require('../utils/common'); + var editorUtils = require('../utils/editor'); + var actionUtils = require('../utils/action'); + var range = require('../assets/range'); + var cssEditTree = require('../editTree/css'); + var cssSections = require('../utils/cssSections'); + var lastMatch = null; + + function last(arr) { + return arr[arr.length - 1]; + } + + function balanceHTML(editor, direction) { + var info = editorUtils.outputInfo(editor); + var content = info.content; + var sel = range(editor.getSelectionRange()); + + // 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 = utils.narrowToNonSpace(content, lastMatch.innerRange); + lastMatch = htmlMatcher.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 = htmlMatcher.find(content, sel.start); + if (lastMatch && lastMatch.range.equal(sel) && lastMatch.innerRange.equal(sel)) { + lastMatch.range = lastMatch.outerRange; + } + } + } + } else { + lastMatch = htmlMatcher.find(content, sel.start); + } + + if (lastMatch) { + if (lastMatch.innerRange.equal(sel)) { + lastMatch.range = lastMatch.outerRange; + } + + if (!lastMatch.range.equal(sel)) { + editor.createSelection(lastMatch.range.start, lastMatch.range.end); + return true; + } + } + + lastMatch = null; + return false; + } + + function rangesForCSSRule(rule, pos) { + // find all possible ranges + var ranges = [rule.range(true)]; + + // braces content + ranges.push(rule.valueRange(true)); + + // find nested sections + var nestedSections = cssSections.nestedSectionsInRule(rule); + + // real content, e.g. from first property name to + // last property value + var items = rule.list(); + if (items.length || nestedSections.length) { + var start = Number.POSITIVE_INFINITY, end = -1; + if (items.length) { + start = items[0].namePosition(true); + end = last(items).range(true).end; + } + + if (nestedSections.length) { + if (nestedSections[0].start < start) { + start = nestedSections[0].start; + } + + if (last(nestedSections).end > end) { + end = last(nestedSections).end; + } + } + + ranges.push(range.create2(start, end)); + } + + ranges = ranges.concat(nestedSections); + + var prop = cssEditTree.propertyFromPosition(rule, pos) || items[0]; + if (prop) { + ranges.push(prop.range(true)); + var valueRange = prop.valueRange(true); + if (!prop.end()) { + valueRange._unterminated = true; + } + ranges.push(valueRange); + } + + return ranges; + } + + /** + * Returns all possible selection ranges for given caret position + * @param {String} content CSS content + * @param {Number} pos Caret position(where to start searching) + * @return {Array} + */ + function getCSSRanges(content, pos) { + var rule; + if (typeof content === 'string') { + var ruleRange = cssSections.matchEnclosingRule(content, pos); + if (ruleRange) { + rule = cssEditTree.parse(ruleRange.substring(content), { + offset: ruleRange.start + }); + } + } else { + // passed parsed CSS rule + rule = content; + } + + if (!rule) { + return null; + } + + // find all possible ranges + var ranges = rangesForCSSRule(rule, pos); + + // remove empty ranges + ranges = ranges.filter(function(item) { + return !!item.length; + }); + + return utils.unique(ranges, function(item) { + return item.valueOf(); + }); + } + + function balanceCSS(editor, direction) { + var info = editorUtils.outputInfo(editor); + var content = info.content; + var sel = range(editor.getSelectionRange()); + + var ranges = getCSSRanges(info.content, sel.start); + if (!ranges && sel.length()) { + // possible reason: user has already selected + // CSS rule from last match + try { + var rule = cssEditTree.parse(sel.substring(info.content), { + offset: sel.start + }); + ranges = getCSSRanges(rule, sel.start); + } catch(e) {} + } + + if (!ranges) { + return false; + } + + ranges = range.sort(ranges, true); + + // edge case: find match that equals current selection, + // in case if user moves inward after selecting full CSS rule + var bestMatch = utils.find(ranges, function(r) { + return r.equal(sel); + }); + + if (!bestMatch) { + bestMatch = utils.find(ranges, function(r) { + // Check for edge case: caret right after CSS value + // but it doesn‘t contains terminating semicolon. + // In this case we have to check full value range + return r._unterminated ? r.include(sel.start) : r.inside(sel.start); + }); + } + + if (!bestMatch) { + return false; + } + + // if best match equals to current selection, move index + // one position up or down, depending on direction + var bestMatchIx = ranges.indexOf(bestMatch); + if (bestMatch.equal(sel)) { + bestMatchIx += direction == 'out' ? 1 : -1; + } + + if (bestMatchIx < 0 || bestMatchIx >= ranges.length) { + if (bestMatchIx >= ranges.length && direction == 'out') { + pos = bestMatch.start - 1; + + var outerRanges = getCSSRanges(content, pos); + if (outerRanges) { + bestMatch = last(outerRanges.filter(function(r) { + return r.inside(pos); + })); + } + } else if (bestMatchIx < 0 && direction == 'in') { + bestMatch = null; + } else { + bestMatch = null; + } + } else { + bestMatch = ranges[bestMatchIx]; + } + + if (bestMatch) { + editor.createSelection(bestMatch.start, bestMatch.end); + return true; + } + + return false; + } + + return { + /** + * Find and select HTML tag pair + * @param {IEmmetEditor} editor Editor instance + * @param {String} direction Direction of pair matching: 'in' or 'out'. + * Default is 'out' + */ + balance: function(editor, direction) { + direction = String((direction || 'out').toLowerCase()); + var info = editorUtils.outputInfo(editor); + if (actionUtils.isSupportedCSS(info.syntax)) { + return balanceCSS(editor, direction); + } + + return balanceHTML(editor, direction); + }, + + balanceInwardAction: function(editor) { + return this.balance(editor, 'in'); + }, + + balanceOutwardAction: function(editor) { + return this.balance(editor, 'out'); + }, + + /** + * Moves caret to matching opening or closing tag + * @param {IEmmetEditor} editor + */ + goToMatchingPairAction: 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 = htmlMatcher.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; + } + }; +}); +},{"../assets/htmlMatcher":26,"../assets/range":30,"../editTree/css":37,"../utils/action":69,"../utils/common":72,"../utils/cssSections":73,"../utils/editor":74}],6:[function(require,module,exports){ +/** + * Encodes/decodes image under cursor to/from base64 + * @param {IEmmetEditor} editor + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var file = require('../plugin/file'); + var base64 = require('../utils/base64'); + var actionUtils = require('../utils/action'); + var editorUtils = require('../utils/editor'); + + /** + * 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 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 = 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 absPath = file.createPath(editor.getFilePath(), filePath); + if (!absPath) { + throw "Can't save file"; + } + + file.save(absPath, base64.decode( data.replace(/^data\:.+?;.+?,/, '') )); + editor.replaceContent('$0' + filePath, pos, pos + data.length); + return true; + } + + return { + /** + * Action to encode or decode file to data:url + * @param {IEmmetEditor} editor Editor instance + * @param {String} syntax Current document syntax + * @param {String} profile Output profile name + * @return {Boolean} + */ + encodeDecodeDataUrlAction: function(editor) { + var data = String(editor.getSelection()); + var caretPos = editor.getCaretPos(); + var info = editorUtils.outputInfo(editor); + + if (!data) { + // no selection, try to find image bounds from current caret position + var text = info.content, 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; + } + }; +}); + +},{"../plugin/file":62,"../utils/action":69,"../utils/base64":70,"../utils/editor":74}],7:[function(require,module,exports){ +/** + * Move between next/prev edit points. 'Edit points' are places between tags + * and quotes of empty attributes in html + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + /** + * 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; + } + + return { + /** + * Move to previous edit point + * @param {IEmmetEditor} editor Editor instance + * @param {String} syntax Current document syntax + * @param {String} profile Output profile name + * @return {Boolean} + */ + previousEditPointAction: function(editor, syntax, profile) { + 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; + }, + + /** + * Move to next edit point + * @param {IEmmetEditor} editor Editor instance + * @param {String} syntax Current document syntax + * @param {String} profile Output profile name + * @return {Boolean} + */ + nextEditPointAction: function(editor, syntax, profile) { + var newPoint = findNewEditPoint(editor, 1); + if (newPoint != -1) { + editor.setCaretPos(newPoint); + return true; + } + + return false; + } + }; +}); +},{}],8:[function(require,module,exports){ +/** + * Evaluates simple math expression under caret + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var actionUtils = require('../utils/action'); + var utils = require('../utils/common'); + var math = require('../utils/math'); + var range = require('../assets/range'); + + return { + /** + * Evaluates math expression under the caret + * @param {IEmmetEditor} editor + * @return {Boolean} + */ + evaluateMathAction: function(editor) { + var content = editor.getContent(); + var chars = '.+-*/\\'; + + /** @type Range */ + var sel = range(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, 'round($1/$2)'); + + try { + var result = utils.prettifyNumber(math.evaluate(expr)); + editor.replaceContent(result, sel.start, sel.end); + editor.setCaretPos(sel.start + result.length); + return true; + } catch (e) {} + } + + return false; + } + }; +}); + +},{"../assets/range":30,"../utils/action":69,"../utils/common":72,"../utils/math":75}],9:[function(require,module,exports){ +/** + * '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. + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var handlerList = require('../assets/handlerList'); + var range = require('../assets/range'); + var prefs = require('../assets/preferences'); + var utils = require('../utils/common'); + var editorUtils = require('../utils/editor'); + var actionUtils = require('../utils/action'); + var cssGradient = require('../resolver/cssGradient'); + var parser = require('../parser/abbreviation'); + + /** + * Search for abbreviation in editor from current caret position + * @param {IEmmetEditor} editor Editor instance + * @return {String} + */ + function findAbbreviation(editor) { + var r = range(editor.getSelectionRange()); + var content = String(editor.getContent()); + if (r.length()) { + // abbreviation is selected by user + return r.substring(content); + } + + // search for new abbreviation from current caret position + var curLine = editor.getCurrentLineRange(); + return actionUtils.extractAbbreviation(content.substring(curLine.start, r.start)); + } + + /** + * @type HandlerList List of registered handlers + */ + var handlers = handlerList.create(); + + // XXX setup default expand handlers + + /** + * 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 = findAbbreviation(editor); + + if (abbr) { + var content = parser.expand(abbr, { + syntax: syntax, + profile: profile, + contextNode: actionUtils.captureContext(editor) + }); + + if (content) { + var replaceFrom = caretPos - abbr.length; + var replaceTo = caretPos; + + // a special case for CSS: if editor already contains + // semicolon right after current caret position — replace it too + var cssSyntaxes = prefs.getArray('css.syntaxes'); + if (cssSyntaxes && ~cssSyntaxes.indexOf(syntax)) { + var curContent = editor.getContent(); + if (curContent.charAt(caretPos) == ';' && content.charAt(content.length - 1) == ';') { + replaceTo++; + } + } + + editor.replaceContent(content, replaceFrom, replaceTo); + return true; + } + } + + return false; + }, {order: -1}); + handlers.add(cssGradient.expandAbbreviationHandler.bind(cssGradient)); + + return { + /** + * The actual “Expand Abbreviation“ action routine + * @param {IEmmetEditor} editor Editor instance + * @param {String} syntax Current document syntax + * @param {String} profile Output profile name + * @return {Boolean} + */ + expandAbbreviationAction: function(editor, syntax, profile) { + var args = utils.toArray(arguments); + + // normalize incoming arguments + var info = editorUtils.outputInfo(editor, syntax, profile); + args[1] = info.syntax; + args[2] = info.profile; + + return handlers.exec(false, args); + }, + + /** + * A special case of “Expand Abbreviation“ action, invoked by Tab key. + * In this case if abbreviation wasn’t expanded successfully or there’s a selecetion, + * the current line/selection will be indented. + * @param {IEmmetEditor} editor Editor instance + * @param {String} syntax Current document syntax + * @param {String} profile Output profile name + * @return {Boolean} + */ + expandAbbreviationWithTabAction: function(editor, syntax, profile) { + var sel = editor.getSelection(); + var indent = '\t'; + + // if something is selected in editor, + // we should indent the selected content + if (sel) { + var selRange = range(editor.getSelectionRange()); + var content = utils.padString(sel, indent); + + editor.replaceContent(indent + '${0}', editor.getCaretPos()); + var replaceRange = range(editor.getCaretPos(), selRange.length()); + editor.replaceContent(content, replaceRange.start, replaceRange.end, true); + editor.createSelection(replaceRange.start, replaceRange.start + content.length); + return true; + } + + // nothing selected, try to expand + if (!this.expandAbbreviationAction(editor, syntax, profile)) { + editor.replaceContent(indent, editor.getCaretPos()); + } + + return true; + }, + + + _defaultHandler: function(editor, syntax, profile) { + var caretPos = editor.getSelectionRange().end; + var abbr = this.findAbbreviation(editor); + + if (abbr) { + var ctx = actionUtils.captureContext(editor); + var content = parser.expand(abbr, syntax, profile, ctx); + if (content) { + editor.replaceContent(content, caretPos - abbr.length, caretPos); + return true; + } + } + + return false; + }, + + /** + * 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); + }, + + findAbbreviation: findAbbreviation + }; +}); +},{"../assets/handlerList":25,"../assets/preferences":28,"../assets/range":30,"../parser/abbreviation":54,"../resolver/cssGradient":64,"../utils/action":69,"../utils/common":72,"../utils/editor":74}],10:[function(require,module,exports){ +/** + * Increment/decrement number under cursor + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var utils = require('../utils/common'); + var actionUtils = require('../utils/action'); + + /** + * 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; + } + + return { + increment01Action: function(editor) { + return this.incrementNumber(editor, .1); + }, + + increment1Action: function(editor) { + return this.incrementNumber(editor, 1); + }, + + increment10Action: function(editor) { + return this.incrementNumber(editor, 10); + }, + + decrement01Action: function(editor) { + return this.incrementNumber(editor, -.1); + }, + + decrement1Action: function(editor) { + return this.incrementNumber(editor, -1); + }, + + decrement10Action: function(editor) { + return this.incrementNumber(editor, -10); + }, + + /** + * Default method to increment/decrement number under + * caret with given step + * @param {IEmmetEditor} editor + * @param {Number} step + * @return {Boolean} + */ + incrementNumber: function(editor, step) { + 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; + } + }; +}); +},{"../utils/action":69,"../utils/common":72}],11:[function(require,module,exports){ +/** + * Actions to insert line breaks. Some simple editors (like browser's + * <textarea>, for example) do not provide such simple things + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var prefs = require('../assets/preferences'); + var utils = require('../utils/common'); + var resources = require('../assets/resources'); + var htmlMatcher = require('../assets/htmlMatcher'); + var editorUtils = require('../utils/editor'); + + var xmlSyntaxes = ['html', 'xml', 'xsl']; + + // 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.'); + + return { + /** + * 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 for inserted newlines + * @param {IEmmetEditor} editor Editor instance + */ + insertLineBreakAction: function(editor) { + if (!this.insertLineBreakOnlyAction(editor)) { + var curPadding = editorUtils.getCurrentLinePadding(editor); + var content = String(editor.getContent()); + var caretPos = editor.getCaretPos(); + var len = content.length; + var nl = '\n'; + + // 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; + }, + + /** + * Inserts newline character with proper indentation in specific positions only. + * @param {IEmmetEditor} editor + * @return {Boolean} Returns true if line break was inserted + */ + insertLineBreakOnlyAction: function(editor) { + var info = editorUtils.outputInfo(editor); + var caretPos = editor.getCaretPos(); + var nl = '\n'; + var pad = '\t'; + + if (~xmlSyntaxes.indexOf(info.syntax)) { + // let's see if we're breaking newly created tag + var tag = 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 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; + } + }; +}); +},{"../assets/htmlMatcher":26,"../assets/preferences":28,"../assets/resources":31,"../utils/common":72,"../utils/editor":74}],12:[function(require,module,exports){ +/** + * Module describes and performs Emmet actions. The actions themselves are + * defined in actions folder + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var utils = require('../utils/common'); + + // all registered actions + var actions = {}; + + // load all default actions + var actionModules = { + base64: require('./base64'), + editPoints: require('./editPoints'), + evaluateMath: require('./evaluateMath'), + expandAbbreviation: require('./expandAbbreviation'), + incrementDecrement: require('./incrementDecrement'), + lineBreaks: require('./lineBreaks'), + balance: require('./balance'), + mergeLines: require('./mergeLines'), + reflectCSSValue: require('./reflectCSSValue'), + removeTag: require('./removeTag'), + selectItem: require('./selectItem'), + selectLine: require('./selectLine'), + splitJoinTag: require('./splitJoinTag'), + toggleComment: require('./toggleComment'), + updateImageSize: require('./updateImageSize'), + wrapWithAbbreviation: require('./wrapWithAbbreviation'), + updateTag: require('./updateTag') + }; + + function addAction(name, fn, options) { + name = name.toLowerCase(); + options = options || {}; + + if (typeof options === 'string') { + options = {label: options}; + } + + if (!options.label) { + options.label = humanizeActionName(name); + } + + actions[name] = { + name: name, + fn: fn, + options: options + }; + } + + /** + * “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 utils.trim(name.charAt(0).toUpperCase() + + name.substring(1).replace(/_[a-z]/g, function(str) { + return ' ' + str.charAt(1).toUpperCase(); + })); + } + + var bind = function(name, method) { + var m = actionModules[name]; + return m[method].bind(m); + }; + + // XXX register default actions + addAction('encode_decode_data_url', bind('base64', 'encodeDecodeDataUrlAction'), 'Encode\\Decode data:URL image'); + addAction('prev_edit_point', bind('editPoints', 'previousEditPointAction'), 'Previous Edit Point'); + addAction('next_edit_point', bind('editPoints', 'nextEditPointAction'), 'Next Edit Point'); + addAction('evaluate_math_expression', bind('evaluateMath', 'evaluateMathAction'), 'Numbers/Evaluate Math Expression'); + addAction('expand_abbreviation_with_tab', bind('expandAbbreviation', 'expandAbbreviationWithTabAction'), {hidden: true}); + addAction('expand_abbreviation', bind('expandAbbreviation', 'expandAbbreviationAction'), 'Expand Abbreviation'); + addAction('insert_formatted_line_break_only', bind('lineBreaks', 'insertLineBreakOnlyAction'), {hidden: true}); + addAction('insert_formatted_line_break', bind('lineBreaks', 'insertLineBreakAction'), {hidden: true}); + addAction('balance_inward', bind('balance', 'balanceInwardAction'), 'Balance (inward)'); + addAction('balance_outward', bind('balance', 'balanceOutwardAction'), 'Balance (outward)'); + addAction('matching_pair', bind('balance', 'goToMatchingPairAction'), 'HTML/Go To Matching Tag Pair'); + addAction('merge_lines', bind('mergeLines', 'mergeLinesAction'), 'Merge Lines'); + addAction('reflect_css_value', bind('reflectCSSValue', 'reflectCSSValueAction'), 'CSS/Reflect Value'); + addAction('remove_tag', bind('removeTag', 'removeTagAction'), 'HTML/Remove Tag'); + addAction('select_next_item', bind('selectItem', 'selectNextItemAction'), 'Select Next Item'); + addAction('select_previous_item', bind('selectItem', 'selectPreviousItemAction'), 'Select Previous Item'); + addAction('split_join_tag', bind('splitJoinTag', 'splitJoinTagAction'), 'HTML/Split\\Join Tag Declaration'); + addAction('toggle_comment', bind('toggleComment', 'toggleCommentAction'), 'Toggle Comment'); + addAction('update_image_size', bind('updateImageSize', 'updateImageSizeAction'), 'Update Image Size'); + addAction('wrap_with_abbreviation', bind('wrapWithAbbreviation', 'wrapWithAbbreviationAction'), 'Wrap With Abbreviation'); + addAction('update_tag', bind('updateTag', 'updateTagAction'), 'HTML/Update Tag'); + + [1, -1, 10, -10, 0.1, -0.1].forEach(function(num) { + var prefix = num > 0 ? 'increment' : 'decrement'; + var suffix = String(Math.abs(num)).replace('.', '').substring(0, 2); + var actionId = prefix + '_number_by_' + suffix; + var actionMethod = prefix + suffix + 'Action'; + var actionLabel = 'Numbers/' + prefix.charAt(0).toUpperCase() + prefix.substring(1) + ' number by ' + Math.abs(num); + addAction(actionId, bind('incrementDecrement', actionMethod), actionLabel); + }); + + 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) + */ + add: addAction, + + /** + * 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 + * require('action/main').run('expand_abbreviation', editor); + * require('action/main').run('wrap_with_abbreviation', [editor, 'div']); + */ + run: function(name, args) { + if (!Array.isArray(args)) { + args = utils.toArray(arguments, 1); + } + + var action = this.get(name); + if (!action) { + throw new Error('Action "' + name + '" is not defined'); + } + + return action.fn.apply(action, args); + }, + + /** + * Returns all registered actions as object + * @returns {Object} + */ + getAll: function() { + return actions; + }, + + /** + * Returns all registered actions as array + * @returns {Array} + */ + getList: function() { + var all = this.getAll(); + return Object.keys(all).map(function(key) { + return all[key]; + }); + }, + + /** + * 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 || []; + this.getList().forEach(function(action) { + if (action.options.hidden || ~skipActions.indexOf(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 = utils.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) { + return utils.find(menu || this.getMenu(), function(val) { + if (val.type == 'action') { + if (val.label == title || val.name == title) { + return val.name; + } + } else { + return this.getActionNameForMenuTitle(title, val.items); + } + }, this); + } + }; +}); +},{"../utils/common":72,"./balance":5,"./base64":6,"./editPoints":7,"./evaluateMath":8,"./expandAbbreviation":9,"./incrementDecrement":10,"./lineBreaks":11,"./mergeLines":13,"./reflectCSSValue":14,"./removeTag":15,"./selectItem":16,"./selectLine":17,"./splitJoinTag":18,"./toggleComment":19,"./updateImageSize":20,"./updateTag":21,"./wrapWithAbbreviation":22}],13:[function(require,module,exports){ +/** + * Merges selected lines or lines between XHTML tag pairs + * @param {Function} require + * @param {Underscore} _ + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var htmlMatcher = require('../assets/htmlMatcher'); + var utils = require('../utils/common'); + var editorUtils = require('../utils/editor'); + var range = require('../assets/range'); + + return { + mergeLinesAction: function(editor) { + var info = editorUtils.outputInfo(editor); + + var selection = range(editor.getSelectionRange()); + if (!selection.length()) { + // find matching tag + var pair = htmlMatcher.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; + } + }; +}); +},{"../assets/htmlMatcher":26,"../assets/range":30,"../utils/common":72,"../utils/editor":74}],14:[function(require,module,exports){ +/** + * Reflect CSS value: takes rule's value under caret and pastes it for the same + * rules with vendor prefixes + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var handlerList = require('../assets/handlerList'); + var prefs = require('../assets/preferences'); + var cssResolver = require('../resolver/css'); + var cssEditTree = require('../editTree/css'); + var utils = require('../utils/common'); + var actionUtils = require('../utils/action'); + var editorUtils = require('../utils/editor'); + var cssGradient = require('../resolver/cssGradient'); + + prefs.define('css.reflect.oldIEOpacity', false, 'Support IE6/7/8 opacity notation, e.g. filter:alpha(opacity=...).\ + Note that CSS3 and SVG also provides filter property so this option is disabled by default.') + + /** + * @type HandlerList List of registered handlers + */ + var handlers = handlerList.create(); + + function doCSSReflection(editor) { + var outputInfo = 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 = cssEditTree.baseName(name); + var vendorPrefix = '^(?:\\-\\w+\\-)?', m; + + if ((name == 'opacity' || name == 'filter') && prefs.get('css.reflect.oldIEOpacity')) { + 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 inner CSS properites in given value + * agains name‘s vendor prefix. In other words, it tries + * to modify `transform 0.2s linear` value for `-webkit-transition` + * property + * @param {String} name Reciever CSS property name + * @param {String} value New property value + * @return {String} + */ + function reflectValueParts(name, value) { + // detects and updates vendor-specific properties in value, + // e.g. -webkit-transition: -webkit-transform + + var reVendor = /^\-(\w+)\-/; + var propPrefix = reVendor.test(name) ? RegExp.$1.toLowerCase() : ''; + var parts = cssEditTree.findParts(value); + + parts.reverse(); + parts.forEach(function(part) { + var partValue = part.substring(value).replace(reVendor, ''); + var prefixes = cssResolver.vendorPrefixes(partValue); + if (prefixes) { + // if prefixes are not null then given value can + // be resolved against Can I Use database and may or + // may not contain prefixed variant + if (propPrefix && ~prefixes.indexOf(propPrefix)) { + partValue = '-' + propPrefix + '-' + partValue; + } + + value = utils.replaceSubstring(value, partValue, part); + } + }); + + return value; + } + + /** + * 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()); + + value = reflectValueParts(receiver.name(), 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) { + 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], 10) / 100) : refValue; + } + + return curValue; + } + + module = module || {}; + module.exports = { + reflectCSSValueAction: function(editor) { + if (editor.getSyntax() != 'css') { + return false; + } + + return actionUtils.compoundUpdate(editor, doCSSReflection(editor)); + }, + + _defaultHandler: function(property) { + var reName = getReflectedCSSName(property.name()); + property.parent.list().forEach(function(p) { + if (reName.test(p.name())) { + reflectValue(property, p); + } + }); + }, + + /** + * 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); + } + }; + + // XXX add default handlers + handlers.add(module.exports._defaultHandler.bind(module.exports), {order: -1}); + handlers.add(cssGradient.reflectValueHandler.bind(cssGradient)); + + return module.exports; +}); +},{"../assets/handlerList":25,"../assets/preferences":28,"../editTree/css":37,"../resolver/css":63,"../resolver/cssGradient":64,"../utils/action":69,"../utils/common":72,"../utils/editor":74}],15:[function(require,module,exports){ +/** + * Gracefully removes tag under cursor + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var utils = require('../utils/common'); + var editorUtils = require('../utils/editor'); + var htmlMatcher = require('../assets/htmlMatcher'); + + return { + removeTagAction: function(editor) { + var info = editorUtils.outputInfo(editor); + + // search for tag + var tag = 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; + } + }; +}); + +},{"../assets/htmlMatcher":26,"../utils/common":72,"../utils/editor":74}],16:[function(require,module,exports){ +/** + * Actions that use stream parsers and tokenizers for traversing: + * -- Search for next/previous items in HTML + * -- Search for next/previous items in CSS + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var range = require('../assets/range'); + var utils = require('../utils/common'); + var editorUtils = require('../utils/editor'); + var actionUtils = require('../utils/action'); + var stringStream = require('../assets/stringStream'); + var xmlParser = require('../parser/xml'); + var cssEditTree = require('../editTree/css'); + var cssSections = require('../utils/cssSections'); + + 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 content = editorUtils.outputInfo(editor).content; + + var contentLength = content.length; + var itemRange, rng; + /** @type Range */ + var prevRange = range(-1, 0); + /** @type Range */ + var sel = range(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 result = []; + var attrStart = -1, attrName = '', attrValue = '', attrValueRange, tagName; + tokens.forEach(function(tok) { + switch (tok.type) { + case 'tag': + tagName = source.substring(tok.start, tok.end); + if (/^<[\w\:\-]/.test(tagName)) { + // add tag name + result.push(range({ + 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(attrStart, tok.end - attrStart)); + + attrValueRange = range(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 + result = result.filter(function(item) { + if (item.length()) { + item.shift(offset); + return true; + } + }); + + // remove duplicates + return utils.unique(result, function(item) { + return item.toString(); + }); + } + + /** + * 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 = stringStream.create(className); + + // skip whitespace + stream.eatSpace(); + stream.start = stream.pos; + + var ch; + while ((ch = stream.next())) { + if (/[\s\u00a0]/.test(ch)) { + result.push(range(stream.start + offset, stream.pos - stream.start - 1)); + stream.eatSpace(); + stream.start = stream.pos; + } + } + + result.push(range(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, xmlParser.parse(tag), offset); + + if (isBackward) + ranges.reverse(); + + // try to find selected range + var curRange = utils.find(ranges, function(r) { + return r.equal(selRange); + }); + + if (curRange) { + var ix = ranges.indexOf(curRange); + if (ix < ranges.length - 1) + return ranges[ix + 1]; + + return null; + } + + // no selected range, find nearest one + if (isBackward) + // search backward + return utils.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 = ranges.filter(function(r) { + return r.inside(selRange.end); + }); + + if (matchedRanges.length > 1) + return matchedRanges[1]; + } + + + return utils.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 range(pos, m[0]); + } + } + + function isQuote(ch) { + return ch == '"' || ch == "'"; + } + + /** + * Returns all ranges inside given rule, available for selection + * @param {CSSEditContainer} rule + * @return {Array} + */ + function findInnerRanges(rule) { + // rule selector + var ranges = [rule.nameRange(true)]; + + // find nested sections, keep selectors only + var nestedSections = cssSections.nestedSectionsInRule(rule); + nestedSections.forEach(function(section) { + ranges.push(range.create2(section.start, section._selectorEnd)); + }); + + // add full property ranges and values + rule.list().forEach(function(property) { + ranges = ranges.concat(makePossibleRangesCSS(property)); + }); + + ranges = range.sort(ranges); + + // optimize result: remove empty ranges and duplicates + ranges = ranges.filter(function(item) { + return !!item.length(); + }); + return utils.unique(ranges, function(item) { + return item.toString(); + }); + } + + /** + * 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]; + + // 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(); + property.valueParts().forEach(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.backUp(1); + stream.skipToPair('(', ')'); + stream.backUp(1); + var fnBody = stream.current(); + result.push(range(clone.start + stream.start, fnBody)); + + // find parts + cssEditTree.findParts(fnBody).forEach(function(part) { + result.push(range(clone.start + stream.start + part.start, part.substring(fnBody))); + }); + } + }); + + return result; + } + + /** + * 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) { + var ranges = findInnerRanges(rule); + if (isBackward) { + ranges.reverse(); + } + + // return next to selected range, if possible + var r = utils.find(ranges, function(item) { + return item.equal(selRange); + }); + + if (r) { + return ranges[ranges.indexOf(r) + 1]; + } + + // find matched and (possibly) overlapping ranges + var nested = ranges.filter(function(item) { + return item.inside(selRange.end); + }); + + if (nested.length) { + return nested.sort(function(a, b) { + return a.length() - b.length(); + })[0]; + } + + // return range next to caret + var test = + r = utils.find(ranges, isBackward + ? function(item) {return item.end < selRange.start;} + : function(item) {return item.end > selRange.start;} + ); + + if (!r) { + // can’t find anything, just pick first one + r = ranges[0]; + } + + return r; + } + + function findNextCSSItem(editor) { + return findItem(editor, false, cssSections.locateRule.bind(cssSections), getRangeForNextItemInCSS); + } + + function findPrevCSSItem(editor) { + return findItem(editor, true, cssSections.locateRule.bind(cssSections), 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 = cssEditTree.parse(rule, { + offset: offset + }); + + 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 = cssEditTree.parse(rule, { + offset: offset + }); + + return matchedRangeForCSSProperty(tree, selRange, true); + } + + return { + selectNextItemAction: function(editor) { + if (actionUtils.isSupportedCSS(editor.getSyntax())) { + return findNextCSSItem(editor); + } else { + return findNextHTMLItem(editor); + } + }, + + selectPreviousItemAction: function(editor) { + if (actionUtils.isSupportedCSS(editor.getSyntax())) { + return findPrevCSSItem(editor); + } else { + return findPrevHTMLItem(editor); + } + } + }; +}); +},{"../assets/range":30,"../assets/stringStream":32,"../editTree/css":37,"../parser/xml":61,"../utils/action":69,"../utils/common":72,"../utils/cssSections":73,"../utils/editor":74}],17:[function(require,module,exports){ +/** + * Select current line (for simple editors like browser's <textarea>) + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + return { + selectLineAction: function(editor) { + var range = editor.getCurrentLineRange(); + editor.createSelection(range.start, range.end); + return true; + } + }; +}); +},{}],18:[function(require,module,exports){ +/** + * Splits or joins tag, e.g. transforms it into a short notation and vice versa:
+ * <div></div> → <div /> : join
+ * <div /> → <div></div> : split + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var utils = require('../utils/common'); + var resources = require('../assets/resources'); + var matcher = require('../assets/htmlMatcher'); + var editorUtils = require('../utils/editor'); + var profile = require('../assets/profile'); + + /** + * @param {IEmmetEditor} editor + * @param {Object} profile + * @param {Object} tag + */ + function joinTag(editor, profile, tag) { + // 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) { + var caretPos = editor.getCaretPos(); + + // define tag content depending on profile + var tagContent = (profile.tag_nl === true) ? '\n\t\n' : ''; + 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; + } + + return { + splitJoinTagAction: function(editor, profileName) { + var info = editorUtils.outputInfo(editor, null, profileName); + var curProfile = profile.get(info.profile); + + // find tag at current position + var tag = matcher.tag(info.content, editor.getCaretPos()); + if (tag) { + return tag.close + ? joinTag(editor, curProfile, tag) + : splitTag(editor, curProfile, tag); + } + + return false; + } + }; +}); +},{"../assets/htmlMatcher":26,"../assets/profile":29,"../assets/resources":31,"../utils/common":72,"../utils/editor":74}],19:[function(require,module,exports){ +/** + * 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. + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var prefs = require('../assets/preferences'); + var range = require('../assets/range'); + var utils = require('../utils/common'); + var actionUtils = require('../utils/action'); + var editorUtils = require('../utils/editor'); + var htmlMatcher = require('../assets/htmlMatcher'); + var cssEditTree = require('../editTree/css'); + + /** + * 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 r = range(editor.getSelectionRange()); + var info = editorUtils.outputInfo(editor); + + if (!r.length()) { + // no selection, find matching tag + var tag = htmlMatcher.tag(info.content, editor.getCaretPos()); + if (tag) { // found pair + r = tag.outerRange; + } + } + + return genericCommentToggle(editor, '', r); + } + + /** + * Simple CSS commenting + * @param {IEmmetEditor} editor + * @return {Boolean} Returns true if comment was toggled + */ + function toggleCSSComment(editor) { + /** @type Range */ + var rng = range(editor.getSelectionRange()); + var info = editorUtils.outputInfo(editor); + + if (!rng.length()) { + // no selection, try to get current rule + /** @type CSSRule */ + var rule = cssEditTree.parseFromPosition(info.content, editor.getCaretPos()); + if (rule) { + var property = cssItemFromPosition(rule, editor.getCaretPos()); + rng = property + ? property.range(true) + : range(rule.nameRange(true).start, rule.source); + } + } + + if (!rng.length()) { + // still no selection, get current line + rng = range(editor.getCurrentLineRange()); + utils.narrowToNonSpace(info.content, rng); + } + + return genericCommentToggle(editor, '/*', '*/', rng); + } + + /** + * 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 utils.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) + ? range(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 content = editorUtils.outputInfo(editor).content; + var caretPos = editor.getCaretPos(); + var newContent = null; + + /** + * 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; + } + + return { + /** + * Toggle comment on current editor's selection or HTML tag/CSS rule + * @param {IEmmetEditor} editor + */ + toggleCommentAction: function(editor) { + var info = editorUtils.outputInfo(editor); + if (actionUtils.isSupportedCSS(info.syntax)) { + // 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 = htmlMatcher.tag(info.content, caretPos); + if (tag && tag.open.range.inside(caretPos)) { + info.syntax = 'html'; + } + } + + var cssSyntaxes = prefs.getArray('css.syntaxes'); + if (~cssSyntaxes.indexOf(info.syntax)) { + return toggleCSSComment(editor); + } + + return toggleHTMLComment(editor); + } + }; +}); +},{"../assets/htmlMatcher":26,"../assets/preferences":28,"../assets/range":30,"../editTree/css":37,"../utils/action":69,"../utils/common":72,"../utils/editor":74}],20:[function(require,module,exports){ +/** + * Automatically updates image size attributes in HTML's <img> element or + * CSS rule + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var utils = require('../utils/common'); + var editorUtils = require('../utils/editor'); + var actionUtils = require('../utils/action'); + var xmlEditTree = require('../editTree/xml'); + var cssEditTree = require('../editTree/css'); + var base64 = require('../utils/base64'); + var file = require('../plugin/file'); + + /** + * 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 = editorUtils.outputInfo(editor); + var xmlElem = 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); + + actionUtils.compoundUpdate(editor, utils.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 = editorUtils.outputInfo(editor); + var cssRule = 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); + + actionUtils.compoundUpdate(editor, utils.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; + if (src) { + // check if it is data:url + if (/^data:/.test(src)) { + fileContent = base64.decode( src.replace(/^data\:.+?;.+?,/, '') ); + return callback(actionUtils.getImageSize(fileContent)); + } + + 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(actionUtils.getImageSize(content)); + }); + } + } + + return { + updateImageSizeAction: function(editor) { + // this action will definitely won’t work in SASS dialect, + // but may work in SCSS or LESS + if (actionUtils.isSupportedCSS(editor.getSyntax())) { + updateImageSizeCSS(editor); + } else { + updateImageSizeHTML(editor); + } + + return true; + } + }; +}); +},{"../editTree/css":37,"../editTree/xml":38,"../plugin/file":62,"../utils/action":69,"../utils/base64":70,"../utils/common":72,"../utils/editor":74}],21:[function(require,module,exports){ +/** + * Update Tag action: allows users to update existing HTML tags and add/remove + * attributes or even tag name + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var xmlEditTree = require('../editTree/xml'); + var editorUtils = require('../utils/editor'); + var actionUtils = require('../utils/action'); + var utils = require('../utils/common'); + var parser = require('../parser/abbreviation'); + + function updateAttributes(tag, abbrNode, ix) { + var classNames = (abbrNode.attribute('class') || '').split(/\s+/g); + if (ix) { + classNames.push('+' + abbrNode.name()); + } + + var r = function(str) { + return utils.replaceCounter(str, abbrNode.counter); + }; + + // update class + classNames.forEach(function(className) { + if (!className) { + return; + } + + className = r(className); + var ch = className.charAt(0); + if (ch == '+') { + tag.addClass(className.substr(1)); + } else if (ch == '-') { + tag.removeClass(className.substr(1)); + } else { + tag.value('class', className); + } + }); + + // update attributes + abbrNode.attributeList().forEach(function(attr) { + if (attr.name.toLowerCase() == 'class') { + return; + } + + var ch = attr.name.charAt(0); + if (ch == '+') { + var attrName = attr.name.substr(1); + var tagAttr = tag.get(attrName); + if (tagAttr) { + tagAttr.value(tagAttr.value() + r(attr.value)); + } else { + tag.value(attrName, r(attr.value)); + } + } else if (ch == '-') { + tag.remove(attr.name.substr(1)); + } else { + tag.value(attr.name, r(attr.value)); + } + }); + } + + return { + /** + * Matches HTML tag under caret and updates its definition + * according to given abbreviation + * @param {IEmmetEditor} Editor instance + * @param {String} abbr Abbreviation to update with + */ + updateTagAction: function(editor, abbr) { + abbr = abbr || editor.prompt("Enter abbreviation"); + + if (!abbr) { + return false; + } + + var content = editor.getContent(); + var ctx = actionUtils.captureContext(editor); + var tag = this.getUpdatedTag(abbr, ctx, content); + + if (!tag) { + // nothing to update + return false; + } + + // check if tag name was updated + if (tag.name() != ctx.name && ctx.match.close) { + editor.replaceContent('', ctx.match.close.range.start, ctx.match.close.range.end, true); + } + + editor.replaceContent(tag.source, ctx.match.open.range.start, ctx.match.open.range.end, true); + return true; + }, + + /** + * Returns XMLEditContainer node with updated tag structure + * of existing tag context. + * This data can be used to modify existing tag + * @param {String} abbr Abbreviation + * @param {Object} ctx Tag to be updated (captured with `htmlMatcher`) + * @param {String} content Original editor content + * @return {XMLEditContainer} + */ + getUpdatedTag: function(abbr, ctx, content, options) { + if (!ctx) { + // nothing to update + return null; + } + + var tree = parser.parse(abbr, options || {}); + + // for this action some characters in abbreviation has special + // meaning. For example, `.-c2` means “remove `c2` class from + // element” and `.+c3` means “append class `c3` to exising one. + // + // But `.+c3` abbreviation will actually produce two elements: + //
and . Thus, we have to walk on each element + // of parsed tree and use their definitions to update current element + var tag = xmlEditTree.parse(ctx.match.open.range.substring(content), { + offset: ctx.match.outerRange.start + }); + + tree.children.forEach(function(node, i) { + updateAttributes(tag, node, i); + }); + + // if tag name was resolved by implicit tag name resolver, + // then user omitted it in abbreviation and wants to keep + // original tag name + var el = tree.children[0]; + if (!el.data('nameResolved')) { + tag.name(el.name()); + } + + return tag; + } + }; +}); +},{"../editTree/xml":38,"../parser/abbreviation":54,"../utils/action":69,"../utils/common":72,"../utils/editor":74}],22:[function(require,module,exports){ +/** + * Action that wraps content with abbreviation. For convenience, action is + * defined as reusable module + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var range = require('../assets/range'); + var htmlMatcher = require('../assets/htmlMatcher'); + var utils = require('../utils/common'); + var editorUtils = require('../utils/editor'); + var actionUtils = require('../utils/action'); + var parser = require('../parser/abbreviation'); + + return { + /** + * 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) + */ + wrapWithAbbreviationAction: function(editor, abbr, syntax, profile) { + var info = editorUtils.outputInfo(editor, syntax, profile); + abbr = abbr || editor.prompt("Enter abbreviation"); + + if (!abbr) { + return null; + } + + abbr = String(abbr); + + var r = range(editor.getSelectionRange()); + + if (!r.length()) { + // no selection, find tag pair + var match = htmlMatcher.tag(info.content, r.start); + if (!match) { // nothing to wrap + return false; + } + + r = utils.narrowToNonSpace(info.content, match.range); + } + + var newContent = utils.escapeText(r.substring(info.content)); + var result = parser.expand(abbr, { + pastedContent: editorUtils.unindent(editor, newContent), + syntax: info.syntax, + profile: info.profile, + contextNode: actionUtils.captureContext(editor) + }); + + if (result) { + editor.replaceContent(result, r.start, r.end); + return true; + } + + return false; + } + }; +}); +},{"../assets/htmlMatcher":26,"../assets/range":30,"../parser/abbreviation":54,"../utils/action":69,"../utils/common":72,"../utils/editor":74}],23:[function(require,module,exports){ +(function (__dirname){ +/** + * 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 + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var prefs = require('./preferences'); + var utils = require('../utils/common'); + + prefs.define('caniuse.enabled', true, 'Enable support of Can I Use database. When enabled,\ + CSS abbreviation resolver will look at Can I Use database first before detecting\ + CSS properties that should be resolved'); + + prefs.define('caniuse.vendors', 'all', 'A comma-separated list vendor identifiers\ + (as described in Can I Use database) that should be supported\ + when resolving vendor-prefixed properties. Set value to all\ + to support all available properties'); + + prefs.define('caniuse.era', 'e-2', 'Browser era, as defined in Can I Use database.\ + Examples: e0 (current version), e1 (near future)\ + e-2 (2 versions back) and so on.'); + + var cssSections = { + 'border-image': ['border-image'], + 'css-boxshadow': ['box-shadow'], + 'css3-boxsizing': ['box-sizing'], + 'multicolumn': ['column-width', 'column-count', 'columns', 'column-gap', 'column-rule-color', 'column-rule-style', 'column-rule-width', 'column-rule', 'column-span', 'column-fill'], + 'border-radius': ['border-radius', 'border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'], + 'transforms2d': ['transform'], + 'css-hyphens': ['hyphens'], + 'css-transitions': ['transition', 'transition-property', 'transition-duration', 'transition-timing-function', 'transition-delay'], + 'font-feature': ['font-feature-settings'], + 'css-animation': ['animation', 'animation-name', 'animation-duration', 'animation-timing-function', 'animation-iteration-count', 'animation-direction', 'animation-play-state', 'animation-delay', 'animation-fill-mode', '@keyframes'], + 'css-gradients': ['linear-gradient'], + 'css-masks': ['mask-image', 'mask-source-type', 'mask-repeat', 'mask-position', 'mask-clip', 'mask-origin', 'mask-size', 'mask', 'mask-type', 'mask-box-image-source', 'mask-box-image-slice', 'mask-box-image-width', 'mask-box-image-outset', 'mask-box-image-repeat', 'mask-box-image', 'clip-path', 'clip-rule'], + 'css-featurequeries': ['@supports'], + 'flexbox': ['flex', 'inline-flex', 'flex-direction', 'flex-wrap', 'flex-flow', 'order', 'flex'], + 'calc': ['calc'], + 'object-fit': ['object-fit', 'object-position'], + 'css-grid': ['grid', 'inline-grid', 'grid-template-rows', 'grid-template-columns', 'grid-template-areas', 'grid-template', 'grid-auto-rows', 'grid-auto-columns', ' grid-auto-flow', 'grid-auto-position', 'grid', ' grid-row-start', 'grid-column-start', 'grid-row-end', 'grid-column-end', 'grid-column', 'grid-row', 'grid-area', 'justify-self', 'justify-items', 'align-self', 'align-items'], + 'css-repeating-gradients': ['repeating-linear-gradient'], + 'css-filters': ['filter'], + 'user-select-none': ['user-select'], + 'intrinsic-width': ['min-content', 'max-content', 'fit-content', 'fill-available'], + 'css3-tabsize': ['tab-size'] + }; + + /** @type {Object} The Can I Use database for CSS */ + var cssDB = null; + /** @type {Object} A list of available vendors (browsers) and their prefixes */ + var vendorsDB = null; + var erasDB = null; + + function intersection(arr1, arr2) { + var result = []; + var smaller = arr1, larger = arr2; + if (smaller.length > larger.length) { + smaller = arr2; + larger = arr1; + } + larger.forEach(function(item) { + if (~smaller.indexOf(item)) { + result.push(item); + } + }); + return result; + } + + /** + * Parses raw Can I Use database for better lookups + * @param {String} data Raw database + * @param {Boolean} optimized Pass `true` if given `data` is already optimized + * @return {Object} + */ + function parseDB(data, optimized) { + if (typeof data == 'string') { + data = JSON.parse(data); + } + + if (!optimized) { + data = optimize(data); + } + + vendorsDB = data.vendors; + cssDB = data.css; + erasDB = data.era; + } + + /** + * Extract required data only from CIU database + * @param {Object} data Raw Can I Use database + * @return {Object} Optimized database + */ + function optimize(data) { + if (typeof data == 'string') { + data = JSON.parse(data); + } + + return { + vendors: parseVendors(data), + css: parseCSS(data), + era: parseEra(data) + }; + } + + /** + * Parses vendor data + * @param {Object} data + * @return {Object} + */ + function parseVendors(data) { + var out = {}; + Object.keys(data.agents).forEach(function(name) { + var agent = data.agents[name]; + out[name] = { + prefix: agent.prefix, + versions: agent.versions + }; + }); + return out; + } + + /** + * Parses CSS data from Can I Use raw database + * @param {Object} data + * @return {Object} + */ + function parseCSS(data) { + var out = {}; + var cssCategories = data.cats.CSS; + Object.keys(data.data).forEach(function(name) { + var section = data.data[name]; + if (name in cssSections) { + cssSections[name].forEach(function(kw) { + out[kw] = section.stats; + }); + } + }); + + return out; + } + + /** + * Parses era data from Can I Use raw database + * @param {Object} data + * @return {Array} + */ + function parseEra(data) { + // some runtimes (like Mozilla Rhino) does not preserves + // key order so we have to sort values manually + return Object.keys(data.eras).sort(function(a, b) { + return parseInt(a.substr(1)) - parseInt(b.substr(1)); + }); + } + + /** + * Returs list of supported vendors, depending on user preferences + * @return {Array} + */ + function getVendorsList() { + var allVendors = Object.keys(vendorsDB); + var vendors = prefs.getArray('caniuse.vendors'); + if (!vendors || vendors[0] == 'all') { + return allVendors; + } + + return intersection(allVendors, vendors); + } + + /** + * Returns size of version slice as defined by era identifier + * @return {Number} + */ + function getVersionSlice() { + var era = prefs.get('caniuse.era'); + var ix = erasDB.indexOf(era); + if (!~ix) { + ix = erasDB.indexOf('e-2'); + } + + return ix; + } + + // try to load caniuse database + // hide it from Require.JS parser + var db = null; + (function(r) { + if (typeof define === 'undefined' || !define.amd) { + try { + var fs = r('fs'); + var path = r('path'); + db = fs.readFileSync(path.join(__dirname, '../caniuse.json'), {encoding: 'utf8'}); + } catch(e) {} + } + })(require); + + if (db) { + parseDB(db); + } + + return { + load: parseDB, + optimize: optimize, + + /** + * Resolves prefixes for given property + * @param {String} property A property to resolve. It can start with `@` symbol + * (CSS section, like `@keyframes`) or `:` (CSS value, like `flex`) + * @return {Array} Array of resolved prefixes or null + * if prefixes can't be resolved. Empty array means property has no vendor + * prefixes + */ + resolvePrefixes: function(property) { + if (!prefs.get('caniuse.enabled') || !cssDB || !(property in cssDB)) { + return null; + } + + var prefixes = []; + var propStats = cssDB[property]; + var versions = getVersionSlice(); + + getVendorsList().forEach(function(vendor) { + var vendorVesions = vendorsDB[vendor].versions.slice(versions); + for (var i = 0, v; i < vendorVesions.length; i++) { + v = vendorVesions[i]; + if (!v) { + continue; + } + + if (~propStats[vendor][v].indexOf('x')) { + prefixes.push(vendorsDB[vendor].prefix); + break; + } + } + }); + + return utils.unique(prefixes).sort(function(a, b) { + return b.length - a.length; + }); + } + }; +}); +}).call(this,"/node_modules/emmet/lib/assets") +},{"../utils/common":72,"./preferences":28}],24:[function(require,module,exports){ +/** + * Module that contains factories for element types used by Emmet + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var factories = {}; + var reAttrs = /([@\!]?)([\w\-:]+)\s*=\s*(['"])(.*?)\3/g; + + // register resource references + function commonFactory(value) { + return {data: value}; + } + + module = module || {}; + module.exports = { + /** + * 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 this.type(elem) === type; + }, + + /** + * Returns type of element + * @param {Object} elem + * @return {String} + */ + type: function(elem) { + return elem && elem.type; + } + }; + + /** + * 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 + */ + module.exports.add('element', function(elementName, attrs, isEmpty) { + var ret = { + name: elementName, + is_empty: !!isEmpty + }; + + if (attrs) { + ret.attributes = []; + if (Array.isArray(attrs)) { + ret.attributes = attrs; + } else if (typeof attrs === 'string') { + var m; + while ((m = reAttrs.exec(attrs))) { + ret.attributes.push({ + name: m[2], + value: m[4], + isDefault: m[1] == '@', + isImplied: m[1] == '!' + }); + } + } else { + ret.attributes = Object.keys(attrs).map(function(name) { + return { + name: name, + value: attrs[name] + }; + }); + } + } + + return ret; + }); + + module.exports.add('snippet', commonFactory); + module.exports.add('reference', commonFactory); + module.exports.add('empty', function() { + return {}; + }); + + return module.exports; +}); +},{}],25:[function(require,module,exports){ +/** + * 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 + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var utils = require('../utils/common'); + + /** + * @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) { + // TODO hack for stable sort, remove after fixing `list()` + var order = this._list.length; + if (options && 'order' in options) { + order = options.order * 10000; + } + this._list.push(utils.extend({}, options, {order: order, fn: fn})); + }, + + /** + * Removes handler from list + * @param {Function} fn + */ + remove: function(fn) { + var item = utils.find(this._list, function(item) { + return item.fn === fn; + }); + if (item) { + this._list.splice(this._list.indexOf(item), 1); + } + }, + + /** + * 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() { + // TODO make stable sort + return this._list.sort(function(a, b) { + return b.order - a.order; + }); + }, + + /** + * Returns ordered list of handler functions + * @returns {Array} + */ + listFn: function() { + return this.list().map(function(item) { + return item.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; + utils.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(); + } + }; +}); +},{"../utils/common":72}],26:[function(require,module,exports){ +/** + * 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.) + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var range = require('./range'); + + // 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: range(i, match[0]), + type: 'open' + }; + } + + function closeTag(i, match) { + return { + name: match[1], + /** @type Range */ + range: range(i, match[0]), + type: 'close' + }; + } + + function comment(i, match) { + return { + /** @type Range */ + range: range(i, typeof match == 'number' ? 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)) { + memo[key] = false; + 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); + } + } + } + + return memo[key]; + }, + + /** + * Returns original text + * @returns {String} + */ + text: function() { + return text; + }, + + clean: function() { + memo = text = m = null; + } + }; + } + + 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 (stack[stack.length - 1] == 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; + } + } + } + + pos = tag.range.end - 1; + } + } + } + + 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 matcher = createMatcher(text); + var open = null, close = null; + var j, jl; + + 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 (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; + } + } + + matcher.clean(); + + 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; + } + } + }; +}); +},{"./range":30}],27:[function(require,module,exports){ +/** + * Simple logger for Emmet + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + return { + log: function() { + if (typeof console != 'undefined' && console.log) { + console.log.apply(console, arguments); + } + } + } +}) +},{}],28:[function(require,module,exports){ +/** + * 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 + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var utils = require('../utils/common'); + + var preferences = {}; + var defaults = {}; + var _dbgDefaults = null; + var _dbgPreferences = null; + + function toBoolean(val) { + if (typeof val === 'string') { + val = val.toLowerCase(); + return val == 'yes' || val == 'true' || val == '1'; + } + + return !!val; + } + + function isValueObj(obj) { + return typeof obj === 'object' + && !Array.isArray(obj) + && 'value' in obj + && Object.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 (typeof name === 'string') { + prefs = {}; + prefs[name] = { + value: value, + description: description + }; + } + + Object.keys(prefs).forEach(function(k) { + var v = prefs[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 (typeof name === 'string') { + prefs = {}; + prefs[name] = value; + } + + Object.keys(prefs).forEach(function(k) { + var v = prefs[k]; + if (!(k in defaults)) { + throw new Error('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 (typeof val === 'undefined' || val === null || val === '') { + return null; + } + + val = val.split(',').map(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 = {}; + this.getArray(name).forEach(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 (!Array.isArray(name)) { + name = [name]; + } + + name.forEach(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 Object.keys(defaults).sort().map(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) { + Object.keys(json).forEach(function(key) { + this.set(key, json[key]); + }, this); + }, + + /** + * Returns hash of user-modified preferences + * @returns {Object} + */ + exportModified: function() { + return utils.extend({}, 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; + } + }; +}); +},{"../utils/common":72}],29:[function(require,module,exports){ +/** + * Output profile module. + * Profile defines how XHTML output data should look like + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var utils = require('../utils/common'); + var resources = require('./resources'); + var prefs = require('./preferences'); + + prefs.define('profile.allowCompactBoolean', true, + 'This option can be used to globally disable compact form of boolean ' + + 'attribues (attributes where name and value are equal). With compact' + + 'form enabled, HTML tags can be outputted as <div contenteditable> ' + + 'instead of <div contenteditable="contenteditable">'); + + prefs.define('profile.booleanAttributes', '^contenteditable|seamless$', + 'A regular expression for attributes that should be boolean by default.' + + 'If attribute name matches this expression, you don’t have to write dot ' + + 'after attribute name in Emmet abbreviation to mark it as boolean.'); + + 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, + + // Produce compact notation of boolean attribues: + // attributes where name and value are equal. + // With this option enabled, HTML filter will + // produce
instead of
+ compact_bool: false, + + // 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) { + utils.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 + * @returns {String} + */ + selfClosing: function() { + 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 ? utils.getCaretPlaceholder() : ''; + }, + + /** + * Check if attribute with given name is boolean, + * e.g. written as `contenteditable` instead of + * `contenteditable="contenteditable"` + * @param {String} name Attribute name + * @return {Boolean} + */ + isBoolean: function(name, value) { + if (name == value) { + return true; + } + + var boolAttrs = prefs.get('profile.booleanAttributes'); + if (!value && boolAttrs) { + boolAttrs = new RegExp(boolAttrs, 'i'); + return boolAttrs.test(name); + } + + return false; + }, + + /** + * Check if compact boolean attribute record is + * allowed for current profile + * @return {Boolean} + */ + allowCompactBoolean: function() { + return this.compact_bool && prefs.get('profile.allowCompactBoolean'); + } + }; + + /** + * 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, compact_bool: true}); + 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'}); + createProfile('css', {tag_nl: true}); + createProfile('css_line', {tag_nl: false}); + } + + 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(utils.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 = resources.findItem(syntax, 'profile'); + if (profile) { + name = profile; + } + } + + if (!name) { + return profiles.plain; + } + + if (name instanceof OutputProfile) { + return name; + } + + if (typeof name === 'string' && 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 + }; +}); +},{"../utils/common":72,"./preferences":28,"./resources":31}],30:[function(require,module,exports){ +/** + * Helper module to work with ranges + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + 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 (typeof start === 'object' && '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 (Array.isArray(start)) { + this.start = start[0]; + this.end = start[1]; + } else { + len = typeof len === 'string' ? 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 delta + * @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(r, '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.valueOf(); + }, + + valueOf: function() { + return '{' + this.start + ', ' + this.length() + '}'; + } + }; + + /** + * 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} + */ + module.exports = function(start, len) { + if (typeof start == 'undefined' || start === null) + return null; + + if (start instanceof Range) + return start; + + if (typeof start == 'object' && 'start' in start && 'end' in start) { + len = start.end - start.start; + start = start.start; + } + + return new Range(start, len); + }; + + module.exports.create = module.exports; + + module.exports.isRange = function(val) { + return val instanceof Range; + }; + + /** + * Range object factory, the same as this.create() + * but last argument represents end of range, not length + * @returns {Range} + */ + module.exports.create2 = function(start, end) { + if (typeof start === 'number' && typeof end === 'number') { + end -= start; + } + + return this.create(start, end); + }; + + /** + * Helper function that sorts ranges in order as they + * appear in text + * @param {Array} ranges + * @return {Array} + */ + module.exports.sort = function(ranges, reverse) { + ranges = ranges.sort(function(a, b) { + if (a.start === b.start) { + return b.end - a.end; + } + + return a.start - b.start; + }); + + reverse && ranges.reverse(); + return ranges; + }; + + return module.exports; +}); +},{}],31:[function(require,module,exports){ +(function (__dirname){ +/** + * 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 + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var handlerList = require('./handlerList'); + var utils = require('../utils/common'); + var elements = require('./elements'); + var logger = require('../assets/logger'); + var stringScore = require('../vendor/stringScore'); + var cssResolver = require('../resolver/css'); + + 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 = handlerList.create(); + + function each(obj, fn) { + if (!obj) { + return; + } + + Object.keys(obj).forEach(function(key) { + fn(obj[key], key); + }); + } + + /** + * Normalizes caret plceholder in passed text: replaces | character with + * default caret placeholder + * @param {String} text + * @returns {String} + */ + function normalizeCaretPlaceholder(text) { + return utils.replaceUnescapedSymbol(text, '|', utils.getCaretPlaceholder()); + } + + function parseItem(name, value, type) { + value = normalizeCaretPlaceholder(value); + + if (type == 'snippets') { + return 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 = utils.trim(key); + 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, '-'); + } + + function expandSnippetsDefinition(snippets) { + var out = {}; + each(snippets, function(val, key) { + var items = key.split('|'); + // do not use iterators for better performance + for (var i = items.length - 1; i >= 0; i--) { + out[items[i]] = val; + } + }); + + return out; + } + + utils.extend(exports, { + /** + * 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 = {}; + + // sections like "snippets" and "abbreviations" could have + // definitions like `"f|fs": "fieldset"` which is the same as distinct + // "f" and "fs" keys both equals to "fieldset". + // We should parse these definitions first + var voc = {}; + each(data, function(section, syntax) { + var _section = {}; + each(section, function(subsection, name) { + if (name == 'abbreviations' || name == 'snippets') { + subsection = expandSnippetsDefinition(subsection); + } + _section[name] = subsection; + }); + + voc[syntax] = _section; + }); + + + if (type == VOC_SYSTEM) { + systemSettings = voc; + } else { + userSettings = voc; + } + }, + + /** + * 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, utils.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] = utils.deepMerge({}, systemSettings[name], userSettings[name]); + } + + var data = cache[name], subsections = utils.toArray(arguments, 1), 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; + ['snippets', 'abbreviations'].some(function(sectionName) { + var data = this.getSection(syntax, sectionName); + if (data) { + return names.some(function(n) { + if (data[n]) { + return matchedItem = parseItem(n, data[n], sectionName); + } + }); + } + }, this); + + memo.push(syntax); + if (!matchedItem && data['extends'] && !~memo.indexOf(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) { + var result = this.fuzzyFindMatches(syntax, name, minScore)[0]; + if (result) { + return result.value.parsedValue; + } + }, + + fuzzyFindMatches: function(syntax, name, minScore) { + minScore = minScore || 0.3; + name = normalizeName(name); + var snippets = this.getAllSnippets(syntax); + + return Object.keys(snippets) + .map(function(key) { + var value = snippets[key]; + return { + key: key, + score: stringScore.score(value.nk, name, 0.1), + value: value + }; + }) + .filter(function(item) { + return item.score >= minScore; + }) + .sort(function(a, b) { + return a.score - b.score; + }) + .reverse(); + }, + + /** + * 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; + + ['snippets', 'abbreviations'].forEach(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 && !~memo.indexOf(sectionKey)); + + + cache[cacheKey] = utils.extend.apply(utils, stack.reverse()); + } + + return cache[cacheKey]; + }, + + /** + * Returns newline character + * @returns {String} + */ + getNewline: function() { + var nl = this.getVariable('newline'); + return typeof nl === 'string' ? nl : '\n'; + }, + + /** + * Sets new newline character that will be used in output + * @param {String} str + */ + setNewline: function(str) { + this.setVariable('newline', str); + this.setVariable('nl', str); + } + }); + + // XXX add default resolvers + exports.addResolver(cssResolver.resolve.bind(cssResolver)); + + // try to load snippets + // hide it from Require.JS parser + (function(r) { + if (typeof define === 'undefined' || !define.amd) { + try { + var fs = r('fs'); + var path = r('path'); + + var defaultSnippets = fs.readFileSync(path.join(__dirname, '../snippets.json'), {encoding: 'utf8'}); + exports.setVocabulary(JSON.parse(defaultSnippets), VOC_SYSTEM); + } catch (e) {} + } + })(require); + + + return exports; +}); +}).call(this,"/node_modules/emmet/lib/assets") +},{"../assets/logger":27,"../resolver/css":63,"../utils/common":72,"../vendor/stringScore":78,"./elements":24,"./handlerList":25}],32:[function(require,module,exports){ +/** + * A trimmed version of CodeMirror's StringStream module for string parsing + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + /** + * @type StringStream + * @constructor + * @param {String} string Assuming that bound string should be + * immutable + */ + function StringStream(string) { + this.pos = this.start = 0; + this.string = string; + this._length = string.length; + } + + StringStream.prototype = { + /** + * Returns true only if the stream is at the end of the line. + * @returns {Boolean} + */ + eol: function() { + return this.pos >= this._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._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._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, skipString) { + var braceCount = 0, ch; + var pos = this.pos, len = this._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; + } + } else if (skipString && (ch == '"' || ch == "'")) { + this.skipString(ch); + } + } + + return false; + }, + + /** + * A helper function which, in case of either single or + * double quote was found in current position, skips entire + * string (quoted value) + * @return {Boolean} Wether quoted string was skipped + */ + skipQuoted: function(noBackup) { + var ch = this.string.charAt(noBackup ? this.pos : this.pos - 1); + if (ch === '"' || ch === "'") { + if (noBackup) { + this.pos++; + } + return this.skipString(ch); + } + }, + + /** + * A custom function to skip string literal, e.g. a "double-quoted" + * or 'single-quoted' value + * @param {String} quote An opening quote + * @return {Boolean} + */ + skipString: function(quote) { + var pos = this.pos, len = this._length, ch; + while (pos < len) { + ch = this.string.charAt(pos++); + if (ch == '\\') { + continue; + } else if (ch == quote) { + 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(backUp) { + return this.string.slice(this.start, this.pos - (backUp ? 1 : 0)); + } + }; + + module.exports = function(string) { + return new StringStream(string); + }; + + /** @deprecated */ + module.exports.create = module.exports; + return module.exports; +}); +},{}],33:[function(require,module,exports){ +/** + * 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. + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + var utils = require('../utils/common'); + var stringStream = require('./stringStream'); + var resources = require('./resources'); + + /** + * 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; + } + }; + + 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 placeholders = {carets: ''}; + var marks = []; + + options = utils.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 = '', lastIx = 0; + var tabStops = marks.map(function(mark) { + buf += text.substring(lastIx, mark.start); + + var pos = buf.length; + var ph = placeholders[mark.group] || ''; + + buf += ph; + lastIx = mark.end; + + return { + group: mark.group, + start: pos, + end: pos + ph.length + }; + }); + + buf += text.substring(lastIx); + + return { + text: buf, + tabstops: tabStops.sort(function(a, b) { + return a.start - b.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 = utils.extend({}, defaultOptions, options); + + var buf = ''; + /** @type StringStream */ + var stream = stringStream.create(text); + var ch, m, a; + + while ((ch = stream.next())) { + if (ch == '\\' && !stream.eol()) { + // handle escaped character + buf += 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 += a; + } + + return buf; + }, + + /** + * 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, 10); + if (group > maxNum) maxNum = group; + + if (data.placeholder) + return '${' + (group + offset) + ':' + data.placeholder + '}'; + else + return '${' + (group + offset) + '}'; + } + }; + + ['start', 'end', 'content'].forEach(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 = {}; + 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 utils.getCaretPlaceholder(); + } + + var attr = node.attribute(varName); + if (typeof attr !== 'undefined' && attr !== str) { + return attr; + } + + var varValue = resources.getVariable(varName); + if (varValue) { + return varValue; + } + + // output as placeholder + if (!placeholderMemo[varName]) { + placeholderMemo[varName] = startPlaceholderNum++; + } + + return '${' + placeholderMemo[varName] + ':' + varName + '}'; + }; + }, + + /** + * 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 = typeof vars === 'function' ? vars : function(str, p1) { + return p1 in vars ? vars[p1] : null; + }; + + return this.processText(str, { + variable: function(data) { + var newValue = resolver(data.token, data.name, data); + if (newValue === null) { + // try to find variable in resources + newValue = resources.getVariable(data.name); + } + + if (newValue === null || typeof newValue === 'undefined') + // nothing found, return token itself + newValue = data.token; + return newValue; + } + }); + }, + + /** + * 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; + }, + + /** + * Output processor for abbreviation parser that will upgrade tabstops + * of parsed node in order to prevent tabstop index conflicts + */ + abbrOutputProcessor: function(text, node, type) { + var maxNum = 0; + var that = this; + + var tsOptions = { + tabstop: function(data) { + var group = parseInt(data.group, 10); + if (group === 0) + return '${0}'; + + if (group > maxNum) maxNum = group; + if (data.placeholder) { + // respect nested placeholders + var ix = group + tabstopIndex; + var placeholder = that.processText(data.placeholder, tsOptions); + return '${' + ix + ':' + placeholder + '}'; + } else { + return '${' + (group + tabstopIndex) + '}'; + } + } + }; + + // upgrade tabstops + text = this.processText(text, tsOptions); + + // resolve variables + text = this.replaceVariables(text, this.variablesResolver(node)); + + tabstopIndex += maxNum + 1; + return text; + } + }; +}); +},{"../utils/common":72,"./resources":31,"./stringStream":32}],34:[function(require,module,exports){ +/** + * Helper class for convenient token iteration + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function (factory) { + module.exports = factory(require, exports, module); + }; +} + +define(function(require, exports, module) { + /** + * @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; + } else { + this._i = this._il; + } + + return null; + }, + + current: function() { + return this.tokens[this._i]; + }, + + peek: function() { + return this.tokens[this._i + i]; + }, + + position: function() { + return this._position; + }, + + hasNext: function() { + return this._i < this._il - 1; + }, + + reset: function() { + this._i = 0; + 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 = typeof type == 'string' + ? 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); + } + }; +}); +},{}],35:[function(require,module,exports){ +module.exports={ + "eras": { + "e-26": "26 versions back", + "e-25": "25 versions back", + "e-24": "24 versions back", + "e-23": "23 versions back", + "e-22": "22 versions back", + "e-21": "21 versions back", + "e-20": "20 versions back", + "e-19": "19 versions back", + "e-18": "18 versions back", + "e-17": "17 versions back", + "e-16": "16 versions back", + "e-15": "15 versions back", + "e-14": "14 versions back", + "e-13": "13 versions back", + "e-12": "12 versions back", + "e-11": "11 versions back", + "e-10": "10 versions back", + "e-9": "9 versions back", + "e-8": "8 versions back", + "e-7": "7 versions back", + "e-6": "6 versions back", + "e-5": "5 versions back", + "e-4": "4 versions back", + "e-3": "3 versions back", + "e-2": "2 versions back", + "e-1": "Previous version", + "e0": "Current", + "e1": "Near future", + "e2": "Farther future" + }, + "agents": { + "ie": { + "browser": "IE", + "abbr": "IE", + "prefix": "ms", + "type": "desktop", + "usage_global": { + "10": 10.7866, + "11": 0.114751, + "5.5": 0.009298, + "6": 0.204912, + "7": 0.508182, + "8": 8.31124, + "9": 5.21297 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "5.5", "6", "7", "8", "9", "10", "11", null, null], + "current_version": "" + }, + "firefox": { + "browser": "Firefox", + "abbr": "FF", + "prefix": "moz", + "type": "desktop", + "usage_global": { + "10": 0.112406, + "11": 0.088319, + "12": 0.208754, + "13": 0.096348, + "14": 0.096348, + "15": 0.136493, + "16": 0.264957, + "17": 0.192696, + "18": 0.112406, + "19": 0.128464, + "2": 0.016058, + "20": 0.16058, + "21": 0.216783, + "22": 0.256928, + "23": 0.907277, + "24": 11.0318, + "25": 0.529914, + "26": 0.016058, + "27": 0.016058, + "3": 0.088319, + "3.5": 0.040145, + "3.6": 0.305102, + "4": 0.072261, + "5": 0.048174, + "6": 0.048174, + "7": 0.040145, + "8": 0.072261, + "9": 0.056203 + }, + "versions": [null, "2", "3", "3.5", "3.6", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27"], + "current_version": "" + }, + "chrome": { + "browser": "Chrome", + "abbr": "Chr.", + "prefix": "webkit", + "type": "desktop", + "usage_global": { + "10": 0.048174, + "11": 0.112406, + "12": 0.064232, + "13": 0.056203, + "14": 0.056203, + "15": 0.072261, + "16": 0.048174, + "17": 0.040145, + "18": 0.08029, + "19": 0.040145, + "20": 0.040145, + "21": 0.48174, + "22": 0.248899, + "23": 0.216783, + "24": 0.200725, + "25": 0.361305, + "26": 0.353276, + "27": 0.369334, + "28": 0.610204, + "29": 5.08236, + "30": 24.6089, + "31": 0.16058, + "32": 0.064232, + "4": 0.024087, + "5": 0.024087, + "6": 0.032116, + "7": 0.024087, + "8": 0.032116, + "9": 0.024087 + }, + "versions": ["4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32"], + "current_version": "" + }, + "safari": { + "browser": "Safari", + "abbr": "Saf.", + "prefix": "webkit", + "type": "desktop", + "usage_global": { + "3.1": 0, + "3.2": 0.008692, + "4": 0.104377, + "5": 0.305102, + "5.1": 1.28464, + "6": 2.04739, + "6.1": 0.064232, + "7": 0.16058 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "3.1", "3.2", "4", "5", "5.1", "6", "6.1", "7", null, null], + "current_version": "" + }, + "opera": { + "browser": "Opera", + "abbr": "Op.", + "prefix": "o", + "type": "desktop", + "usage_global": { + "10.0-10.1": 0.016058, + "10.5": 0.008392, + "10.6": 0.008029, + "11": 0.008219, + "11.1": 0.008219, + "11.5": 0.016058, + "11.6": 0.032116, + "12": 0.040145, + "12.1": 0.48174, + "15": 0.032116, + "16": 0.104377, + "17": 0.16058, + "18": 0, + "9.5-9.6": 0.008219 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, "9.5-9.6", "10.0-10.1", "10.5", "10.6", "11", "11.1", "11.5", "11.6", "12", "12.1", "15", "16", "17", "18", null], + "current_version": "", + "prefix_exceptions": { + "15": "webkit", + "16": "webkit", + "17": "webkit", + "18": "webkit" + } + }, + "ios_saf": { + "browser": "iOS Safari", + "abbr": "iOS", + "prefix": "webkit", + "type": "mobile", + "usage_global": { + "3.2": 0.00400113, + "4.0-4.1": 0.00800226, + "4.2-4.3": 0.0280079, + "5.0-5.1": 0.28408, + "6.0-6.1": 1.15633, + "7.0": 2.52071 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "3.2", "4.0-4.1", "4.2-4.3", "5.0-5.1", "6.0-6.1", "7.0", null, null], + "current_version": "" + }, + "op_mini": { + "browser": "Opera Mini", + "abbr": "O.Mini", + "prefix": "o", + "type": "mobile", + "usage_global": { + "5.0-7.0": 4.58374 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "5.0-7.0", null, null], + "current_version": "" + }, + "android": { + "browser": "Android Browser", + "abbr": "And.", + "prefix": "webkit", + "type": "mobile", + "usage_global": { + "2.1": 0.0251229, + "2.2": 0.0854178, + "2.3": 1.32146, + "3": 0.00502458, + "4": 0.994867, + "4.1": 1.87417, + "4.2-4.3": 0.743638, + "4.4": 0 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "2.1", "2.2", "2.3", "3", "4", "4.1", "4.2-4.3", "4.4", null], + "current_version": "" + }, + "op_mob": { + "browser": "Opera Mobile", + "abbr": "O.Mob", + "prefix": "o", + "type": "mobile", + "usage_global": { + "0": 0, + "10": 0, + "11.5": 0.00726525, + "12": 0.0363263, + "12.1": 0.101714 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "10", null, null, "11.5", "12", "12.1", "0", null, null], + "current_version": "16", + "prefix_exceptions": { + "0": "webkit" + } + }, + "bb": { + "browser": "Blackberry Browser", + "abbr": "BB", + "prefix": "webkit", + "type": "mobile", + "usage_global": { + "10": 0, + "7": 0.141419 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "7", "10", null, null], + "current_version": "" + }, + "and_chr": { + "browser": "Chrome for Android", + "abbr": "Chr/And.", + "prefix": "webkit", + "type": "mobile", + "usage_global": { + "0": 1.38176 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "0", null, null], + "current_version": "30" + }, + "and_ff": { + "browser": "Firefox for Android", + "abbr": "FF/And.", + "prefix": "moz", + "type": "mobile", + "usage_global": { + "0": 0.070956 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "0", null, null], + "current_version": "25" + }, + "ie_mob": { + "browser": "IE Mobile", + "abbr": "IE.Mob", + "prefix": "ms", + "type": "mobile", + "usage_global": { + "10": 0.205595 + }, + "versions": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "10", null, null], + "current_version": "" + } + }, + "statuses": { + "rec": "Recommendation", + "pr": "Proposed Recommendation", + "cr": "Candidate Recommendation", + "wd": "Working Draft", + "other": "Other", + "unoff": "Unofficial / Note" + }, + "cats": { + "CSS": ["CSS", "CSS2", "CSS3"], + "HTML5": ["Canvas", "HTML5"], + "JS API": ["JS API"], + "Other": ["PNG", "Other", "DOM"], + "SVG": ["SVG"] + }, + "updated": 1383587152, + "data": { + "png-alpha": { + "title": "PNG alpha transparency", + "description": "Semi-transparent areas in PNG files", + "spec": "http://www.w3.org/TR/PNG/", + "status": "rec", + "links": [{ + "url": "http://dillerdesign.com/experiment/DD_belatedPNG/", + "title": "Workaround for IE6" + }, { + "url": "http://en.wikipedia.org/wiki/Portable_Network_Graphics", + "title": "Wikipedia" + }], + "categories": ["PNG"], + "stats": { + "ie": { + "5.5": "n", + "6": "p", + "7": "y", + "8": "y", + "9": "y", + "10": "y", + "11": "y" + }, + "firefox": { + "2": "y", + "3": "y", + "3.5": "y", + "3.6": "y", + "4": "y", + "5": "y", + "6": "y", + "7": "y", + "8": "y", + "9": "y", + "10": "y", + "11": "y", + "12": "y", + "13": "y", + "14": "y", + "15": "y", + "16": "y", + "17": "y", + "18": "y", + "19": "y", + "20": "y", + "21": "y", + "22": "y", + "23": "y", + "24": "y", + "25": "y", + "26": "y", + "27": "y" + }, + "chrome": { + "4": "y", + "5": "y", + "6": "y", + "7": "y", + "8": "y", + "9": "y", + "10": "y", + "11": "y", + "12": "y", + "13": "y", + "14": "y", + "15": "y", + "16": "y", + "17": "y", + "18": "y", + "19": "y", + "20": "y", + "21": "y", + "22": "y", + "23": "y", + "24": "y", + "25": "y", + "26": "y", + "27": "y", + "28": "y", + "29": "y", + "30": "y", + "31": "y", + "32": "y" + }, + "safari": { + "3.1": "y", + "3.2": "y", + "4": "y", + "5": "y", + "5.1": "y", + "6": "y", + "6.1": "y", + "7": "y" + }, + "opera": { + "9": "y", + "9.5-9.6": "y", + "10.0-10.1": "y", + "10.5": "y", + "10.6": "y", + "11": "y", + "11.1": "y", + "11.5": "y", + "11.6": "y", + "12": "y", + "12.1": "y", + "15": "y", + "16": "y", + "17": "y", + "18": "y" + }, + "ios_saf": { + "3.2": "y", + "4.0-4.1": "y", + "4.2-4.3": "y", + "5.0-5.1": "y", + "6.0-6.1": "y", + "7.0": "y" + }, + "op_mini": { + "5.0-7.0": "y" + }, + "android": { + "2.1": "y", + "2.2": "y", + "2.3": "y", + "3": "y", + "4": "y", + "4.1": "y", + "4.2-4.3": "y", + "4.4": "y" + }, + "bb": { + "7": "y", + "10": "y" + }, + "op_mob": { + "10": "y", + "11": "y", + "11.1": "y", + "11.5": "y", + "12": "y", + "12.1": "y", + "0": "y" + }, + "and_chr": { + "0": "y" + }, + "and_ff": { + "0": "y" + }, + "ie_mob": { + "10": "y" + } + }, + "notes": "IE6 does support full transparency in 8-bit PNGs, which can sometimes be an alternative to 24-bit PNGs.", + "usage_perc_y": 94.36, + "usage_perc_a": 0, + "ucprefix": false, + "parent": "", + "keywords": "" + }, + "apng": { + "title": "Animated PNG (APNG)", + "description": "Like animated GIFs, but allowing 24-bit colors and alpha transparency", + "spec": "https://wiki.mozilla.org/APNG_Specification", + "status": "unoff", + "links": [{ + "url": "http://en.wikipedia.org/wiki/APNG", + "title": "Wikipedia" + }, { + "url": "https://github.com/davidmz/apng-canvas", + "title": "Polyfill using canvas" + }, { + "url": "https://chrome.google.com/webstore/detail/ehkepjiconegkhpodgoaeamnpckdbblp", + "title": "Chrome extension providing support" + }, { + "url": "http://www.truekolor.net/learn-how-to-create-an-animated-png/", + "title": "APNG tutorial" + }], + "categories": ["PNG"], + "stats": { + "ie": { + "5.5": "n", + "6": "n", + "7": "n", + "8": "n", + "9": "n", + "10": "n", + "11": "n" + }, + "firefox": { + "2": "n", + "3": "y", + "3.5": "y", + "3.6": "y", + "4": "y", + "5": "y", + "6": "y", + "7": "y", + "8": "y", + "9": "y", + "10": "y", + "11": "y", + "12": "y", + "13": "y", + "14": "y", + "15": "y", + "16": "y", + "17": "y", + "18": "y", + "19": "y", + "20": "y", + "21": "y", + "22": "y", + "23": "y", + "24": "y", + "25": "y", + "26": "y", + "27": "y" + }, + "chrome": { + "4": "n", + "5": "n", + "6": "n", + "7": "n", + "8": "n", + "9": "n", + "10": "n", + "11": "n", + "12": "n", + "13": "n", + "14": "n", + "15": "n", + "16": "n", + "17": "n", + "18": "n", + "19": "n", + "20": "n", + "21": "n", + "22": "n", + "23": "n", + "24": "n", + "25": "n", + "26": "n", + "27": "n", + "28": "n", + "29": "n", + "30": "n", + "31": "n", + "32": "n" + }, + "safari": { + "3.1": "n", + "3.2": "n", + "4": "n", + "5": "n", + "5.1": "n", + "6": "n", + "6.1": "n", + "7": "n" + }, + "opera": { + "9": "n", + "9.5-9.6": "y", + "10.0-10.1": "y", + "10.5": "y", + "10.6": "y", + "11": "y", + "11.1": "y", + "11.5": "y", + "11.6": "y", + "12": "y", + "12.1": "y", + "15": "n", + "16": "n", + "17": "n", + "18": "n" + }, + "ios_saf": { + "3.2": "n", + "4.0-4.1": "n", + "4.2-4.3": "n", + "5.0-5.1": "n", + "6.0-6.1": "n", + "7.0": "n" + }, + "op_mini": { + "5.0-7.0": "n" + }, + "android": { + "2.1": "n", + "2.2": "n", + "2.3": "n", + "3": "n", + "4": "n", + "4.1": "n", + "4.2-4.3": "n", + "4.4": "n" + }, + "bb": { + "7": "n", + "10": "n" + }, + "op_mob": { + "10": "y", + "11": "y", + "11.1": "y", + "11.5": "y", + "12": "y", + "12.1": "y", + "0": "n" + }, + "and_chr": { + "0": "n" + }, + "and_ff": { + "0": "y" + }, + "ie_mob": { + "10": "n" + } + }, + "notes": "Where support for APNG is missing, only the first frame is displayed", + "usage_perc_y": 16.19, + "usage_perc_a": 0, + "ucprefix": false, + "parent": "", + "keywords": "" + }, + "video": { + "title": "Video element", + "description": "Method of playing videos on webpages (without requiring a plug-in)", + "spec": "http://www.whatwg.org/specs/web-apps/current-work/multipage/video.html#video", + "status": "wd", + "links": [{ + "url": "https://raw.github.com/phiggins42/has.js/master/detect/video.js#video", + "title": "has.js test" + }, { + "url": "http://webmproject.org", + "title": "WebM format information" + }, { + "url": "http://docs.webplatform.org/wiki/html/elements/video", + "title": "WebPlatform Docs" + }, { + "url": "http://camendesign.co.uk/code/video_for_everybody", + "title": "Video for Everybody" + }, { + "url": "http://diveinto.org/html5/video.html", + "title": "Video on the Web - includes info on Android support" + }, { + "url": "http://dev.opera.com/articles/view/everything-you-need-to-know-about-html5-video-and-audio/", + "title": "Detailed article on video/audio elements" + }], + "categories": ["HTML5"], + "stats": { + "ie": { + "5.5": "n", + "6": "n", + "7": "n", + "8": "n", + "9": "y", + "10": "y", + "11": "y" + }, + "firefox": { + "2": "n", + "3": "n", + "3.5": "y", + "3.6": "y", + "4": "y", + "5": "y", + "6": "y", + "7": "y", + "8": "y", + "9": "y", + "10": "y", + "11": "y", + "12": "y", + "13": "y", + "14": "y", + "15": "y", + "16": "y", + "17": "y", + "18": "y", + "19": "y", + "20": "y", + "21": "y", + "22": "y", + "23": "y", + "24": "y", + "25": "y", + "26": "y", + "27": "y" + }, + "chrome": { + "4": "y", + "5": "y", + "6": "y", + "7": "y", + "8": "y", + "9": "y", + "10": "y", + "11": "y", + "12": "y", + "13": "y", + "14": "y", + "15": "y", + "16": "y", + "17": "y", + "18": "y", + "19": "y", + "20": "y", + "21": "y", + "22": "y", + "23": "y", + "24": "y", + "25": "y", + "26": "y", + "27": "y", + "28": "y", + "29": "y", + "30": "y", + "31": "y", + "32": "y" + }, + "safari": { + "3.1": "n", + "3.2": "n", + "4": "y", + "5": "y", + "5.1": "y", + "6": "y", + "6.1": "y", + "7": "y" + }, + "opera": { + "9": "n", + "9.5-9.6": "n", + "10.0-10.1": "n", + "10.5": "y", + "10.6": "y", + "11": "y", + "11.1": "y", + "11.5": "y", + "11.6": "y", + "12": "y", + "12.1": "y", + "15": "y", + "16": "y", + "17": "y", + "18": "y" + }, + "ios_saf": { + "3.2": "y", + "4.0-4.1": "y", + "4.2-4.3": "y", + "5.0-5.1": "y", + "6.0-6.1": "y", + "7.0": "y" + }, + "op_mini": { + "5.0-7.0": "n" + }, + "android": { + "2.1": "a", + "2.2": "a", + "2.3": "y", + "3": "y", + "4": "y", + "4.1": "y", + "4.2-4.3": "y", + "4.4": "y" + }, + "bb": { + "7": "y", + "10": "y" + }, + "op_mob": { + "10": "n", + "11": "y", + "11.1": "y", + "11.5": "y", + "12": "y", + "12.1": "y", + "0": "y" + }, + "and_chr": { + "0": "y" + }, + "and_ff": { + "0": "y" + }, + "ie_mob": { + "10": "y" + } + }, + "notes": "Different browsers have support for different video formats, see sub-features for details. \r\n\r\nThe Android browser (before 2.3) requires specific handling to run the video element.", + "usage_perc_y": 80.71, + "usage_perc_a": 0.11, + "ucprefix": false, + "parent": "", + "keywords": "