jasmine-fixture
Version:
Makes injecting HTML snippets into the DOM easy & clean!
1,597 lines (1,392 loc) • 468 kB
JavaScript
/* jasmine-fixture - 2.0.0
* Makes injecting HTML snippets into the DOM easy & clean!
* https://github.com/searls/jasmine-fixture
*/
(function(){
var root = (1, eval)('this');
if(root.hasOwnProperty('emmet')) {
root.__jasmineFixtureEmmetNoConflict = root.emmet;
}
})()
// The MIT License (MIT)
//
// Copyright (c) 2012 Sergey Chikuyonok <serge.che@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in the
// Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.emmet=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<r.length;o++)s(r[o]);return s})({"./lib/emmet.js":[function(require,module,exports){
if (typeof module === 'object' && typeof define !== 'function') {
var define = function (factory) {
module.exports = factory(require, exports, module);
};
}
define(function(require, exports, module) {
var global = typeof self != 'undefined' ? self : this;
var utils = require('./utils/common');
var actions = require('./action/main');
var parser = require('./parser/abbreviation');
var file = require('./plugin/file');
var preferences = require('./assets/preferences');
var resources = require('./assets/resources');
var profile = require('./assets/profile');
var ciu = require('./assets/caniuse');
var logger = require('./assets/logger');
var sliceFn = Array.prototype.slice;
/**
* Returns file name part from path
* @param {String} path Path to file
* @return {String}
*/
function getFileName(path) {
var re = /([\w\.\-]+)$/i;
var m = re.exec(path);
return m ? m[1] : '';
}
/**
* Normalizes profile definition: converts some
* properties to valid data types
* @param {Object} profile
* @return {Object}
*/
function normalizeProfile(profile) {
if (typeof profile === 'object') {
if ('indent' in profile) {
profile.indent = !!profile.indent;
}
if ('self_closing_tag' in profile) {
if (typeof profile.self_closing_tag === 'number') {
profile.self_closing_tag = !!profile.self_closing_tag;
}
}
}
return profile;
}
return {
/**
* The essential function that expands Emmet abbreviation
* @param {String} abbr Abbreviation to parse
* @param {String} syntax Abbreviation's context syntax
* @param {String} profile Output profile (or its name)
* @param {Object} contextNode Contextual node where abbreviation is
* written
* @return {String}
*/
expandAbbreviation: function(abbr, syntax, profile, contextNode) {
return parser.expand(abbr, {
syntax: syntax,
profile: profile,
contextNode: contextNode
});
},
/**
* Runs given action
* @param {String} name Action name
* @param {IEmmetEditor} editor Editor instance
* @return {Boolean} Returns true if action was performed successfully
*/
run: function(name) {
return actions.run.apply(actions, sliceFn.call(arguments, 0));
},
/**
* Loads Emmet extensions. Extensions are simple .js files that
* uses Emmet modules and resources to create new actions, modify
* existing ones etc.
* @param {Array} fileList List of absolute paths to files in extensions
* folder. Back-end app should not filter this list (e.g. by extension)
* but return it "as-is" so bootstrap can decide how to load contents
* of each file.
* This method requires a <code>file</code> module of <code>IEmmetFile</code>
* interface to be implemented.
* @memberOf bootstrap
*/
loadExtensions: function(fileList) {
var payload = {};
var userSnippets = null;
var that = this;
// make sure file list contians only valid extension files
fileList = fileList.filter(function(f) {
var ext = file.getExt(f);
return ext === 'json' || ext === 'js';
});
var reader = (file.readText || file.read).bind(file);
var next = function() {
if (fileList.length) {
var f = fileList.shift();
reader(f, function(err, content) {
if (err) {
logger.log('Unable to read "' + f + '" file: '+ err);
return next();
}
switch (file.getExt(f)) {
case 'js':
try {
eval(content);
} catch (e) {
logger.log('Unable to eval "' + f + '" file: '+ e);
}
break;
case 'json':
var fileName = getFileName(f).toLowerCase().replace(/\.json$/, '');
if (/^snippets/.test(fileName)) {
if (fileName === 'snippets') {
// data in snippets.json is more important to user
userSnippets = utils.parseJSON(content);
} else {
payload.snippets = utils.deepMerge(payload.snippets || {}, utils.parseJSON(content));
}
} else {
payload[fileName] = content;
}
break;
}
next();
});
} else {
// complete
if (userSnippets) {
payload.snippets = utils.deepMerge(payload.snippets || {}, userSnippets);
}
that.loadUserData(payload);
}
};
next();
},
/**
* Loads preferences from JSON object (or string representation of JSON)
* @param {Object} data
* @returns
*/
loadPreferences: function(data) {
preferences.load(utils.parseJSON(data));
},
/**
* Loads user snippets and abbreviations. It doesn’t replace current
* user resource vocabulary but merges it with passed one. If you need
* to <i>replaces</i> user snippets you should call
* <code>resetSnippets()</code> method first
*/
loadSnippets: function(data) {
data = utils.parseJSON(data);
var userData = resources.getVocabulary('user') || {};
resources.setVocabulary(utils.deepMerge(userData, data), 'user');
},
/**
* Helper function that loads default snippets, defined in project’s
* <i>snippets.json</i>
* @param {Object} data
*/
loadSystemSnippets: function(data) {
resources.setVocabulary(utils.parseJSON(data), 'system');
},
/**
* Helper function that loads Can I Use database
* @param {Object} data
*/
loadCIU: function(data) {
ciu.load(utils.parseJSON(data));
},
/**
* Removes all user-defined snippets
*/
resetSnippets: function() {
resources.setVocabulary({}, 'user');
},
/**
* Helper function that loads all user data (snippets and preferences)
* defined as a single JSON object. This is useful for loading data
* stored in a common storage, for example <code>NSUserDefaults</code>
* @param {Object} data
*/
loadUserData: function(data) {
data = utils.parseJSON(data);
if (data.snippets) {
this.loadSnippets(data.snippets);
}
if (data.preferences) {
this.loadPreferences(data.preferences);
}
if (data.profiles) {
this.loadProfiles(data.profiles);
}
if (data.caniuse) {
this.loadCIU(data.caniuse);
}
var profiles = data.syntaxProfiles || data.syntaxprofiles;
if (profiles) {
this.loadSyntaxProfiles(profiles);
}
},
/**
* Resets all user-defined data: preferences, snippets etc.
* @returns
*/
resetUserData: function() {
this.resetSnippets();
preferences.reset();
profile.reset();
},
/**
* Load syntax-specific output profiles. These are essentially
* an extension to syntax snippets
* @param {Object} profiles Dictionary of profiles
*/
loadSyntaxProfiles: function(profiles) {
profiles = utils.parseJSON(profiles);
var snippets = {};
Object.keys(profiles).forEach(function(syntax) {
var options = profiles[syntax];
if (!(syntax in snippets)) {
snippets[syntax] = {};
}
snippets[syntax].profile = normalizeProfile(options);
});
this.loadSnippets(snippets);
},
/**
* Load named profiles
* @param {Object} profiles
*/
loadProfiles: function(profiles) {
profiles = utils.parseJSON(profiles);
Object.keys(profiles).forEach(function(name) {
profile.create(name, normalizeProfile(profiles[name]));
});
},
require: require,
// expose some useful data for plugin authors
actions: actions,
parser: parser,
file: file,
preferences: preferences,
resources: resources,
profile: profile,
tabStops: require('./assets/tabStops'),
htmlMatcher: require('./assets/htmlMatcher'),
utils: {
common: utils,
action: require('./utils/action'),
editor: require('./utils/editor')
}
};
});
},{"./action/main":"action/main.js","./assets/caniuse":"assets/caniuse.js","./assets/htmlMatcher":"assets/htmlMatcher.js","./assets/logger":"assets/logger.js","./assets/preferences":"assets/preferences.js","./assets/profile":"assets/profile.js","./assets/resources":"assets/resources.js","./assets/tabStops":"assets/tabStops.js","./parser/abbreviation":"parser/abbreviation.js","./plugin/file":"plugin/file.js","./utils/action":"utils/action.js","./utils/common":"utils/common.js","./utils/editor":"utils/editor.js"}],"action/balance.js":[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":"assets/htmlMatcher.js","../assets/range":"assets/range.js","../editTree/css":"editTree/css.js","../utils/action":"utils/action.js","../utils/common":"utils/common.js","../utils/cssSections":"utils/cssSections.js","../utils/editor":"utils/editor.js"}],"action/base64.js":[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 <code>text</code> starts with <code>token</code> at <code>pos</code>
* position. If <code>pos</code> 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 <img src="">
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":"plugin/file.js","../utils/action":"utils/action.js","../utils/base64":"utils/base64.js","../utils/editor":"utils/editor.js"}],"action/editPoints.js":[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;
}
};
});
},{}],"action/evaluateMath.js":[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":"assets/range.js","../utils/action":"utils/action.js","../utils/common":"utils/common.js","../utils/math":"utils/math.js"}],"action/expandAbbreviation.js":[function(require,module,exports){
/**
* 'Expand abbreviation' editor action: extracts abbreviation from current caret
* position and replaces it with formatted output.
* <br><br>
* 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 <code>true</code> 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 <code>true</code> if it was performed successfully,
* <code>false</code> 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":"assets/handlerList.js","../assets/preferences":"assets/preferences.js","../assets/range":"assets/range.js","../parser/abbreviation":"parser/abbreviation.js","../resolver/cssGradient":"resolver/cssGradient.js","../utils/action":"utils/action.js","../utils/common":"utils/common.js","../utils/editor":"utils/editor.js"}],"action/incrementDecrement.js":[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":"utils/action.js","../utils/common":"utils/common.js"}],"action/lineBreaks.js":[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 <code>\\n\\t</code> 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 <code>true</code> 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":"assets/htmlMatcher.js","../assets/preferences":"assets/preferences.js","../assets/resources":"assets/resources.js","../utils/common":"utils/common.js","../utils/editor":"utils/editor.js"}],"action/main.js":[function(require,module,exports){
/**
* Module describes and performs Emmet actions. The actions themselves are
* defined in <i>actions</i> 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:<br>
* <b>label</b> : (<code>String</code>) – Human-readable action name.
* May contain '/' symbols as submenu separators<br>
* <b>hidden</b> : (<code>Boolean</code>) – Indicates whether action
* should be displayed in menu (<code>getMenu()</code> 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 <i>actions</i> 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 <code>IEmmetEditor</code> instance
* @returns {Boolean} Status of performed operation, <code>true</code>
* 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 <i>label</i>,
* 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":"utils/common.js","./balance":"action/balance.js","./base64":"action/base64.js","./editPoints":"action/editPoints.js","./evaluateMath":"action/evaluateMath.js","./expandAbbreviation":"action/expandAbbreviation.js","./incrementDecrement":"action/incrementDecrement.js","./lineBreaks":"action/lineBreaks.js","./mergeLines":"action/mergeLines.js","./reflectCSSValue":"action/reflectCSSValue.js","./removeTag":"action/removeTag.js","./selectItem":"action/selectItem.js","./selectLine":"action/selectLine.js","./splitJoinTag":"action/splitJoinTag.js","./toggleComment":"action/toggleComment.js","./updateImageSize":"action/updateImageSize.js","./updateTag":"action/updateTag.js","./wrapWithAbbreviation":"action/wrapWithAbbreviation.js"}],"action/mergeLines.js":[function(require