4947 lines
182 KiB
JavaScript
4947 lines
182 KiB
JavaScript
(function () {
|
|
// https://github.com/umdjs/umd/blob/master/templates/returnExports.js
|
|
(function (root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
define('documented-method',[], factory);
|
|
} else if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory();
|
|
} else {
|
|
root.DocumentedMethod = factory();
|
|
}
|
|
}(this, function () {
|
|
function extend(target, src) {
|
|
Object.keys(src).forEach(function(prop) {
|
|
target[prop] = src[prop];
|
|
});
|
|
return target;
|
|
}
|
|
|
|
function DocumentedMethod(classitem) {
|
|
extend(this, classitem);
|
|
|
|
if (this.overloads) {
|
|
// Make each overload inherit properties from their parent
|
|
// classitem.
|
|
this.overloads = this.overloads.map(function(overload) {
|
|
return extend(Object.create(this), overload);
|
|
}, this);
|
|
|
|
if (this.params) {
|
|
throw new Error('params for overloaded methods should be undefined');
|
|
}
|
|
|
|
this.params = this._getMergedParams();
|
|
}
|
|
}
|
|
|
|
DocumentedMethod.prototype = {
|
|
// Merge parameters across all overloaded versions of this item.
|
|
_getMergedParams: function() {
|
|
var paramNames = {};
|
|
var params = [];
|
|
|
|
this.overloads.forEach(function(overload) {
|
|
if (!overload.params) {
|
|
return;
|
|
}
|
|
overload.params.forEach(function(param) {
|
|
if (param.name in paramNames) {
|
|
return;
|
|
}
|
|
paramNames[param.name] = param;
|
|
params.push(param);
|
|
});
|
|
});
|
|
|
|
return params;
|
|
}
|
|
};
|
|
|
|
return DocumentedMethod;
|
|
}));
|
|
|
|
/**
|
|
* @license RequireJS text 2.0.10 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
|
|
* Available via the MIT or new BSD license.
|
|
* see: http://github.com/requirejs/text for details
|
|
*/
|
|
/*jslint regexp: true */
|
|
/*global require, XMLHttpRequest, ActiveXObject,
|
|
define, window, process, Packages,
|
|
java, location, Components, FileUtils */
|
|
|
|
define('text',['module'], function (module) {
|
|
'use strict';
|
|
|
|
var text, fs, Cc, Ci, xpcIsWindows,
|
|
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
|
|
xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
|
|
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
|
|
hasLocation = typeof location !== 'undefined' && location.href,
|
|
defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
|
|
defaultHostName = hasLocation && location.hostname,
|
|
defaultPort = hasLocation && (location.port || undefined),
|
|
buildMap = {},
|
|
masterConfig = (module.config && module.config()) || {};
|
|
|
|
text = {
|
|
version: '2.0.10',
|
|
|
|
strip: function (content) {
|
|
//Strips <?xml ...?> declarations so that external SVG and XML
|
|
//documents can be added to a document without worry. Also, if the string
|
|
//is an HTML document, only the part inside the body tag is returned.
|
|
if (content) {
|
|
content = content.replace(xmlRegExp, "");
|
|
var matches = content.match(bodyRegExp);
|
|
if (matches) {
|
|
content = matches[1];
|
|
}
|
|
} else {
|
|
content = "";
|
|
}
|
|
return content;
|
|
},
|
|
|
|
jsEscape: function (content) {
|
|
return content.replace(/(['\\])/g, '\\$1')
|
|
.replace(/[\f]/g, "\\f")
|
|
.replace(/[\b]/g, "\\b")
|
|
.replace(/[\n]/g, "\\n")
|
|
.replace(/[\t]/g, "\\t")
|
|
.replace(/[\r]/g, "\\r")
|
|
.replace(/[\u2028]/g, "\\u2028")
|
|
.replace(/[\u2029]/g, "\\u2029");
|
|
},
|
|
|
|
createXhr: masterConfig.createXhr || function () {
|
|
//Would love to dump the ActiveX crap in here. Need IE 6 to die first.
|
|
var xhr, i, progId;
|
|
if (typeof XMLHttpRequest !== "undefined") {
|
|
return new XMLHttpRequest();
|
|
} else if (typeof ActiveXObject !== "undefined") {
|
|
for (i = 0; i < 3; i += 1) {
|
|
progId = progIds[i];
|
|
try {
|
|
xhr = new ActiveXObject(progId);
|
|
} catch (e) {}
|
|
|
|
if (xhr) {
|
|
progIds = [progId]; // so faster next time
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return xhr;
|
|
},
|
|
|
|
/**
|
|
* Parses a resource name into its component parts. Resource names
|
|
* look like: module/name.ext!strip, where the !strip part is
|
|
* optional.
|
|
* @param {String} name the resource name
|
|
* @returns {Object} with properties "moduleName", "ext" and "strip"
|
|
* where strip is a boolean.
|
|
*/
|
|
parseName: function (name) {
|
|
var modName, ext, temp,
|
|
strip = false,
|
|
index = name.indexOf("."),
|
|
isRelative = name.indexOf('./') === 0 ||
|
|
name.indexOf('../') === 0;
|
|
|
|
if (index !== -1 && (!isRelative || index > 1)) {
|
|
modName = name.substring(0, index);
|
|
ext = name.substring(index + 1, name.length);
|
|
} else {
|
|
modName = name;
|
|
}
|
|
|
|
temp = ext || modName;
|
|
index = temp.indexOf("!");
|
|
if (index !== -1) {
|
|
//Pull off the strip arg.
|
|
strip = temp.substring(index + 1) === "strip";
|
|
temp = temp.substring(0, index);
|
|
if (ext) {
|
|
ext = temp;
|
|
} else {
|
|
modName = temp;
|
|
}
|
|
}
|
|
|
|
return {
|
|
moduleName: modName,
|
|
ext: ext,
|
|
strip: strip
|
|
};
|
|
},
|
|
|
|
xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
|
|
|
|
/**
|
|
* Is an URL on another domain. Only works for browser use, returns
|
|
* false in non-browser environments. Only used to know if an
|
|
* optimized .js version of a text resource should be loaded
|
|
* instead.
|
|
* @param {String} url
|
|
* @returns Boolean
|
|
*/
|
|
useXhr: function (url, protocol, hostname, port) {
|
|
var uProtocol, uHostName, uPort,
|
|
match = text.xdRegExp.exec(url);
|
|
if (!match) {
|
|
return true;
|
|
}
|
|
uProtocol = match[2];
|
|
uHostName = match[3];
|
|
|
|
uHostName = uHostName.split(':');
|
|
uPort = uHostName[1];
|
|
uHostName = uHostName[0];
|
|
|
|
return (!uProtocol || uProtocol === protocol) &&
|
|
(!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) &&
|
|
((!uPort && !uHostName) || uPort === port);
|
|
},
|
|
|
|
finishLoad: function (name, strip, content, onLoad) {
|
|
content = strip ? text.strip(content) : content;
|
|
if (masterConfig.isBuild) {
|
|
buildMap[name] = content;
|
|
}
|
|
onLoad(content);
|
|
},
|
|
|
|
load: function (name, req, onLoad, config) {
|
|
//Name has format: some.module.filext!strip
|
|
//The strip part is optional.
|
|
//if strip is present, then that means only get the string contents
|
|
//inside a body tag in an HTML string. For XML/SVG content it means
|
|
//removing the <?xml ...?> declarations so the content can be inserted
|
|
//into the current doc without problems.
|
|
|
|
// Do not bother with the work if a build and text will
|
|
// not be inlined.
|
|
if (config.isBuild && !config.inlineText) {
|
|
onLoad();
|
|
return;
|
|
}
|
|
|
|
masterConfig.isBuild = config.isBuild;
|
|
|
|
var parsed = text.parseName(name),
|
|
nonStripName = parsed.moduleName +
|
|
(parsed.ext ? '.' + parsed.ext : ''),
|
|
url = req.toUrl(nonStripName),
|
|
useXhr = (masterConfig.useXhr) ||
|
|
text.useXhr;
|
|
|
|
// Do not load if it is an empty: url
|
|
if (url.indexOf('empty:') === 0) {
|
|
onLoad();
|
|
return;
|
|
}
|
|
|
|
//Load the text. Use XHR if possible and in a browser.
|
|
if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) {
|
|
text.get(url, function (content) {
|
|
text.finishLoad(name, parsed.strip, content, onLoad);
|
|
}, function (err) {
|
|
if (onLoad.error) {
|
|
onLoad.error(err);
|
|
}
|
|
});
|
|
} else {
|
|
//Need to fetch the resource across domains. Assume
|
|
//the resource has been optimized into a JS module. Fetch
|
|
//by the module name + extension, but do not include the
|
|
//!strip part to avoid file system issues.
|
|
req([nonStripName], function (content) {
|
|
text.finishLoad(parsed.moduleName + '.' + parsed.ext,
|
|
parsed.strip, content, onLoad);
|
|
});
|
|
}
|
|
},
|
|
|
|
write: function (pluginName, moduleName, write, config) {
|
|
if (buildMap.hasOwnProperty(moduleName)) {
|
|
var content = text.jsEscape(buildMap[moduleName]);
|
|
write.asModule(pluginName + "!" + moduleName,
|
|
"define(function () { return '" +
|
|
content +
|
|
"';});\n");
|
|
}
|
|
},
|
|
|
|
writeFile: function (pluginName, moduleName, req, write, config) {
|
|
var parsed = text.parseName(moduleName),
|
|
extPart = parsed.ext ? '.' + parsed.ext : '',
|
|
nonStripName = parsed.moduleName + extPart,
|
|
//Use a '.js' file name so that it indicates it is a
|
|
//script that can be loaded across domains.
|
|
fileName = req.toUrl(parsed.moduleName + extPart) + '.js';
|
|
|
|
//Leverage own load() method to load plugin value, but only
|
|
//write out values that do not have the strip argument,
|
|
//to avoid any potential issues with ! in file names.
|
|
text.load(nonStripName, req, function (value) {
|
|
//Use own write() method to construct full module value.
|
|
//But need to create shell that translates writeFile's
|
|
//write() to the right interface.
|
|
var textWrite = function (contents) {
|
|
return write(fileName, contents);
|
|
};
|
|
textWrite.asModule = function (moduleName, contents) {
|
|
return write.asModule(moduleName, fileName, contents);
|
|
};
|
|
|
|
text.write(pluginName, nonStripName, textWrite, config);
|
|
}, config);
|
|
}
|
|
};
|
|
|
|
if (masterConfig.env === 'node' || (!masterConfig.env &&
|
|
typeof process !== "undefined" &&
|
|
process.versions &&
|
|
!!process.versions.node &&
|
|
!process.versions['node-webkit'])) {
|
|
//Using special require.nodeRequire, something added by r.js.
|
|
fs = require.nodeRequire('fs');
|
|
|
|
text.get = function (url, callback, errback) {
|
|
try {
|
|
var file = fs.readFileSync(url, 'utf8');
|
|
//Remove BOM (Byte Mark Order) from utf8 files if it is there.
|
|
if (file.indexOf('\uFEFF') === 0) {
|
|
file = file.substring(1);
|
|
}
|
|
callback(file);
|
|
} catch (e) {
|
|
errback(e);
|
|
}
|
|
};
|
|
} else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
|
|
text.createXhr())) {
|
|
text.get = function (url, callback, errback, headers) {
|
|
var xhr = text.createXhr(), header;
|
|
xhr.open('GET', url, true);
|
|
|
|
//Allow plugins direct access to xhr headers
|
|
if (headers) {
|
|
for (header in headers) {
|
|
if (headers.hasOwnProperty(header)) {
|
|
xhr.setRequestHeader(header.toLowerCase(), headers[header]);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Allow overrides specified in config
|
|
if (masterConfig.onXhr) {
|
|
masterConfig.onXhr(xhr, url);
|
|
}
|
|
|
|
xhr.onreadystatechange = function (evt) {
|
|
var status, err;
|
|
//Do not explicitly handle errors, those should be
|
|
//visible via console output in the browser.
|
|
if (xhr.readyState === 4) {
|
|
status = xhr.status;
|
|
if (status > 399 && status < 600) {
|
|
//An http 4xx or 5xx error. Signal an error.
|
|
err = new Error(url + ' HTTP status: ' + status);
|
|
err.xhr = xhr;
|
|
errback(err);
|
|
} else {
|
|
callback(xhr.responseText);
|
|
}
|
|
|
|
if (masterConfig.onXhrComplete) {
|
|
masterConfig.onXhrComplete(xhr, url);
|
|
}
|
|
}
|
|
};
|
|
xhr.send(null);
|
|
};
|
|
} else if (masterConfig.env === 'rhino' || (!masterConfig.env &&
|
|
typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
|
|
//Why Java, why is this so awkward?
|
|
text.get = function (url, callback) {
|
|
var stringBuffer, line,
|
|
encoding = "utf-8",
|
|
file = new java.io.File(url),
|
|
lineSeparator = java.lang.System.getProperty("line.separator"),
|
|
input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)),
|
|
content = '';
|
|
try {
|
|
stringBuffer = new java.lang.StringBuffer();
|
|
line = input.readLine();
|
|
|
|
// Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
|
|
// http://www.unicode.org/faq/utf_bom.html
|
|
|
|
// Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK:
|
|
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
|
|
if (line && line.length() && line.charAt(0) === 0xfeff) {
|
|
// Eat the BOM, since we've already found the encoding on this file,
|
|
// and we plan to concatenating this buffer with others; the BOM should
|
|
// only appear at the top of a file.
|
|
line = line.substring(1);
|
|
}
|
|
|
|
if (line !== null) {
|
|
stringBuffer.append(line);
|
|
}
|
|
|
|
while ((line = input.readLine()) !== null) {
|
|
stringBuffer.append(lineSeparator);
|
|
stringBuffer.append(line);
|
|
}
|
|
//Make sure we return a JavaScript string and not a Java string.
|
|
content = String(stringBuffer.toString()); //String
|
|
} finally {
|
|
input.close();
|
|
}
|
|
callback(content);
|
|
};
|
|
} else if (masterConfig.env === 'xpconnect' || (!masterConfig.env &&
|
|
typeof Components !== 'undefined' && Components.classes &&
|
|
Components.interfaces)) {
|
|
//Avert your gaze!
|
|
Cc = Components.classes,
|
|
Ci = Components.interfaces;
|
|
Components.utils['import']('resource://gre/modules/FileUtils.jsm');
|
|
xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc);
|
|
|
|
text.get = function (url, callback) {
|
|
var inStream, convertStream, fileObj,
|
|
readData = {};
|
|
|
|
if (xpcIsWindows) {
|
|
url = url.replace(/\//g, '\\');
|
|
}
|
|
|
|
fileObj = new FileUtils.File(url);
|
|
|
|
//XPCOM, you so crazy
|
|
try {
|
|
inStream = Cc['@mozilla.org/network/file-input-stream;1']
|
|
.createInstance(Ci.nsIFileInputStream);
|
|
inStream.init(fileObj, 1, 0, false);
|
|
|
|
convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
|
|
.createInstance(Ci.nsIConverterInputStream);
|
|
convertStream.init(inStream, "utf-8", inStream.available(),
|
|
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
|
|
|
convertStream.readString(inStream.available(), readData);
|
|
convertStream.close();
|
|
inStream.close();
|
|
callback(readData.value);
|
|
} catch (e) {
|
|
throw new Error((fileObj && fileObj.path || '') + ': ' + e);
|
|
}
|
|
};
|
|
}
|
|
return text;
|
|
});
|
|
|
|
|
|
define('text!tpl/search.html',[],function () { return '<h2 class="sr-only">search</h2>\n<form>\n <input id="search_reference_field" type="text" class="<%=className%>" value="" placeholder="<%=placeholder%>" aria-label="search reference">\n <label class="sr-only" for="search_reference_field">Search reference</label>\n</form>\n\n';});
|
|
|
|
|
|
define('text!tpl/search_suggestion.html',[],function () { return '<p id="index-<%=idx%>" class="search-suggestion">\n\n <strong><%=name%></strong>\n\n <span class="small">\n <% if (final) { %>\n constant\n <% } else if (itemtype) { %>\n <%=itemtype%> \n <% } %>\n\n <% if (className) { %>\n in <strong><%=className%></strong>\n <% } %>\n\n <% if (typeof is_constructor !== \'undefined\' && is_constructor) { %>\n <strong><span class="glyphicon glyphicon-star"></span> constructor</strong>\n <% } %>\n </span>\n\n</p>';});
|
|
|
|
/*!
|
|
* typeahead.js 0.10.2
|
|
* https://github.com/twitter/typeahead.js
|
|
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
|
|
*/
|
|
define('typeahead',[], function() {
|
|
|
|
//(function($) {
|
|
|
|
|
|
var _ = {
|
|
isMsie: function() {
|
|
return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
|
|
},
|
|
isBlankString: function(str) {
|
|
return !str || /^\s*$/.test(str);
|
|
},
|
|
escapeRegExChars: function(str) {
|
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
|
},
|
|
isString: function(obj) {
|
|
return typeof obj === "string";
|
|
},
|
|
isNumber: function(obj) {
|
|
return typeof obj === "number";
|
|
},
|
|
isArray: $.isArray,
|
|
isFunction: $.isFunction,
|
|
isObject: $.isPlainObject,
|
|
isUndefined: function(obj) {
|
|
return typeof obj === "undefined";
|
|
},
|
|
bind: $.proxy,
|
|
each: function(collection, cb) {
|
|
$.each(collection, reverseArgs);
|
|
function reverseArgs(index, value) {
|
|
return cb(value, index);
|
|
}
|
|
},
|
|
map: $.map,
|
|
filter: $.grep,
|
|
every: function(obj, test) {
|
|
var result = true;
|
|
if (!obj) {
|
|
return result;
|
|
}
|
|
$.each(obj, function(key, val) {
|
|
if (!(result = test.call(null, val, key, obj))) {
|
|
return false;
|
|
}
|
|
});
|
|
return !!result;
|
|
},
|
|
some: function(obj, test) {
|
|
var result = false;
|
|
if (!obj) {
|
|
return result;
|
|
}
|
|
$.each(obj, function(key, val) {
|
|
if (result = test.call(null, val, key, obj)) {
|
|
return false;
|
|
}
|
|
});
|
|
return !!result;
|
|
},
|
|
mixin: $.extend,
|
|
getUniqueId: function() {
|
|
var counter = 0;
|
|
return function() {
|
|
return counter++;
|
|
};
|
|
}(),
|
|
templatify: function templatify(obj) {
|
|
return $.isFunction(obj) ? obj : template;
|
|
function template() {
|
|
return String(obj);
|
|
}
|
|
},
|
|
defer: function(fn) {
|
|
setTimeout(fn, 0);
|
|
},
|
|
debounce: function(func, wait, immediate) {
|
|
var timeout, result;
|
|
return function() {
|
|
var context = this, args = arguments, later, callNow;
|
|
later = function() {
|
|
timeout = null;
|
|
if (!immediate) {
|
|
result = func.apply(context, args);
|
|
}
|
|
};
|
|
callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
if (callNow) {
|
|
result = func.apply(context, args);
|
|
}
|
|
return result;
|
|
};
|
|
},
|
|
throttle: function(func, wait) {
|
|
var context, args, timeout, result, previous, later;
|
|
previous = 0;
|
|
later = function() {
|
|
previous = new Date();
|
|
timeout = null;
|
|
result = func.apply(context, args);
|
|
};
|
|
return function() {
|
|
var now = new Date(), remaining = wait - (now - previous);
|
|
context = this;
|
|
args = arguments;
|
|
if (remaining <= 0) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
previous = now;
|
|
result = func.apply(context, args);
|
|
} else if (!timeout) {
|
|
timeout = setTimeout(later, remaining);
|
|
}
|
|
return result;
|
|
};
|
|
},
|
|
noop: function() {}
|
|
};
|
|
var VERSION = "0.10.2";
|
|
var tokenizers = function(root) {
|
|
return {
|
|
nonword: nonword,
|
|
whitespace: whitespace,
|
|
obj: {
|
|
nonword: getObjTokenizer(nonword),
|
|
whitespace: getObjTokenizer(whitespace)
|
|
}
|
|
};
|
|
function whitespace(s) {
|
|
return s.split(/\s+/);
|
|
}
|
|
function nonword(s) {
|
|
return s.split(/\W+/);
|
|
}
|
|
function getObjTokenizer(tokenizer) {
|
|
return function setKey(key) {
|
|
return function tokenize(o) {
|
|
return tokenizer(o[key]);
|
|
};
|
|
};
|
|
}
|
|
}();
|
|
var LruCache = function() {
|
|
function LruCache(maxSize) {
|
|
this.maxSize = maxSize || 100;
|
|
this.size = 0;
|
|
this.hash = {};
|
|
this.list = new List();
|
|
}
|
|
_.mixin(LruCache.prototype, {
|
|
set: function set(key, val) {
|
|
var tailItem = this.list.tail, node;
|
|
if (this.size >= this.maxSize) {
|
|
this.list.remove(tailItem);
|
|
delete this.hash[tailItem.key];
|
|
}
|
|
if (node = this.hash[key]) {
|
|
node.val = val;
|
|
this.list.moveToFront(node);
|
|
} else {
|
|
node = new Node(key, val);
|
|
this.list.add(node);
|
|
this.hash[key] = node;
|
|
this.size++;
|
|
}
|
|
},
|
|
get: function get(key) {
|
|
var node = this.hash[key];
|
|
if (node) {
|
|
this.list.moveToFront(node);
|
|
return node.val;
|
|
}
|
|
}
|
|
});
|
|
function List() {
|
|
this.head = this.tail = null;
|
|
}
|
|
_.mixin(List.prototype, {
|
|
add: function add(node) {
|
|
if (this.head) {
|
|
node.next = this.head;
|
|
this.head.prev = node;
|
|
}
|
|
this.head = node;
|
|
this.tail = this.tail || node;
|
|
},
|
|
remove: function remove(node) {
|
|
node.prev ? node.prev.next = node.next : this.head = node.next;
|
|
node.next ? node.next.prev = node.prev : this.tail = node.prev;
|
|
},
|
|
moveToFront: function(node) {
|
|
this.remove(node);
|
|
this.add(node);
|
|
}
|
|
});
|
|
function Node(key, val) {
|
|
this.key = key;
|
|
this.val = val;
|
|
this.prev = this.next = null;
|
|
}
|
|
return LruCache;
|
|
}();
|
|
var PersistentStorage = function() {
|
|
var ls, methods;
|
|
try {
|
|
ls = window.localStorage;
|
|
ls.setItem("~~~", "!");
|
|
ls.removeItem("~~~");
|
|
} catch (err) {
|
|
ls = null;
|
|
}
|
|
function PersistentStorage(namespace) {
|
|
this.prefix = [ "__", namespace, "__" ].join("");
|
|
this.ttlKey = "__ttl__";
|
|
this.keyMatcher = new RegExp("^" + this.prefix);
|
|
}
|
|
if (ls && window.JSON) {
|
|
methods = {
|
|
_prefix: function(key) {
|
|
return this.prefix + key;
|
|
},
|
|
_ttlKey: function(key) {
|
|
return this._prefix(key) + this.ttlKey;
|
|
},
|
|
get: function(key) {
|
|
if (this.isExpired(key)) {
|
|
this.remove(key);
|
|
}
|
|
return decode(ls.getItem(this._prefix(key)));
|
|
},
|
|
set: function(key, val, ttl) {
|
|
if (_.isNumber(ttl)) {
|
|
ls.setItem(this._ttlKey(key), encode(now() + ttl));
|
|
} else {
|
|
ls.removeItem(this._ttlKey(key));
|
|
}
|
|
return ls.setItem(this._prefix(key), encode(val));
|
|
},
|
|
remove: function(key) {
|
|
ls.removeItem(this._ttlKey(key));
|
|
ls.removeItem(this._prefix(key));
|
|
return this;
|
|
},
|
|
clear: function() {
|
|
var i, key, keys = [], len = ls.length;
|
|
for (i = 0; i < len; i++) {
|
|
if ((key = ls.key(i)).match(this.keyMatcher)) {
|
|
keys.push(key.replace(this.keyMatcher, ""));
|
|
}
|
|
}
|
|
for (i = keys.length; i--; ) {
|
|
this.remove(keys[i]);
|
|
}
|
|
return this;
|
|
},
|
|
isExpired: function(key) {
|
|
var ttl = decode(ls.getItem(this._ttlKey(key)));
|
|
return _.isNumber(ttl) && now() > ttl ? true : false;
|
|
}
|
|
};
|
|
} else {
|
|
methods = {
|
|
get: _.noop,
|
|
set: _.noop,
|
|
remove: _.noop,
|
|
clear: _.noop,
|
|
isExpired: _.noop
|
|
};
|
|
}
|
|
_.mixin(PersistentStorage.prototype, methods);
|
|
return PersistentStorage;
|
|
function now() {
|
|
return new Date().getTime();
|
|
}
|
|
function encode(val) {
|
|
return JSON.stringify(_.isUndefined(val) ? null : val);
|
|
}
|
|
function decode(val) {
|
|
return JSON.parse(val);
|
|
}
|
|
}();
|
|
var Transport = function() {
|
|
var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, requestCache = new LruCache(10);
|
|
function Transport(o) {
|
|
o = o || {};
|
|
this._send = o.transport ? callbackToDeferred(o.transport) : $.ajax;
|
|
this._get = o.rateLimiter ? o.rateLimiter(this._get) : this._get;
|
|
}
|
|
Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
|
|
maxPendingRequests = num;
|
|
};
|
|
Transport.resetCache = function clearCache() {
|
|
requestCache = new LruCache(10);
|
|
};
|
|
_.mixin(Transport.prototype, {
|
|
_get: function(url, o, cb) {
|
|
var that = this, jqXhr;
|
|
if (jqXhr = pendingRequests[url]) {
|
|
jqXhr.done(done).fail(fail);
|
|
} else if (pendingRequestsCount < maxPendingRequests) {
|
|
pendingRequestsCount++;
|
|
pendingRequests[url] = this._send(url, o).done(done).fail(fail).always(always);
|
|
} else {
|
|
this.onDeckRequestArgs = [].slice.call(arguments, 0);
|
|
}
|
|
function done(resp) {
|
|
cb && cb(null, resp);
|
|
requestCache.set(url, resp);
|
|
}
|
|
function fail() {
|
|
cb && cb(true);
|
|
}
|
|
function always() {
|
|
pendingRequestsCount--;
|
|
delete pendingRequests[url];
|
|
if (that.onDeckRequestArgs) {
|
|
that._get.apply(that, that.onDeckRequestArgs);
|
|
that.onDeckRequestArgs = null;
|
|
}
|
|
}
|
|
},
|
|
get: function(url, o, cb) {
|
|
var resp;
|
|
if (_.isFunction(o)) {
|
|
cb = o;
|
|
o = {};
|
|
}
|
|
if (resp = requestCache.get(url)) {
|
|
_.defer(function() {
|
|
cb && cb(null, resp);
|
|
});
|
|
} else {
|
|
this._get(url, o, cb);
|
|
}
|
|
return !!resp;
|
|
}
|
|
});
|
|
return Transport;
|
|
function callbackToDeferred(fn) {
|
|
return function customSendWrapper(url, o) {
|
|
var deferred = $.Deferred();
|
|
fn(url, o, onSuccess, onError);
|
|
return deferred;
|
|
function onSuccess(resp) {
|
|
_.defer(function() {
|
|
deferred.resolve(resp);
|
|
});
|
|
}
|
|
function onError(err) {
|
|
_.defer(function() {
|
|
deferred.reject(err);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
}();
|
|
var SearchIndex = function() {
|
|
function SearchIndex(o) {
|
|
o = o || {};
|
|
if (!o.datumTokenizer || !o.queryTokenizer) {
|
|
$.error("datumTokenizer and queryTokenizer are both required");
|
|
}
|
|
this.datumTokenizer = o.datumTokenizer;
|
|
this.queryTokenizer = o.queryTokenizer;
|
|
this.reset();
|
|
}
|
|
_.mixin(SearchIndex.prototype, {
|
|
bootstrap: function bootstrap(o) {
|
|
this.datums = o.datums;
|
|
this.trie = o.trie;
|
|
},
|
|
add: function(data) {
|
|
var that = this;
|
|
data = _.isArray(data) ? data : [ data ];
|
|
_.each(data, function(datum) {
|
|
var id, tokens;
|
|
id = that.datums.push(datum) - 1;
|
|
tokens = normalizeTokens(that.datumTokenizer(datum));
|
|
_.each(tokens, function(token) {
|
|
var node, chars, ch;
|
|
node = that.trie;
|
|
chars = token.split("");
|
|
while (ch = chars.shift()) {
|
|
node = node.children[ch] || (node.children[ch] = newNode());
|
|
node.ids.push(id);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
get: function get(query) {
|
|
var that = this, tokens, matches;
|
|
tokens = normalizeTokens(this.queryTokenizer(query));
|
|
_.each(tokens, function(token) {
|
|
var node, chars, ch, ids;
|
|
if (matches && matches.length === 0) {
|
|
return false;
|
|
}
|
|
node = that.trie;
|
|
chars = token.split("");
|
|
while (node && (ch = chars.shift())) {
|
|
node = node.children[ch];
|
|
}
|
|
if (node && chars.length === 0) {
|
|
ids = node.ids.slice(0);
|
|
matches = matches ? getIntersection(matches, ids) : ids;
|
|
} else {
|
|
matches = [];
|
|
return false;
|
|
}
|
|
});
|
|
return matches ? _.map(unique(matches), function(id) {
|
|
return that.datums[id];
|
|
}) : [];
|
|
},
|
|
reset: function reset() {
|
|
this.datums = [];
|
|
this.trie = newNode();
|
|
},
|
|
serialize: function serialize() {
|
|
return {
|
|
datums: this.datums,
|
|
trie: this.trie
|
|
};
|
|
}
|
|
});
|
|
return SearchIndex;
|
|
function normalizeTokens(tokens) {
|
|
tokens = _.filter(tokens, function(token) {
|
|
return !!token;
|
|
});
|
|
tokens = _.map(tokens, function(token) {
|
|
return token.toLowerCase();
|
|
});
|
|
return tokens;
|
|
}
|
|
function newNode() {
|
|
return {
|
|
ids: [],
|
|
children: {}
|
|
};
|
|
}
|
|
function unique(array) {
|
|
var seen = {}, uniques = [];
|
|
for (var i = 0; i < array.length; i++) {
|
|
if (!seen[array[i]]) {
|
|
seen[array[i]] = true;
|
|
uniques.push(array[i]);
|
|
}
|
|
}
|
|
return uniques;
|
|
}
|
|
function getIntersection(arrayA, arrayB) {
|
|
var ai = 0, bi = 0, intersection = [];
|
|
arrayA = arrayA.sort(compare);
|
|
arrayB = arrayB.sort(compare);
|
|
while (ai < arrayA.length && bi < arrayB.length) {
|
|
if (arrayA[ai] < arrayB[bi]) {
|
|
ai++;
|
|
} else if (arrayA[ai] > arrayB[bi]) {
|
|
bi++;
|
|
} else {
|
|
intersection.push(arrayA[ai]);
|
|
ai++;
|
|
bi++;
|
|
}
|
|
}
|
|
return intersection;
|
|
function compare(a, b) {
|
|
return a - b;
|
|
}
|
|
}
|
|
}();
|
|
var oParser = function() {
|
|
return {
|
|
local: getLocal,
|
|
prefetch: getPrefetch,
|
|
remote: getRemote
|
|
};
|
|
function getLocal(o) {
|
|
return o.local || null;
|
|
}
|
|
function getPrefetch(o) {
|
|
var prefetch, defaults;
|
|
defaults = {
|
|
url: null,
|
|
thumbprint: "",
|
|
ttl: 24 * 60 * 60 * 1e3,
|
|
filter: null,
|
|
ajax: {}
|
|
};
|
|
if (prefetch = o.prefetch || null) {
|
|
prefetch = _.isString(prefetch) ? {
|
|
url: prefetch
|
|
} : prefetch;
|
|
prefetch = _.mixin(defaults, prefetch);
|
|
prefetch.thumbprint = VERSION + prefetch.thumbprint;
|
|
prefetch.ajax.type = prefetch.ajax.type || "GET";
|
|
prefetch.ajax.dataType = prefetch.ajax.dataType || "json";
|
|
!prefetch.url && $.error("prefetch requires url to be set");
|
|
}
|
|
return prefetch;
|
|
}
|
|
function getRemote(o) {
|
|
var remote, defaults;
|
|
defaults = {
|
|
url: null,
|
|
wildcard: "%QUERY",
|
|
replace: null,
|
|
rateLimitBy: "debounce",
|
|
rateLimitWait: 300,
|
|
send: null,
|
|
filter: null,
|
|
ajax: {}
|
|
};
|
|
if (remote = o.remote || null) {
|
|
remote = _.isString(remote) ? {
|
|
url: remote
|
|
} : remote;
|
|
remote = _.mixin(defaults, remote);
|
|
remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait);
|
|
remote.ajax.type = remote.ajax.type || "GET";
|
|
remote.ajax.dataType = remote.ajax.dataType || "json";
|
|
delete remote.rateLimitBy;
|
|
delete remote.rateLimitWait;
|
|
!remote.url && $.error("remote requires url to be set");
|
|
}
|
|
return remote;
|
|
function byDebounce(wait) {
|
|
return function(fn) {
|
|
return _.debounce(fn, wait);
|
|
};
|
|
}
|
|
function byThrottle(wait) {
|
|
return function(fn) {
|
|
return _.throttle(fn, wait);
|
|
};
|
|
}
|
|
}
|
|
}();
|
|
(function(root) {
|
|
var old, keys;
|
|
old = root.Bloodhound;
|
|
keys = {
|
|
data: "data",
|
|
protocol: "protocol",
|
|
thumbprint: "thumbprint"
|
|
};
|
|
root.Bloodhound = Bloodhound;
|
|
function Bloodhound(o) {
|
|
if (!o || !o.local && !o.prefetch && !o.remote) {
|
|
$.error("one of local, prefetch, or remote is required");
|
|
}
|
|
this.limit = o.limit || 5;
|
|
this.sorter = getSorter(o.sorter);
|
|
this.dupDetector = o.dupDetector || ignoreDuplicates;
|
|
this.local = oParser.local(o);
|
|
this.prefetch = oParser.prefetch(o);
|
|
this.remote = oParser.remote(o);
|
|
this.cacheKey = this.prefetch ? this.prefetch.cacheKey || this.prefetch.url : null;
|
|
this.index = new SearchIndex({
|
|
datumTokenizer: o.datumTokenizer,
|
|
queryTokenizer: o.queryTokenizer
|
|
});
|
|
this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null;
|
|
}
|
|
Bloodhound.noConflict = function noConflict() {
|
|
root.Bloodhound = old;
|
|
return Bloodhound;
|
|
};
|
|
Bloodhound.tokenizers = tokenizers;
|
|
_.mixin(Bloodhound.prototype, {
|
|
_loadPrefetch: function loadPrefetch(o) {
|
|
var that = this, serialized, deferred;
|
|
if (serialized = this._readFromStorage(o.thumbprint)) {
|
|
this.index.bootstrap(serialized);
|
|
deferred = $.Deferred().resolve();
|
|
} else {
|
|
deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse);
|
|
}
|
|
return deferred;
|
|
function handlePrefetchResponse(resp) {
|
|
that.clear();
|
|
that.add(o.filter ? o.filter(resp) : resp);
|
|
that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl);
|
|
}
|
|
},
|
|
_getFromRemote: function getFromRemote(query, cb) {
|
|
var that = this, url, uriEncodedQuery;
|
|
query = query || "";
|
|
uriEncodedQuery = encodeURIComponent(query);
|
|
url = this.remote.replace ? this.remote.replace(this.remote.url, query) : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery);
|
|
return this.transport.get(url, this.remote.ajax, handleRemoteResponse);
|
|
function handleRemoteResponse(err, resp) {
|
|
err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp);
|
|
}
|
|
},
|
|
_saveToStorage: function saveToStorage(data, thumbprint, ttl) {
|
|
if (this.storage) {
|
|
this.storage.set(keys.data, data, ttl);
|
|
this.storage.set(keys.protocol, location.protocol, ttl);
|
|
this.storage.set(keys.thumbprint, thumbprint, ttl);
|
|
}
|
|
},
|
|
_readFromStorage: function readFromStorage(thumbprint) {
|
|
var stored = {}, isExpired;
|
|
if (this.storage) {
|
|
stored.data = this.storage.get(keys.data);
|
|
stored.protocol = this.storage.get(keys.protocol);
|
|
stored.thumbprint = this.storage.get(keys.thumbprint);
|
|
}
|
|
isExpired = stored.thumbprint !== thumbprint || stored.protocol !== location.protocol;
|
|
return stored.data && !isExpired ? stored.data : null;
|
|
},
|
|
_initialize: function initialize() {
|
|
var that = this, local = this.local, deferred;
|
|
deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve();
|
|
local && deferred.done(addLocalToIndex);
|
|
this.transport = this.remote ? new Transport(this.remote) : null;
|
|
return this.initPromise = deferred.promise();
|
|
function addLocalToIndex() {
|
|
that.add(_.isFunction(local) ? local() : local);
|
|
}
|
|
},
|
|
initialize: function initialize(force) {
|
|
return !this.initPromise || force ? this._initialize() : this.initPromise;
|
|
},
|
|
add: function add(data) {
|
|
this.index.add(data);
|
|
},
|
|
get: function get(query, cb) {
|
|
var that = this, matches = [], cacheHit = false;
|
|
matches = this.index.get(query);
|
|
matches = this.sorter(matches).slice(0, this.limit);
|
|
if (matches.length < this.limit && this.transport) {
|
|
cacheHit = this._getFromRemote(query, returnRemoteMatches);
|
|
}
|
|
if (!cacheHit) {
|
|
(matches.length > 0 || !this.transport) && cb && cb(matches);
|
|
}
|
|
function returnRemoteMatches(remoteMatches) {
|
|
var matchesWithBackfill = matches.slice(0);
|
|
_.each(remoteMatches, function(remoteMatch) {
|
|
var isDuplicate;
|
|
isDuplicate = _.some(matchesWithBackfill, function(match) {
|
|
return that.dupDetector(remoteMatch, match);
|
|
});
|
|
!isDuplicate && matchesWithBackfill.push(remoteMatch);
|
|
return matchesWithBackfill.length < that.limit;
|
|
});
|
|
cb && cb(that.sorter(matchesWithBackfill));
|
|
}
|
|
},
|
|
clear: function clear() {
|
|
this.index.reset();
|
|
},
|
|
clearPrefetchCache: function clearPrefetchCache() {
|
|
this.storage && this.storage.clear();
|
|
},
|
|
clearRemoteCache: function clearRemoteCache() {
|
|
this.transport && Transport.resetCache();
|
|
},
|
|
ttAdapter: function ttAdapter() {
|
|
return _.bind(this.get, this);
|
|
}
|
|
});
|
|
return Bloodhound;
|
|
function getSorter(sortFn) {
|
|
return _.isFunction(sortFn) ? sort : noSort;
|
|
function sort(array) {
|
|
return array.sort(sortFn);
|
|
}
|
|
function noSort(array) {
|
|
return array;
|
|
}
|
|
}
|
|
function ignoreDuplicates() {
|
|
return false;
|
|
}
|
|
})(this);
|
|
var html = {
|
|
wrapper: '<span class="twitter-typeahead"></span>',
|
|
dropdown: '<span class="tt-dropdown-menu"></span>',
|
|
dataset: '<div class="tt-dataset-%CLASS%"></div>',
|
|
suggestions: '<span class="tt-suggestions"></span>',
|
|
suggestion: '<div class="tt-suggestion"></div>'
|
|
};
|
|
var css = {
|
|
wrapper: {
|
|
position: "relative",
|
|
display: "inline-block"
|
|
},
|
|
hint: {
|
|
position: "absolute",
|
|
top: "0",
|
|
left: "0",
|
|
borderColor: "transparent",
|
|
boxShadow: "none"
|
|
},
|
|
input: {
|
|
position: "relative",
|
|
verticalAlign: "top",
|
|
backgroundColor: "transparent"
|
|
},
|
|
inputWithNoHint: {
|
|
position: "relative",
|
|
verticalAlign: "top"
|
|
},
|
|
dropdown: {
|
|
position: "absolute",
|
|
top: "100%",
|
|
left: "0",
|
|
zIndex: "100",
|
|
display: "none"
|
|
},
|
|
suggestions: {
|
|
display: "block"
|
|
},
|
|
suggestion: {
|
|
whiteSpace: "nowrap",
|
|
cursor: "pointer"
|
|
},
|
|
suggestionChild: {
|
|
whiteSpace: "normal"
|
|
},
|
|
ltr: {
|
|
left: "0",
|
|
right: "auto"
|
|
},
|
|
rtl: {
|
|
left: "auto",
|
|
right: " 0"
|
|
}
|
|
};
|
|
if (_.isMsie()) {
|
|
_.mixin(css.input, {
|
|
backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
|
|
});
|
|
}
|
|
if (_.isMsie() && _.isMsie() <= 7) {
|
|
_.mixin(css.input, {
|
|
marginTop: "-1px"
|
|
});
|
|
}
|
|
var EventBus = function() {
|
|
var namespace = "typeahead:";
|
|
function EventBus(o) {
|
|
if (!o || !o.el) {
|
|
$.error("EventBus initialized without el");
|
|
}
|
|
this.$el = $(o.el);
|
|
}
|
|
_.mixin(EventBus.prototype, {
|
|
trigger: function(type) {
|
|
var args = [].slice.call(arguments, 1);
|
|
this.$el.trigger(namespace + type, args);
|
|
}
|
|
});
|
|
return EventBus;
|
|
}();
|
|
var EventEmitter = function() {
|
|
var splitter = /\s+/, nextTick = getNextTick();
|
|
return {
|
|
onSync: onSync,
|
|
onAsync: onAsync,
|
|
off: off,
|
|
trigger: trigger
|
|
};
|
|
function on(method, types, cb, context) {
|
|
var type;
|
|
if (!cb) {
|
|
return this;
|
|
}
|
|
types = types.split(splitter);
|
|
cb = context ? bindContext(cb, context) : cb;
|
|
this._callbacks = this._callbacks || {};
|
|
while (type = types.shift()) {
|
|
this._callbacks[type] = this._callbacks[type] || {
|
|
sync: [],
|
|
async: []
|
|
};
|
|
this._callbacks[type][method].push(cb);
|
|
}
|
|
return this;
|
|
}
|
|
function onAsync(types, cb, context) {
|
|
return on.call(this, "async", types, cb, context);
|
|
}
|
|
function onSync(types, cb, context) {
|
|
return on.call(this, "sync", types, cb, context);
|
|
}
|
|
function off(types) {
|
|
var type;
|
|
if (!this._callbacks) {
|
|
return this;
|
|
}
|
|
types = types.split(splitter);
|
|
while (type = types.shift()) {
|
|
delete this._callbacks[type];
|
|
}
|
|
return this;
|
|
}
|
|
function trigger(types) {
|
|
var type, callbacks, args, syncFlush, asyncFlush;
|
|
if (!this._callbacks) {
|
|
return this;
|
|
}
|
|
types = types.split(splitter);
|
|
args = [].slice.call(arguments, 1);
|
|
while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
|
|
syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args));
|
|
asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args));
|
|
syncFlush() && nextTick(asyncFlush);
|
|
}
|
|
return this;
|
|
}
|
|
function getFlush(callbacks, context, args) {
|
|
return flush;
|
|
function flush() {
|
|
var cancelled;
|
|
for (var i = 0; !cancelled && i < callbacks.length; i += 1) {
|
|
cancelled = callbacks[i].apply(context, args) === false;
|
|
}
|
|
return !cancelled;
|
|
}
|
|
}
|
|
function getNextTick() {
|
|
var nextTickFn;
|
|
if (window.setImmediate) {
|
|
nextTickFn = function nextTickSetImmediate(fn) {
|
|
setImmediate(function() {
|
|
fn();
|
|
});
|
|
};
|
|
} else {
|
|
nextTickFn = function nextTickSetTimeout(fn) {
|
|
setTimeout(function() {
|
|
fn();
|
|
}, 0);
|
|
};
|
|
}
|
|
return nextTickFn;
|
|
}
|
|
function bindContext(fn, context) {
|
|
return fn.bind ? fn.bind(context) : function() {
|
|
fn.apply(context, [].slice.call(arguments, 0));
|
|
};
|
|
}
|
|
}();
|
|
var highlight = function(doc) {
|
|
var defaults = {
|
|
node: null,
|
|
pattern: null,
|
|
tagName: "strong",
|
|
className: null,
|
|
wordsOnly: false,
|
|
caseSensitive: false
|
|
};
|
|
return function hightlight(o) {
|
|
var regex;
|
|
o = _.mixin({}, defaults, o);
|
|
if (!o.node || !o.pattern) {
|
|
return;
|
|
}
|
|
o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
|
|
regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
|
|
traverse(o.node, hightlightTextNode);
|
|
function hightlightTextNode(textNode) {
|
|
var match, patternNode;
|
|
if (match = regex.exec(textNode.data)) {
|
|
wrapperNode = doc.createElement(o.tagName);
|
|
o.className && (wrapperNode.className = o.className);
|
|
patternNode = textNode.splitText(match.index);
|
|
patternNode.splitText(match[0].length);
|
|
wrapperNode.appendChild(patternNode.cloneNode(true));
|
|
textNode.parentNode.replaceChild(wrapperNode, patternNode);
|
|
}
|
|
return !!match;
|
|
}
|
|
function traverse(el, hightlightTextNode) {
|
|
var childNode, TEXT_NODE_TYPE = 3;
|
|
for (var i = 0; i < el.childNodes.length; i++) {
|
|
childNode = el.childNodes[i];
|
|
if (childNode.nodeType === TEXT_NODE_TYPE) {
|
|
i += hightlightTextNode(childNode) ? 1 : 0;
|
|
} else {
|
|
traverse(childNode, hightlightTextNode);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
function getRegex(patterns, caseSensitive, wordsOnly) {
|
|
var escapedPatterns = [], regexStr;
|
|
for (var i = 0; i < patterns.length; i++) {
|
|
escapedPatterns.push(_.escapeRegExChars(patterns[i]));
|
|
}
|
|
regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
|
|
return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
|
|
}
|
|
}(window.document);
|
|
var Input = function() {
|
|
var specialKeyCodeMap;
|
|
specialKeyCodeMap = {
|
|
9: "tab",
|
|
27: "esc",
|
|
37: "left",
|
|
39: "right",
|
|
13: "enter",
|
|
38: "up",
|
|
40: "down"
|
|
};
|
|
function Input(o) {
|
|
var that = this, onBlur, onFocus, onKeydown, onInput;
|
|
o = o || {};
|
|
if (!o.input) {
|
|
$.error("input is missing");
|
|
}
|
|
onBlur = _.bind(this._onBlur, this);
|
|
onFocus = _.bind(this._onFocus, this);
|
|
onKeydown = _.bind(this._onKeydown, this);
|
|
onInput = _.bind(this._onInput, this);
|
|
this.$hint = $(o.hint);
|
|
this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown);
|
|
if (this.$hint.length === 0) {
|
|
this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
|
|
}
|
|
if (!_.isMsie()) {
|
|
this.$input.on("input.tt", onInput);
|
|
} else {
|
|
this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
|
|
if (specialKeyCodeMap[$e.which || $e.keyCode]) {
|
|
return;
|
|
}
|
|
_.defer(_.bind(that._onInput, that, $e));
|
|
});
|
|
}
|
|
this.query = this.$input.val();
|
|
this.$overflowHelper = buildOverflowHelper(this.$input);
|
|
}
|
|
Input.normalizeQuery = function(str) {
|
|
return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
|
|
};
|
|
_.mixin(Input.prototype, EventEmitter, {
|
|
_onBlur: function onBlur() {
|
|
this.resetInputValue();
|
|
this.trigger("blurred");
|
|
},
|
|
_onFocus: function onFocus() {
|
|
this.trigger("focused");
|
|
},
|
|
_onKeydown: function onKeydown($e) {
|
|
var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
|
|
this._managePreventDefault(keyName, $e);
|
|
if (keyName && this._shouldTrigger(keyName, $e)) {
|
|
this.trigger(keyName + "Keyed", $e);
|
|
}
|
|
},
|
|
_onInput: function onInput() {
|
|
this._checkInputValue();
|
|
},
|
|
_managePreventDefault: function managePreventDefault(keyName, $e) {
|
|
var preventDefault, hintValue, inputValue;
|
|
switch (keyName) {
|
|
case "tab":
|
|
hintValue = this.getHint();
|
|
inputValue = this.getInputValue();
|
|
preventDefault = hintValue && hintValue !== inputValue && !withModifier($e);
|
|
break;
|
|
|
|
case "up":
|
|
case "down":
|
|
preventDefault = !withModifier($e);
|
|
break;
|
|
|
|
default:
|
|
preventDefault = false;
|
|
}
|
|
preventDefault && $e.preventDefault();
|
|
},
|
|
_shouldTrigger: function shouldTrigger(keyName, $e) {
|
|
var trigger;
|
|
switch (keyName) {
|
|
case "tab":
|
|
trigger = !withModifier($e);
|
|
break;
|
|
|
|
default:
|
|
trigger = true;
|
|
}
|
|
return trigger;
|
|
},
|
|
_checkInputValue: function checkInputValue() {
|
|
var inputValue, areEquivalent, hasDifferentWhitespace;
|
|
inputValue = this.getInputValue();
|
|
areEquivalent = areQueriesEquivalent(inputValue, this.query);
|
|
hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false;
|
|
if (!areEquivalent) {
|
|
this.trigger("queryChanged", this.query = inputValue);
|
|
} else if (hasDifferentWhitespace) {
|
|
this.trigger("whitespaceChanged", this.query);
|
|
}
|
|
},
|
|
focus: function focus() {
|
|
this.$input.focus();
|
|
},
|
|
blur: function blur() {
|
|
this.$input.blur();
|
|
},
|
|
getQuery: function getQuery() {
|
|
return this.query;
|
|
},
|
|
setQuery: function setQuery(query) {
|
|
this.query = query;
|
|
},
|
|
getInputValue: function getInputValue() {
|
|
return this.$input.val();
|
|
},
|
|
setInputValue: function setInputValue(value, silent) {
|
|
this.$input.val(value);
|
|
silent ? this.clearHint() : this._checkInputValue();
|
|
},
|
|
resetInputValue: function resetInputValue() {
|
|
this.setInputValue(this.query, true);
|
|
},
|
|
getHint: function getHint() {
|
|
return this.$hint.val();
|
|
},
|
|
setHint: function setHint(value) {
|
|
this.$hint.val(value);
|
|
},
|
|
clearHint: function clearHint() {
|
|
this.setHint("");
|
|
},
|
|
clearHintIfInvalid: function clearHintIfInvalid() {
|
|
var val, hint, valIsPrefixOfHint, isValid;
|
|
val = this.getInputValue();
|
|
hint = this.getHint();
|
|
valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
|
|
isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow();
|
|
!isValid && this.clearHint();
|
|
},
|
|
getLanguageDirection: function getLanguageDirection() {
|
|
return (this.$input.css("direction") || "ltr").toLowerCase();
|
|
},
|
|
hasOverflow: function hasOverflow() {
|
|
var constraint = this.$input.width() - 2;
|
|
this.$overflowHelper.text(this.getInputValue());
|
|
return this.$overflowHelper.width() >= constraint;
|
|
},
|
|
isCursorAtEnd: function() {
|
|
var valueLength, selectionStart, range;
|
|
valueLength = this.$input.val().length;
|
|
selectionStart = this.$input[0].selectionStart;
|
|
if (_.isNumber(selectionStart)) {
|
|
return selectionStart === valueLength;
|
|
} else if (document.selection) {
|
|
range = document.selection.createRange();
|
|
range.moveStart("character", -valueLength);
|
|
return valueLength === range.text.length;
|
|
}
|
|
return true;
|
|
},
|
|
destroy: function destroy() {
|
|
this.$hint.off(".tt");
|
|
this.$input.off(".tt");
|
|
this.$hint = this.$input = this.$overflowHelper = null;
|
|
}
|
|
});
|
|
return Input;
|
|
function buildOverflowHelper($input) {
|
|
return $('<pre aria-hidden="true"></pre>').css({
|
|
position: "absolute",
|
|
visibility: "hidden",
|
|
whiteSpace: "pre",
|
|
fontFamily: $input.css("font-family"),
|
|
fontSize: $input.css("font-size"),
|
|
fontStyle: $input.css("font-style"),
|
|
fontVariant: $input.css("font-variant"),
|
|
fontWeight: $input.css("font-weight"),
|
|
wordSpacing: $input.css("word-spacing"),
|
|
letterSpacing: $input.css("letter-spacing"),
|
|
textIndent: $input.css("text-indent"),
|
|
textRendering: $input.css("text-rendering"),
|
|
textTransform: $input.css("text-transform")
|
|
}).insertAfter($input);
|
|
}
|
|
function areQueriesEquivalent(a, b) {
|
|
return Input.normalizeQuery(a) === Input.normalizeQuery(b);
|
|
}
|
|
function withModifier($e) {
|
|
return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
|
|
}
|
|
}();
|
|
var Dataset = function() {
|
|
var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum";
|
|
function Dataset(o) {
|
|
o = o || {};
|
|
o.templates = o.templates || {};
|
|
if (!o.source) {
|
|
$.error("missing source");
|
|
}
|
|
if (o.name && !isValidName(o.name)) {
|
|
$.error("invalid dataset name: " + o.name);
|
|
}
|
|
this.query = null;
|
|
this.highlight = !!o.highlight;
|
|
this.name = o.name || _.getUniqueId();
|
|
this.source = o.source;
|
|
this.displayFn = getDisplayFn(o.display || o.displayKey);
|
|
this.templates = getTemplates(o.templates, this.displayFn);
|
|
this.$el = $(html.dataset.replace("%CLASS%", this.name));
|
|
}
|
|
Dataset.extractDatasetName = function extractDatasetName(el) {
|
|
return $(el).data(datasetKey);
|
|
};
|
|
Dataset.extractValue = function extractDatum(el) {
|
|
return $(el).data(valueKey);
|
|
};
|
|
Dataset.extractDatum = function extractDatum(el) {
|
|
return $(el).data(datumKey);
|
|
};
|
|
_.mixin(Dataset.prototype, EventEmitter, {
|
|
_render: function render(query, suggestions) {
|
|
if (!this.$el) {
|
|
return;
|
|
}
|
|
var that = this, hasSuggestions;
|
|
this.$el.empty();
|
|
hasSuggestions = suggestions && suggestions.length;
|
|
if (!hasSuggestions && this.templates.empty) {
|
|
this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null);
|
|
} else if (hasSuggestions) {
|
|
this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null);
|
|
}
|
|
this.trigger("rendered");
|
|
function getEmptyHtml() {
|
|
return that.templates.empty({
|
|
query: query,
|
|
isEmpty: true
|
|
});
|
|
}
|
|
function getSuggestionsHtml() {
|
|
var $suggestions, nodes;
|
|
$suggestions = $(html.suggestions).css(css.suggestions);
|
|
nodes = _.map(suggestions, getSuggestionNode);
|
|
$suggestions.append.apply($suggestions, nodes);
|
|
that.highlight && highlight({
|
|
node: $suggestions[0],
|
|
pattern: query
|
|
});
|
|
return $suggestions;
|
|
function getSuggestionNode(suggestion) {
|
|
var $el;
|
|
$el = $(html.suggestion).append(that.templates.suggestion(suggestion)).data(datasetKey, that.name).data(valueKey, that.displayFn(suggestion)).data(datumKey, suggestion);
|
|
$el.children().each(function() {
|
|
$(this).css(css.suggestionChild);
|
|
});
|
|
return $el;
|
|
}
|
|
}
|
|
function getHeaderHtml() {
|
|
return that.templates.header({
|
|
query: query,
|
|
isEmpty: !hasSuggestions
|
|
});
|
|
}
|
|
function getFooterHtml() {
|
|
return that.templates.footer({
|
|
query: query,
|
|
isEmpty: !hasSuggestions
|
|
});
|
|
}
|
|
},
|
|
getRoot: function getRoot() {
|
|
return this.$el;
|
|
},
|
|
update: function update(query) {
|
|
var that = this;
|
|
this.query = query;
|
|
this.canceled = false;
|
|
this.source(query, render);
|
|
function render(suggestions) {
|
|
if (!that.canceled && query === that.query) {
|
|
that._render(query, suggestions);
|
|
}
|
|
}
|
|
},
|
|
cancel: function cancel() {
|
|
this.canceled = true;
|
|
},
|
|
clear: function clear() {
|
|
this.cancel();
|
|
this.$el.empty();
|
|
this.trigger("rendered");
|
|
},
|
|
isEmpty: function isEmpty() {
|
|
return this.$el.is(":empty");
|
|
},
|
|
destroy: function destroy() {
|
|
this.$el = null;
|
|
}
|
|
});
|
|
return Dataset;
|
|
function getDisplayFn(display) {
|
|
display = display || "value";
|
|
return _.isFunction(display) ? display : displayFn;
|
|
function displayFn(obj) {
|
|
return obj[display];
|
|
}
|
|
}
|
|
function getTemplates(templates, displayFn) {
|
|
return {
|
|
empty: templates.empty && _.templatify(templates.empty),
|
|
header: templates.header && _.templatify(templates.header),
|
|
footer: templates.footer && _.templatify(templates.footer),
|
|
suggestion: templates.suggestion || suggestionTemplate
|
|
};
|
|
function suggestionTemplate(context) {
|
|
return "<p>" + displayFn(context) + "</p>";
|
|
}
|
|
}
|
|
function isValidName(str) {
|
|
return /^[_a-zA-Z0-9-]+$/.test(str);
|
|
}
|
|
}();
|
|
var Dropdown = function() {
|
|
function Dropdown(o) {
|
|
var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave;
|
|
o = o || {};
|
|
if (!o.menu) {
|
|
$.error("menu is required");
|
|
}
|
|
this.isOpen = false;
|
|
this.isEmpty = true;
|
|
this.datasets = _.map(o.datasets, initializeDataset);
|
|
onSuggestionClick = _.bind(this._onSuggestionClick, this);
|
|
onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this);
|
|
onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this);
|
|
this.$menu = $(o.menu).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave);
|
|
_.each(this.datasets, function(dataset) {
|
|
that.$menu.append(dataset.getRoot());
|
|
dataset.onSync("rendered", that._onRendered, that);
|
|
});
|
|
}
|
|
_.mixin(Dropdown.prototype, EventEmitter, {
|
|
_onSuggestionClick: function onSuggestionClick($e) {
|
|
this.trigger("suggestionClicked", $($e.currentTarget));
|
|
},
|
|
_onSuggestionMouseEnter: function onSuggestionMouseEnter($e) {
|
|
this._removeCursor();
|
|
this._setCursor($($e.currentTarget), true);
|
|
},
|
|
_onSuggestionMouseLeave: function onSuggestionMouseLeave() {
|
|
this._removeCursor();
|
|
},
|
|
_onRendered: function onRendered() {
|
|
this.isEmpty = _.every(this.datasets, isDatasetEmpty);
|
|
this.isEmpty ? this._hide() : this.isOpen && this._show();
|
|
this.trigger("datasetRendered");
|
|
function isDatasetEmpty(dataset) {
|
|
return dataset.isEmpty();
|
|
}
|
|
},
|
|
_hide: function() {
|
|
this.$menu.hide();
|
|
},
|
|
_show: function() {
|
|
this.$menu.css("display", "block");
|
|
},
|
|
_getSuggestions: function getSuggestions() {
|
|
return this.$menu.find(".tt-suggestion");
|
|
},
|
|
_getCursor: function getCursor() {
|
|
return this.$menu.find(".tt-cursor").first();
|
|
},
|
|
_setCursor: function setCursor($el, silent) {
|
|
$el.first().addClass("tt-cursor");
|
|
!silent && this.trigger("cursorMoved");
|
|
},
|
|
_removeCursor: function removeCursor() {
|
|
this._getCursor().removeClass("tt-cursor");
|
|
},
|
|
_moveCursor: function moveCursor(increment) {
|
|
var $suggestions, $oldCursor, newCursorIndex, $newCursor;
|
|
if (!this.isOpen) {
|
|
return;
|
|
}
|
|
$oldCursor = this._getCursor();
|
|
$suggestions = this._getSuggestions();
|
|
this._removeCursor();
|
|
newCursorIndex = $suggestions.index($oldCursor) + increment;
|
|
newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1;
|
|
if (newCursorIndex === -1) {
|
|
this.trigger("cursorRemoved");
|
|
return;
|
|
} else if (newCursorIndex < -1) {
|
|
newCursorIndex = $suggestions.length - 1;
|
|
}
|
|
this._setCursor($newCursor = $suggestions.eq(newCursorIndex));
|
|
this._ensureVisible($newCursor);
|
|
},
|
|
_ensureVisible: function ensureVisible($el) {
|
|
var elTop, elBottom, menuScrollTop, menuHeight;
|
|
elTop = $el.position().top;
|
|
elBottom = elTop + $el.outerHeight(true);
|
|
menuScrollTop = this.$menu.scrollTop();
|
|
menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10);
|
|
if (elTop < 0) {
|
|
this.$menu.scrollTop(menuScrollTop + elTop);
|
|
} else if (menuHeight < elBottom) {
|
|
this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
|
|
}
|
|
},
|
|
close: function close() {
|
|
if (this.isOpen) {
|
|
this.isOpen = false;
|
|
this._removeCursor();
|
|
this._hide();
|
|
this.trigger("closed");
|
|
}
|
|
},
|
|
open: function open() {
|
|
if (!this.isOpen) {
|
|
this.isOpen = true;
|
|
!this.isEmpty && this._show();
|
|
this.trigger("opened");
|
|
}
|
|
},
|
|
setLanguageDirection: function setLanguageDirection(dir) {
|
|
this.$menu.css(dir === "ltr" ? css.ltr : css.rtl);
|
|
},
|
|
moveCursorUp: function moveCursorUp() {
|
|
this._moveCursor(-1);
|
|
},
|
|
moveCursorDown: function moveCursorDown() {
|
|
this._moveCursor(+1);
|
|
},
|
|
getDatumForSuggestion: function getDatumForSuggestion($el) {
|
|
var datum = null;
|
|
if ($el.length) {
|
|
datum = {
|
|
raw: Dataset.extractDatum($el),
|
|
value: Dataset.extractValue($el),
|
|
datasetName: Dataset.extractDatasetName($el)
|
|
};
|
|
}
|
|
return datum;
|
|
},
|
|
getDatumForCursor: function getDatumForCursor() {
|
|
return this.getDatumForSuggestion(this._getCursor().first());
|
|
},
|
|
getDatumForTopSuggestion: function getDatumForTopSuggestion() {
|
|
return this.getDatumForSuggestion(this._getSuggestions().first());
|
|
},
|
|
update: function update(query) {
|
|
_.each(this.datasets, updateDataset);
|
|
function updateDataset(dataset) {
|
|
dataset.update(query);
|
|
}
|
|
},
|
|
empty: function empty() {
|
|
_.each(this.datasets, clearDataset);
|
|
this.isEmpty = true;
|
|
function clearDataset(dataset) {
|
|
dataset.clear();
|
|
}
|
|
},
|
|
isVisible: function isVisible() {
|
|
return this.isOpen && !this.isEmpty;
|
|
},
|
|
destroy: function destroy() {
|
|
this.$menu.off(".tt");
|
|
this.$menu = null;
|
|
_.each(this.datasets, destroyDataset);
|
|
function destroyDataset(dataset) {
|
|
dataset.destroy();
|
|
}
|
|
}
|
|
});
|
|
return Dropdown;
|
|
function initializeDataset(oDataset) {
|
|
return new Dataset(oDataset);
|
|
}
|
|
}();
|
|
var Typeahead = function() {
|
|
var attrsKey = "ttAttrs";
|
|
function Typeahead(o) {
|
|
var $menu, $input, $hint;
|
|
o = o || {};
|
|
if (!o.input) {
|
|
$.error("missing input");
|
|
}
|
|
this.isActivated = false;
|
|
this.autoselect = !!o.autoselect;
|
|
this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
|
|
this.$node = buildDomStructure(o.input, o.withHint);
|
|
$menu = this.$node.find(".tt-dropdown-menu");
|
|
$input = this.$node.find(".tt-input");
|
|
$hint = this.$node.find(".tt-hint");
|
|
$input.on("blur.tt", function($e) {
|
|
var active, isActive, hasActive;
|
|
active = document.activeElement;
|
|
isActive = $menu.is(active);
|
|
hasActive = $menu.has(active).length > 0;
|
|
if (_.isMsie() && (isActive || hasActive)) {
|
|
$e.preventDefault();
|
|
$e.stopImmediatePropagation();
|
|
_.defer(function() {
|
|
$input.focus();
|
|
});
|
|
}
|
|
});
|
|
$menu.on("mousedown.tt", function($e) {
|
|
$e.preventDefault();
|
|
});
|
|
this.eventBus = o.eventBus || new EventBus({
|
|
el: $input
|
|
});
|
|
this.dropdown = new Dropdown({
|
|
menu: $menu,
|
|
datasets: o.datasets
|
|
}).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this);
|
|
this.input = new Input({
|
|
input: $input,
|
|
hint: $hint
|
|
}).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this);
|
|
this._setLanguageDirection();
|
|
}
|
|
_.mixin(Typeahead.prototype, {
|
|
_onSuggestionClicked: function onSuggestionClicked(type, $el) {
|
|
var datum;
|
|
if (datum = this.dropdown.getDatumForSuggestion($el)) {
|
|
this._select(datum);
|
|
}
|
|
},
|
|
_onCursorMoved: function onCursorMoved() {
|
|
var datum = this.dropdown.getDatumForCursor();
|
|
this.input.setInputValue(datum.value, true);
|
|
this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName);
|
|
},
|
|
_onCursorRemoved: function onCursorRemoved() {
|
|
this.input.resetInputValue();
|
|
this._updateHint();
|
|
},
|
|
_onDatasetRendered: function onDatasetRendered() {
|
|
this._updateHint();
|
|
},
|
|
_onOpened: function onOpened() {
|
|
this._updateHint();
|
|
this.eventBus.trigger("opened");
|
|
},
|
|
_onClosed: function onClosed() {
|
|
this.input.clearHint();
|
|
this.eventBus.trigger("closed");
|
|
},
|
|
_onFocused: function onFocused() {
|
|
this.isActivated = true;
|
|
this.dropdown.open();
|
|
},
|
|
_onBlurred: function onBlurred() {
|
|
this.isActivated = false;
|
|
this.dropdown.empty();
|
|
this.dropdown.close();
|
|
this.setVal("", true); //LM
|
|
},
|
|
_onEnterKeyed: function onEnterKeyed(type, $e) {
|
|
var cursorDatum, topSuggestionDatum;
|
|
cursorDatum = this.dropdown.getDatumForCursor();
|
|
topSuggestionDatum = this.dropdown.getDatumForTopSuggestion();
|
|
if (cursorDatum) {
|
|
this._select(cursorDatum);
|
|
$e.preventDefault();
|
|
} else if (this.autoselect && topSuggestionDatum) {
|
|
this._select(topSuggestionDatum);
|
|
$e.preventDefault();
|
|
}
|
|
},
|
|
_onTabKeyed: function onTabKeyed(type, $e) {
|
|
var datum;
|
|
if (datum = this.dropdown.getDatumForCursor()) {
|
|
this._select(datum);
|
|
$e.preventDefault();
|
|
} else {
|
|
this._autocomplete(true);
|
|
}
|
|
},
|
|
_onEscKeyed: function onEscKeyed() {
|
|
this.dropdown.close();
|
|
this.input.resetInputValue();
|
|
},
|
|
_onUpKeyed: function onUpKeyed() {
|
|
var query = this.input.getQuery();
|
|
this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorUp();
|
|
this.dropdown.open();
|
|
},
|
|
_onDownKeyed: function onDownKeyed() {
|
|
var query = this.input.getQuery();
|
|
this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorDown();
|
|
this.dropdown.open();
|
|
},
|
|
_onLeftKeyed: function onLeftKeyed() {
|
|
this.dir === "rtl" && this._autocomplete();
|
|
},
|
|
_onRightKeyed: function onRightKeyed() {
|
|
this.dir === "ltr" && this._autocomplete();
|
|
},
|
|
_onQueryChanged: function onQueryChanged(e, query) {
|
|
this.input.clearHintIfInvalid();
|
|
query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.empty();
|
|
this.dropdown.open();
|
|
this._setLanguageDirection();
|
|
},
|
|
_onWhitespaceChanged: function onWhitespaceChanged() {
|
|
this._updateHint();
|
|
this.dropdown.open();
|
|
},
|
|
_setLanguageDirection: function setLanguageDirection() {
|
|
var dir;
|
|
if (this.dir !== (dir = this.input.getLanguageDirection())) {
|
|
this.dir = dir;
|
|
this.$node.css("direction", dir);
|
|
this.dropdown.setLanguageDirection(dir);
|
|
}
|
|
},
|
|
_updateHint: function updateHint() {
|
|
var datum, val, query, escapedQuery, frontMatchRegEx, match;
|
|
datum = this.dropdown.getDatumForTopSuggestion();
|
|
if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) {
|
|
val = this.input.getInputValue();
|
|
query = Input.normalizeQuery(val);
|
|
escapedQuery = _.escapeRegExChars(query);
|
|
frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i");
|
|
match = frontMatchRegEx.exec(datum.value);
|
|
match ? this.input.setHint(val + match[1]) : this.input.clearHint();
|
|
} else {
|
|
this.input.clearHint();
|
|
}
|
|
},
|
|
_autocomplete: function autocomplete(laxCursor) {
|
|
var hint, query, isCursorAtEnd, datum;
|
|
hint = this.input.getHint();
|
|
query = this.input.getQuery();
|
|
isCursorAtEnd = laxCursor || this.input.isCursorAtEnd();
|
|
if (hint && query !== hint && isCursorAtEnd) {
|
|
datum = this.dropdown.getDatumForTopSuggestion();
|
|
datum && this.input.setInputValue(datum.value);
|
|
this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName);
|
|
}
|
|
},
|
|
_select: function select(datum) {
|
|
this.input.setQuery(datum.value);
|
|
this.input.setInputValue(datum.value, true);
|
|
this._setLanguageDirection();
|
|
this.eventBus.trigger("selected", datum.raw, datum.datasetName);
|
|
this.dropdown.close();
|
|
_.defer(_.bind(this.dropdown.empty, this.dropdown));
|
|
},
|
|
open: function open() {
|
|
this.dropdown.open();
|
|
},
|
|
close: function close() {
|
|
this.dropdown.close();
|
|
},
|
|
setVal: function setVal(val) {
|
|
if (this.isActivated) {
|
|
this.input.setInputValue(val);
|
|
} else {
|
|
this.input.setQuery(val);
|
|
this.input.setInputValue(val, true);
|
|
}
|
|
this._setLanguageDirection();
|
|
},
|
|
getVal: function getVal() {
|
|
return this.input.getQuery();
|
|
},
|
|
destroy: function destroy() {
|
|
this.input.destroy();
|
|
this.dropdown.destroy();
|
|
destroyDomStructure(this.$node);
|
|
this.$node = null;
|
|
}
|
|
});
|
|
return Typeahead;
|
|
function buildDomStructure(input, withHint) {
|
|
var $input, $wrapper, $dropdown, $hint;
|
|
$input = $(input);
|
|
$wrapper = $(html.wrapper).css(css.wrapper);
|
|
$dropdown = $(html.dropdown).css(css.dropdown);
|
|
$hint = $input.clone().css(css.hint).css(getBackgroundStyles($input));
|
|
$hint.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder").prop("disabled", true).attr({
|
|
autocomplete: "off",
|
|
spellcheck: "false"
|
|
});
|
|
$input.data(attrsKey, {
|
|
dir: $input.attr("dir"),
|
|
autocomplete: $input.attr("autocomplete"),
|
|
spellcheck: $input.attr("spellcheck"),
|
|
style: $input.attr("style")
|
|
});
|
|
$input.addClass("tt-input").attr({
|
|
autocomplete: "off",
|
|
spellcheck: false
|
|
}).css(withHint ? css.input : css.inputWithNoHint);
|
|
try {
|
|
!$input.attr("dir") && $input.attr("dir", "auto");
|
|
} catch (e) {}
|
|
return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown);
|
|
}
|
|
function getBackgroundStyles($el) {
|
|
return {
|
|
backgroundAttachment: $el.css("background-attachment"),
|
|
backgroundClip: $el.css("background-clip"),
|
|
backgroundColor: $el.css("background-color"),
|
|
backgroundImage: $el.css("background-image"),
|
|
backgroundOrigin: $el.css("background-origin"),
|
|
backgroundPosition: $el.css("background-position"),
|
|
backgroundRepeat: $el.css("background-repeat"),
|
|
backgroundSize: $el.css("background-size")
|
|
};
|
|
}
|
|
function destroyDomStructure($node) {
|
|
var $input = $node.find(".tt-input");
|
|
_.each($input.data(attrsKey), function(val, key) {
|
|
_.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
|
|
});
|
|
$input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node);
|
|
$node.remove();
|
|
}
|
|
}();
|
|
(function() {
|
|
var old, typeaheadKey, methods;
|
|
old = $.fn.typeahead;
|
|
typeaheadKey = "ttTypeahead";
|
|
methods = {
|
|
initialize: function initialize(o, datasets) {
|
|
datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
|
|
o = o || {};
|
|
return this.each(attach);
|
|
function attach() {
|
|
var $input = $(this), eventBus, typeahead;
|
|
_.each(datasets, function(d) {
|
|
d.highlight = !!o.highlight;
|
|
});
|
|
typeahead = new Typeahead({
|
|
input: $input,
|
|
eventBus: eventBus = new EventBus({
|
|
el: $input
|
|
}),
|
|
withHint: _.isUndefined(o.hint) ? true : !!o.hint,
|
|
minLength: o.minLength,
|
|
autoselect: o.autoselect,
|
|
datasets: datasets
|
|
});
|
|
$input.data(typeaheadKey, typeahead);
|
|
}
|
|
},
|
|
open: function open() {
|
|
return this.each(openTypeahead);
|
|
function openTypeahead() {
|
|
var $input = $(this), typeahead;
|
|
if (typeahead = $input.data(typeaheadKey)) {
|
|
typeahead.open();
|
|
}
|
|
}
|
|
},
|
|
close: function close() {
|
|
return this.each(closeTypeahead);
|
|
function closeTypeahead() {
|
|
var $input = $(this), typeahead;
|
|
if (typeahead = $input.data(typeaheadKey)) {
|
|
typeahead.close();
|
|
}
|
|
}
|
|
},
|
|
val: function val(newVal) {
|
|
return !arguments.length ? getVal(this.first()) : this.each(setVal);
|
|
function setVal() {
|
|
var $input = $(this), typeahead;
|
|
if (typeahead = $input.data(typeaheadKey)) {
|
|
typeahead.setVal(newVal);
|
|
}
|
|
}
|
|
function getVal($input) {
|
|
var typeahead, query;
|
|
if (typeahead = $input.data(typeaheadKey)) {
|
|
query = typeahead.getVal();
|
|
}
|
|
return query;
|
|
}
|
|
},
|
|
destroy: function destroy() {
|
|
return this.each(unattach);
|
|
function unattach() {
|
|
var $input = $(this), typeahead;
|
|
if (typeahead = $input.data(typeaheadKey)) {
|
|
typeahead.destroy();
|
|
$input.removeData(typeaheadKey);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
$.fn.typeahead = function(method) {
|
|
if (methods[method]) {
|
|
return methods[method].apply(this, [].slice.call(arguments, 1));
|
|
} else {
|
|
return methods.initialize.apply(this, arguments);
|
|
}
|
|
};
|
|
$.fn.typeahead.noConflict = function noConflict() {
|
|
$.fn.typeahead = old;
|
|
return this;
|
|
};
|
|
})();
|
|
|
|
|
|
|
|
//})(window.jQuery);
|
|
|
|
|
|
});
|
|
define('searchView',[
|
|
'App',
|
|
// Templates
|
|
'text!tpl/search.html',
|
|
'text!tpl/search_suggestion.html',
|
|
// Tools
|
|
'typeahead'
|
|
], function(App, searchTpl, suggestionTpl) {
|
|
|
|
var searchView = Backbone.View.extend({
|
|
el: '#search',
|
|
/**
|
|
* Init.
|
|
*/
|
|
init: function() {
|
|
var tpl = _.template(searchTpl);
|
|
var className = 'form-control input-lg';
|
|
var placeholder = 'Search reference';
|
|
this.searchHtml = tpl({
|
|
'placeholder': placeholder,
|
|
'className': className
|
|
});
|
|
this.items = App.classes.concat(App.allItems);
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Render input field with Typehead activated.
|
|
*/
|
|
render: function() {
|
|
// Append the view to the dom
|
|
this.$el.append(this.searchHtml);
|
|
|
|
// Render Typeahead
|
|
var $searchInput = this.$el.find('input[type=text]');
|
|
this.typeaheadRender($searchInput);
|
|
this.typeaheadEvents($searchInput);
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Apply Twitter Typeahead to the search input field.
|
|
* @param {jquery} $input
|
|
*/
|
|
typeaheadRender: function($input) {
|
|
var self = this;
|
|
$input.typeahead(null, {
|
|
'displayKey': 'name',
|
|
'minLength': 2,
|
|
//'highlight': true,
|
|
'source': self.substringMatcher(this.items),
|
|
'templates': {
|
|
'empty': '<p class="empty-message">Unable to find any item that match the current query</p>',
|
|
'suggestion': _.template(suggestionTpl)
|
|
}
|
|
});
|
|
},
|
|
/**
|
|
* Setup typeahead custom events (item selected).
|
|
*/
|
|
typeaheadEvents: function($input) {
|
|
var self = this;
|
|
$input.on('typeahead:selected', function(e, item, datasetName) {
|
|
var selectedItem = self.items[item.idx];
|
|
select(selectedItem);
|
|
});
|
|
$input.on('keydown', function(e) {
|
|
if (e.which === 13) { // enter
|
|
var txt = $input.val();
|
|
var f = _.find(self.items, function(it) { return it.name == txt; });
|
|
if (f) {
|
|
select(f);
|
|
}
|
|
} else if (e.which === 27) {
|
|
$input.blur();
|
|
}
|
|
});
|
|
|
|
function select(selectedItem) {
|
|
var hash = App.router.getHash(selectedItem);//
|
|
App.router.navigate(hash, {'trigger': true});
|
|
$('#item').focus();
|
|
}
|
|
},
|
|
/**
|
|
* substringMatcher function for Typehead (search for strings in an array).
|
|
* @param {array} array
|
|
* @returns {Function}
|
|
*/
|
|
substringMatcher: function(array) {
|
|
return function findMatches(query, callback) {
|
|
var matches = [], substrRegex, arrayLength = array.length;
|
|
|
|
// regex used to determine if a string contains the substring `query`
|
|
substrRegex = new RegExp(query, 'i');
|
|
|
|
// iterate through the pool of strings and for any string that
|
|
// contains the substring `query`, add it to the `matches` array
|
|
for (var i=0; i < arrayLength; i++) {
|
|
var item = array[i];
|
|
if (substrRegex.test(item.name)) {
|
|
// typeahead expects suggestions to be a js object
|
|
matches.push({
|
|
'itemtype': item.itemtype,
|
|
'name': item.name,
|
|
'className': item.class,
|
|
'is_constructor': !!item.is_constructor,
|
|
'final': item.final,
|
|
'idx': i
|
|
});
|
|
}
|
|
}
|
|
|
|
callback(matches);
|
|
};
|
|
}
|
|
|
|
});
|
|
|
|
return searchView;
|
|
|
|
});
|
|
|
|
|
|
define('text!tpl/list.html',[],function () { return '<% _.each(groups, function(group){ %>\n <div class="reference-group clearfix main-ref-page"> \n <h2 class="group-name" id="group-<%=group.name%>" tab-index="-1"><%=group.name%></h2>\n <div class="reference-subgroups clearfix main-ref-page"> \n <% _.each(group.subgroups, function(subgroup, ind) { %>\n <div class="reference-subgroup">\n <% if (subgroup.name !== \'0\') { %>\n <h3 id="<%=group.name%><%=ind%>" class="subgroup-name subgroup-<%=subgroup.name%>"><%=subgroup.name%></h3>\n <% } %>\n <ul aria-labelledby="<%=group.name%> <%=ind%>">\n <% _.each(subgroup.items, function(item) { %>\n <li><a href="<%=item.hash%>"><%=item.name%><% if (item.itemtype === \'method\') { %>()<%}%></a></li>\n <% }); %>\n </ul>\n </div>\n <% }); %>\n </div>\n </div>\n<% }); %>\n';});
|
|
|
|
define('listView',[
|
|
'App',
|
|
// Templates
|
|
'text!tpl/list.html'
|
|
], function (App, listTpl) {
|
|
var striptags = function(html) {
|
|
var div = document.createElement('div');
|
|
div.innerHTML = html;
|
|
return div.textContent;
|
|
};
|
|
|
|
var listView = Backbone.View.extend({
|
|
el: '#list',
|
|
events: {},
|
|
/**
|
|
* Init.
|
|
*/
|
|
init: function () {
|
|
this.listTpl = _.template(listTpl);
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Render the list.
|
|
*/
|
|
render: function (items, listCollection) {
|
|
if (items && listCollection) {
|
|
var self = this;
|
|
|
|
// Render items and group them by module
|
|
// module === group
|
|
this.groups = {};
|
|
_.each(items, function (item, i) {
|
|
|
|
if (!item.private && item.file.indexOf('addons') === -1) { //addons don't get displayed on main page
|
|
|
|
var group = item.module || '_';
|
|
var subgroup = item.submodule || '_';
|
|
if (group === subgroup) {
|
|
subgroup = '0';
|
|
}
|
|
var hash = App.router.getHash(item);
|
|
|
|
// fixes broken links for #/p5/> and #/p5/>=
|
|
item.hash = item.hash.replace('>', '>');
|
|
|
|
// Create a group list
|
|
if (!self.groups[group]) {
|
|
self.groups[group] = {
|
|
name: group.replace('_', ' '),
|
|
subgroups: {}
|
|
};
|
|
}
|
|
|
|
// Create a subgroup list
|
|
if (!self.groups[group].subgroups[subgroup]) {
|
|
self.groups[group].subgroups[subgroup] = {
|
|
name: subgroup.replace('_', ' '),
|
|
items: []
|
|
};
|
|
}
|
|
|
|
// hide the un-interesting constants
|
|
if (group === 'Constants' && !item.example)
|
|
return;
|
|
|
|
if (item.class === 'p5') {
|
|
|
|
self.groups[group].subgroups[subgroup].items.push(item);
|
|
|
|
} else {
|
|
|
|
var found = _.find(self.groups[group].subgroups[subgroup].items,
|
|
function(i){ return i.name == item.class; });
|
|
|
|
if (!found) {
|
|
|
|
// FIX TO INVISIBLE OBJECTS: DH (see also router.js)
|
|
var ind = hash.lastIndexOf('/');
|
|
hash = item.hash.substring(0, ind).replace('p5/','p5.');
|
|
self.groups[group].subgroups[subgroup].items.push({
|
|
name: item.class,
|
|
hash: hash
|
|
});
|
|
}
|
|
|
|
}
|
|
}
|
|
});
|
|
|
|
// Put the <li> items html into the list <ul>
|
|
var listHtml = self.listTpl({
|
|
'striptags': striptags,
|
|
'title': self.capitalizeFirst(listCollection),
|
|
'groups': self.groups,
|
|
'listCollection': listCollection
|
|
});
|
|
|
|
// Render the view
|
|
this.$el.html(listHtml);
|
|
}
|
|
|
|
var renderEvent = new Event('reference-rendered');
|
|
window.dispatchEvent(renderEvent);
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Show a list of items.
|
|
* @param {array} items Array of item objects.
|
|
* @returns {object} This view.
|
|
*/
|
|
show: function (listGroup) {
|
|
if (App[listGroup]) {
|
|
this.render(App[listGroup], listGroup);
|
|
}
|
|
App.pageView.hideContentViews();
|
|
|
|
this.$el.show();
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Helper method to capitalize the first letter of a string
|
|
* @param {string} str
|
|
* @returns {string} Returns the string.
|
|
*/
|
|
capitalizeFirst: function (str) {
|
|
return str.substr(0, 1).toUpperCase() + str.substr(1);
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
return listView;
|
|
|
|
});
|
|
|
|
|
|
define('text!tpl/item.html',[],function () { return '<h2><%=item.name%><% if (item.isMethod) { %>()<% } %></h2>\n\n<% if (item.example) { %>\n<div class="example">\n <h3 id="reference-example">Examples</h3>\n\n <div class="example-content" data-alt="<%= item.alt %>">\n <% _.each(item.example, function(example, i){ %>\n <%= example %>\n <% }); %>\n </div>\n</div>\n<% } %>\n\n<div class="description">\n \n <h3 id="reference-description">Description</h3>\n\n <% if (item.deprecated) { %>\n <p>\n Deprecated: <%=item.name%><% if (item.isMethod) { %>()<% } %> is deprecated and will be removed in a future version of p5. <% if (item.deprecationMessage) { %><%=item.deprecationMessage%><% } %>\n </p>\n <% } %>\n \n\n <span class=\'description-text\'><%= item.description %></span>\n\n <% if (item.extends) { %>\n <p><span id="reference-extends">Extends</span> <a href="/reference/#/<%=item.extends%>" title="<%=item.extends%> reference"><%=item.extends%></a></p>\n <% } %>\n\n <% if (item.module === \'p5.sound\') { %>\n <p>This function requires you include the p5.sound library. Add the following into the head of your index.html file:\n <pre><code class="language-javascript"><script src="path/to/p5.sound.js"></script></code></pre>\n </p>\n <% } %>\n\n <% if (item.constRefs) { %>\n <p>Used by:\n <%\n var refs = item.constRefs;\n for (var i = 0; i < refs.length; i ++) {\n var ref = refs[i];\n var name = ref;\n if (name.substr(0, 3) === \'p5.\') {\n name = name.substr(3);\n }\n if (i !== 0) {\n if (i == refs.length - 1) {\n %> and <%\n } else {\n %>, <%\n }\n }\n %><a href="./#/<%= ref.replace(\'.\', \'/\') %>"><%= name %>()</a><%\n }\n %>\n </p>\n <% } %>\n</div>\n\n<% if (isConstructor || !isClass) { %>\n\n<div>\n <h3 id="reference-syntax">Syntax</h3>\n <p>\n <% syntaxes.forEach(function(syntax) { %>\n <pre><code class="language-javascript"><%= syntax %></code></pre>\n <% }) %>\n </p>\n</div>\n\n\n<% if (item.params) { %>\n <div class="params">\n <h3 id="reference-parameters">Parameters</h3>\n <ul aria-labelledby=\'reference-parameters\'>\n <% for (var i=0; i<item.params.length; i++) { %>\n <% var p = item.params[i] %>\n <li>\n <div class=\'paramname\'><%=p.name%></div>\n <% if (p.type) { %>\n <div class=\'paramtype\'>\n <% var type = p.type.replace(/(p5\\.[A-Z][A-Za-z]*)/, \'<a href="#/$1">$1</a>\'); %>\n <span class="param-type label label-info"><%=type%></span>: <%=p.description%>\n <% if (p.optional) { %> (Optional)<% } %>\n </div>\n <% } %>\n </li>\n <% } %>\n </ul>\n </div>\n<% } %>\n\n<% if (item.return && item.return.type) { %>\n <div>\n <h3 id="reference-returns">Returns</h3>\n <p class=\'returns\'><span class="param-type label label-info"><%=item.return.type%></span>: <%= item.return.description %></p>\n </div>\n<% } %>\n\n<% } %>\n';});
|
|
|
|
|
|
define('text!tpl/class.html',[],function () { return '\n<% if (typeof constructor !== \'undefined\') { %>\n<div class="constructor">\n <%=constructor%>\n</div>\n<% } %>\n\n<% let fields = _.filter(things, function(item) { return item.itemtype === \'property\' && item.access !== \'private\' }); %>\n<% if (fields.length > 0) { %>\n <h3 id=\'reference-fields\'>Fields</h3>\n <ul aria-labelledby=\'reference-fields\'>\n <% _.each(fields, function(item) { %>\n <li>\n <div class=\'paramname\'><a href="<%=item.hash%>" <% if (item.module !== module) { %>class="addon"<% } %>><%=item.name%></a></div>\n <div class=\'paramtype\'><%= item.description %></div>\n </li>\n <% }); %>\n </ul>\n<% } %>\n\n<% let methods = _.filter(things, function(item) { return item.itemtype === \'method\' && item.access !== \'private\' }); %>\n<% if (methods.length > 0) { %>\n <h3 id=\'reference-methods\'>Methods</h3>\n <ul aria-labelledby=\'reference-methods\'>\n <% _.each(methods, function(item) { %>\n <li>\n <div class=\'paramname\'><a href="<%=item.hash%>" <% if (item.module !== module) { %>class="addon"<% } %>><%=item.name%><% if (item.itemtype === \'method\') { %>()<%}%></a></div>\n <div class=\'paramtype\'><%= item.description %></div>\n </li>\n <% }); %>\n </ul>\n<% } %>\n';});
|
|
|
|
|
|
define('text!tpl/itemEnd.html',[],function () { return '\n<br><br>\n\n<div>\n<% if (item.file && item.line) { %>\n<span id="reference-error1">Notice any errors or typos?</span> <a href="https://github.com/processing/p5.js/issues"><span id="reference-contribute2">Please let us know.</span></a> <span id="reference-error3">Please feel free to edit</span> <a href="https://github.com/processing/p5.js/blob/<%= appVersion %>/<%= item.file %>#L<%= item.line %>" target="_blank" ><%= item.file %></a> <span id="reference-error5">and issue a pull request!</span>\n<% } %>\n</div>\n\n<a style="border-bottom:none !important;" href="http://creativecommons.org/licenses/by-nc-sa/4.0/" target=_blank><img src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" style="width:88px" alt="creative commons logo"/></a>\n<br><br>\n';});
|
|
|
|
// Copyright (C) 2006 Google Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
|
|
/**
|
|
* @fileoverview
|
|
* some functions for browser-side pretty printing of code contained in html.
|
|
*
|
|
* <p>
|
|
* For a fairly comprehensive set of languages see the
|
|
* <a href="http://google-code-prettify.googlecode.com/svn/trunk/README.html#langs">README</a>
|
|
* file that came with this source. At a minimum, the lexer should work on a
|
|
* number of languages including C and friends, Java, Python, Bash, SQL, HTML,
|
|
* XML, CSS, Javascript, and Makefiles. It works passably on Ruby, PHP and Awk
|
|
* and a subset of Perl, but, because of commenting conventions, doesn't work on
|
|
* Smalltalk, Lisp-like, or CAML-like languages without an explicit lang class.
|
|
* <p>
|
|
* Usage: <ol>
|
|
* <li> include this source file in an html page via
|
|
* {@code <script src="/path/to/prettify.js"></script>}
|
|
* <li> define style rules. See the example page for examples.
|
|
* <li> mark the {@code <pre>} and {@code <code>} tags in your source with
|
|
* {@code class=prettyprint.}
|
|
* You can also use the (html deprecated) {@code <xmp>} tag, but the pretty
|
|
* printer needs to do more substantial DOM manipulations to support that, so
|
|
* some css styles may not be preserved.
|
|
* </ol>
|
|
* That's it. I wanted to keep the API as simple as possible, so there's no
|
|
* need to specify which language the code is in, but if you wish, you can add
|
|
* another class to the {@code <pre>} or {@code <code>} element to specify the
|
|
* language, as in {@code <pre class="prettyprint lang-java">}. Any class that
|
|
* starts with "lang-" followed by a file extension, specifies the file type.
|
|
* See the "lang-*.js" files in this directory for code that implements
|
|
* per-language file handlers.
|
|
* <p>
|
|
* Change log:<br>
|
|
* cbeust, 2006/08/22
|
|
* <blockquote>
|
|
* Java annotations (start with "@") are now captured as literals ("lit")
|
|
* </blockquote>
|
|
* @requires console
|
|
*/
|
|
|
|
// JSLint declarations
|
|
/*global console, document, navigator, setTimeout, window, define */
|
|
|
|
/** @define {boolean} */
|
|
var IN_GLOBAL_SCOPE = true;
|
|
|
|
/**
|
|
* Split {@code prettyPrint} into multiple timeouts so as not to interfere with
|
|
* UI events.
|
|
* If set to {@code false}, {@code prettyPrint()} is synchronous.
|
|
*/
|
|
window['PR_SHOULD_USE_CONTINUATION'] = true;
|
|
|
|
/**
|
|
* Pretty print a chunk of code.
|
|
* @param {string} sourceCodeHtml The HTML to pretty print.
|
|
* @param {string} opt_langExtension The language name to use.
|
|
* Typically, a filename extension like 'cpp' or 'java'.
|
|
* @param {number|boolean} opt_numberLines True to number lines,
|
|
* or the 1-indexed number of the first line in sourceCodeHtml.
|
|
* @return {string} code as html, but prettier
|
|
*/
|
|
var prettyPrintOne;
|
|
/**
|
|
* Find all the {@code <pre>} and {@code <code>} tags in the DOM with
|
|
* {@code class=prettyprint} and prettify them.
|
|
*
|
|
* @param {Function} opt_whenDone called when prettifying is done.
|
|
* @param {HTMLElement|HTMLDocument} opt_root an element or document
|
|
* containing all the elements to pretty print.
|
|
* Defaults to {@code document.body}.
|
|
*/
|
|
var prettyPrint;
|
|
|
|
|
|
(function () {
|
|
var win = window;
|
|
// Keyword lists for various languages.
|
|
// We use things that coerce to strings to make them compact when minified
|
|
// and to defeat aggressive optimizers that fold large string constants.
|
|
var FLOW_CONTROL_KEYWORDS = ["break,continue,do,else,for,if,return,while"];
|
|
var C_KEYWORDS = [FLOW_CONTROL_KEYWORDS,"auto,case,char,const,default," +
|
|
"double,enum,extern,float,goto,inline,int,long,register,short,signed," +
|
|
"sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];
|
|
var COMMON_KEYWORDS = [C_KEYWORDS,"catch,class,delete,false,import," +
|
|
"new,operator,private,protected,public,this,throw,true,try,typeof"];
|
|
var CPP_KEYWORDS = [COMMON_KEYWORDS,"alignof,align_union,asm,axiom,bool," +
|
|
"concept,concept_map,const_cast,constexpr,decltype,delegate," +
|
|
"dynamic_cast,explicit,export,friend,generic,late_check," +
|
|
"mutable,namespace,nullptr,property,reinterpret_cast,static_assert," +
|
|
"static_cast,template,typeid,typename,using,virtual,where"];
|
|
var JAVA_KEYWORDS = [COMMON_KEYWORDS,
|
|
"abstract,assert,boolean,byte,extends,final,finally,implements,import," +
|
|
"instanceof,interface,null,native,package,strictfp,super,synchronized," +
|
|
"throws,transient"];
|
|
var CSHARP_KEYWORDS = [JAVA_KEYWORDS,
|
|
"as,base,by,checked,decimal,delegate,descending,dynamic,event," +
|
|
"fixed,foreach,from,group,implicit,in,internal,into,is,let," +
|
|
"lock,object,out,override,orderby,params,partial,readonly,ref,sbyte," +
|
|
"sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort," +
|
|
"var,virtual,where"];
|
|
var COFFEE_KEYWORDS = "all,and,by,catch,class,else,extends,false,finally," +
|
|
"for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then," +
|
|
"throw,true,try,unless,until,when,while,yes";
|
|
var JSCRIPT_KEYWORDS = [COMMON_KEYWORDS,
|
|
"debugger,eval,export,function,get,null,set,undefined,var,with," +
|
|
"Infinity,NaN"];
|
|
var PERL_KEYWORDS = "caller,delete,die,do,dump,elsif,eval,exit,foreach,for," +
|
|
"goto,if,import,last,local,my,next,no,our,print,package,redo,require," +
|
|
"sub,undef,unless,until,use,wantarray,while,BEGIN,END";
|
|
var PYTHON_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "and,as,assert,class,def,del," +
|
|
"elif,except,exec,finally,from,global,import,in,is,lambda," +
|
|
"nonlocal,not,or,pass,print,raise,try,with,yield," +
|
|
"False,True,None"];
|
|
var RUBY_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "alias,and,begin,case,class," +
|
|
"def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo," +
|
|
"rescue,retry,self,super,then,true,undef,unless,until,when,yield," +
|
|
"BEGIN,END"];
|
|
var RUST_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "as,assert,const,copy,drop," +
|
|
"enum,extern,fail,false,fn,impl,let,log,loop,match,mod,move,mut,priv," +
|
|
"pub,pure,ref,self,static,struct,true,trait,type,unsafe,use"];
|
|
var SH_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "case,done,elif,esac,eval,fi," +
|
|
"function,in,local,set,then,until"];
|
|
var ALL_KEYWORDS = [
|
|
CPP_KEYWORDS, CSHARP_KEYWORDS, JSCRIPT_KEYWORDS, PERL_KEYWORDS,
|
|
PYTHON_KEYWORDS, RUBY_KEYWORDS, SH_KEYWORDS];
|
|
var C_TYPES = /^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/;
|
|
|
|
// token style names. correspond to css classes
|
|
/**
|
|
* token style for a string literal
|
|
* @const
|
|
*/
|
|
var PR_STRING = 'str';
|
|
/**
|
|
* token style for a keyword
|
|
* @const
|
|
*/
|
|
var PR_KEYWORD = 'kwd';
|
|
/**
|
|
* token style for a comment
|
|
* @const
|
|
*/
|
|
var PR_COMMENT = 'com';
|
|
/**
|
|
* token style for a type
|
|
* @const
|
|
*/
|
|
var PR_TYPE = 'typ';
|
|
/**
|
|
* token style for a literal value. e.g. 1, null, true.
|
|
* @const
|
|
*/
|
|
var PR_LITERAL = 'lit';
|
|
/**
|
|
* token style for a punctuation string.
|
|
* @const
|
|
*/
|
|
var PR_PUNCTUATION = 'pun';
|
|
/**
|
|
* token style for plain text.
|
|
* @const
|
|
*/
|
|
var PR_PLAIN = 'pln';
|
|
|
|
/**
|
|
* token style for an sgml tag.
|
|
* @const
|
|
*/
|
|
var PR_TAG = 'tag';
|
|
/**
|
|
* token style for a markup declaration such as a DOCTYPE.
|
|
* @const
|
|
*/
|
|
var PR_DECLARATION = 'dec';
|
|
/**
|
|
* token style for embedded source.
|
|
* @const
|
|
*/
|
|
var PR_SOURCE = 'src';
|
|
/**
|
|
* token style for an sgml attribute name.
|
|
* @const
|
|
*/
|
|
var PR_ATTRIB_NAME = 'atn';
|
|
/**
|
|
* token style for an sgml attribute value.
|
|
* @const
|
|
*/
|
|
var PR_ATTRIB_VALUE = 'atv';
|
|
|
|
/**
|
|
* A class that indicates a section of markup that is not code, e.g. to allow
|
|
* embedding of line numbers within code listings.
|
|
* @const
|
|
*/
|
|
var PR_NOCODE = 'nocode';
|
|
|
|
|
|
|
|
/**
|
|
* A set of tokens that can precede a regular expression literal in
|
|
* javascript
|
|
* http://web.archive.org/web/20070717142515/http://www.mozilla.org/js/language/js20/rationale/syntax.html
|
|
* has the full list, but I've removed ones that might be problematic when
|
|
* seen in languages that don't support regular expression literals.
|
|
*
|
|
* <p>Specifically, I've removed any keywords that can't precede a regexp
|
|
* literal in a syntactically legal javascript program, and I've removed the
|
|
* "in" keyword since it's not a keyword in many languages, and might be used
|
|
* as a count of inches.
|
|
*
|
|
* <p>The link above does not accurately describe EcmaScript rules since
|
|
* it fails to distinguish between (a=++/b/i) and (a++/b/i) but it works
|
|
* very well in practice.
|
|
*
|
|
* @private
|
|
* @const
|
|
*/
|
|
var REGEXP_PRECEDER_PATTERN = '(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<<?=?|>>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*';
|
|
|
|
// CAVEAT: this does not properly handle the case where a regular
|
|
// expression immediately follows another since a regular expression may
|
|
// have flags for case-sensitivity and the like. Having regexp tokens
|
|
// adjacent is not valid in any language I'm aware of, so I'm punting.
|
|
// TODO: maybe style special characters inside a regexp as punctuation.
|
|
|
|
/**
|
|
* Given a group of {@link RegExp}s, returns a {@code RegExp} that globally
|
|
* matches the union of the sets of strings matched by the input RegExp.
|
|
* Since it matches globally, if the input strings have a start-of-input
|
|
* anchor (/^.../), it is ignored for the purposes of unioning.
|
|
* @param {Array.<RegExp>} regexs non multiline, non-global regexs.
|
|
* @return {RegExp} a global regex.
|
|
*/
|
|
function combinePrefixPatterns(regexs) {
|
|
var capturedGroupIndex = 0;
|
|
|
|
var needToFoldCase = false;
|
|
var ignoreCase = false;
|
|
for (var i = 0, n = regexs.length; i < n; ++i) {
|
|
var regex = regexs[i];
|
|
if (regex.ignoreCase) {
|
|
ignoreCase = true;
|
|
} else if (/[a-z]/i.test(regex.source.replace(
|
|
/\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, ''))) {
|
|
needToFoldCase = true;
|
|
ignoreCase = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var escapeCharToCodeUnit = {
|
|
'b': 8,
|
|
't': 9,
|
|
'n': 0xa,
|
|
'v': 0xb,
|
|
'f': 0xc,
|
|
'r': 0xd
|
|
};
|
|
|
|
function decodeEscape(charsetPart) {
|
|
var cc0 = charsetPart.charCodeAt(0);
|
|
if (cc0 !== 92 /* \\ */) {
|
|
return cc0;
|
|
}
|
|
var c1 = charsetPart.charAt(1);
|
|
cc0 = escapeCharToCodeUnit[c1];
|
|
if (cc0) {
|
|
return cc0;
|
|
} else if ('0' <= c1 && c1 <= '7') {
|
|
return parseInt(charsetPart.substring(1), 8);
|
|
} else if (c1 === 'u' || c1 === 'x') {
|
|
return parseInt(charsetPart.substring(2), 16);
|
|
} else {
|
|
return charsetPart.charCodeAt(1);
|
|
}
|
|
}
|
|
|
|
function encodeEscape(charCode) {
|
|
if (charCode < 0x20) {
|
|
return (charCode < 0x10 ? '\\x0' : '\\x') + charCode.toString(16);
|
|
}
|
|
var ch = String.fromCharCode(charCode);
|
|
return (ch === '\\' || ch === '-' || ch === ']' || ch === '^')
|
|
? "\\" + ch : ch;
|
|
}
|
|
|
|
function caseFoldCharset(charSet) {
|
|
var charsetParts = charSet.substring(1, charSet.length - 1).match(
|
|
new RegExp(
|
|
'\\\\u[0-9A-Fa-f]{4}'
|
|
+ '|\\\\x[0-9A-Fa-f]{2}'
|
|
+ '|\\\\[0-3][0-7]{0,2}'
|
|
+ '|\\\\[0-7]{1,2}'
|
|
+ '|\\\\[\\s\\S]'
|
|
+ '|-'
|
|
+ '|[^-\\\\]',
|
|
'g'));
|
|
var ranges = [];
|
|
var inverse = charsetParts[0] === '^';
|
|
|
|
var out = ['['];
|
|
if (inverse) { out.push('^'); }
|
|
|
|
for (var i = inverse ? 1 : 0, n = charsetParts.length; i < n; ++i) {
|
|
var p = charsetParts[i];
|
|
if (/\\[bdsw]/i.test(p)) { // Don't muck with named groups.
|
|
out.push(p);
|
|
} else {
|
|
var start = decodeEscape(p);
|
|
var end;
|
|
if (i + 2 < n && '-' === charsetParts[i + 1]) {
|
|
end = decodeEscape(charsetParts[i + 2]);
|
|
i += 2;
|
|
} else {
|
|
end = start;
|
|
}
|
|
ranges.push([start, end]);
|
|
// If the range might intersect letters, then expand it.
|
|
// This case handling is too simplistic.
|
|
// It does not deal with non-latin case folding.
|
|
// It works for latin source code identifiers though.
|
|
if (!(end < 65 || start > 122)) {
|
|
if (!(end < 65 || start > 90)) {
|
|
ranges.push([Math.max(65, start) | 32, Math.min(end, 90) | 32]);
|
|
}
|
|
if (!(end < 97 || start > 122)) {
|
|
ranges.push([Math.max(97, start) & ~32, Math.min(end, 122) & ~32]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// [[1, 10], [3, 4], [8, 12], [14, 14], [16, 16], [17, 17]]
|
|
// -> [[1, 12], [14, 14], [16, 17]]
|
|
ranges.sort(function (a, b) { return (a[0] - b[0]) || (b[1] - a[1]); });
|
|
var consolidatedRanges = [];
|
|
var lastRange = [];
|
|
for (var i = 0; i < ranges.length; ++i) {
|
|
var range = ranges[i];
|
|
if (range[0] <= lastRange[1] + 1) {
|
|
lastRange[1] = Math.max(lastRange[1], range[1]);
|
|
} else {
|
|
consolidatedRanges.push(lastRange = range);
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < consolidatedRanges.length; ++i) {
|
|
var range = consolidatedRanges[i];
|
|
out.push(encodeEscape(range[0]));
|
|
if (range[1] > range[0]) {
|
|
if (range[1] + 1 > range[0]) { out.push('-'); }
|
|
out.push(encodeEscape(range[1]));
|
|
}
|
|
}
|
|
out.push(']');
|
|
return out.join('');
|
|
}
|
|
|
|
function allowAnywhereFoldCaseAndRenumberGroups(regex) {
|
|
// Split into character sets, escape sequences, punctuation strings
|
|
// like ('(', '(?:', ')', '^'), and runs of characters that do not
|
|
// include any of the above.
|
|
var parts = regex.source.match(
|
|
new RegExp(
|
|
'(?:'
|
|
+ '\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]' // a character set
|
|
+ '|\\\\u[A-Fa-f0-9]{4}' // a unicode escape
|
|
+ '|\\\\x[A-Fa-f0-9]{2}' // a hex escape
|
|
+ '|\\\\[0-9]+' // a back-reference or octal escape
|
|
+ '|\\\\[^ux0-9]' // other escape sequence
|
|
+ '|\\(\\?[:!=]' // start of a non-capturing group
|
|
+ '|[\\(\\)\\^]' // start/end of a group, or line start
|
|
+ '|[^\\x5B\\x5C\\(\\)\\^]+' // run of other characters
|
|
+ ')',
|
|
'g'));
|
|
var n = parts.length;
|
|
|
|
// Maps captured group numbers to the number they will occupy in
|
|
// the output or to -1 if that has not been determined, or to
|
|
// undefined if they need not be capturing in the output.
|
|
var capturedGroups = [];
|
|
|
|
// Walk over and identify back references to build the capturedGroups
|
|
// mapping.
|
|
for (var i = 0, groupIndex = 0; i < n; ++i) {
|
|
var p = parts[i];
|
|
if (p === '(') {
|
|
// groups are 1-indexed, so max group index is count of '('
|
|
++groupIndex;
|
|
} else if ('\\' === p.charAt(0)) {
|
|
var decimalValue = +p.substring(1);
|
|
if (decimalValue) {
|
|
if (decimalValue <= groupIndex) {
|
|
capturedGroups[decimalValue] = -1;
|
|
} else {
|
|
// Replace with an unambiguous escape sequence so that
|
|
// an octal escape sequence does not turn into a backreference
|
|
// to a capturing group from an earlier regex.
|
|
parts[i] = encodeEscape(decimalValue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Renumber groups and reduce capturing groups to non-capturing groups
|
|
// where possible.
|
|
for (var i = 1; i < capturedGroups.length; ++i) {
|
|
if (-1 === capturedGroups[i]) {
|
|
capturedGroups[i] = ++capturedGroupIndex;
|
|
}
|
|
}
|
|
for (var i = 0, groupIndex = 0; i < n; ++i) {
|
|
var p = parts[i];
|
|
if (p === '(') {
|
|
++groupIndex;
|
|
if (!capturedGroups[groupIndex]) {
|
|
parts[i] = '(?:';
|
|
}
|
|
} else if ('\\' === p.charAt(0)) {
|
|
var decimalValue = +p.substring(1);
|
|
if (decimalValue && decimalValue <= groupIndex) {
|
|
parts[i] = '\\' + capturedGroups[decimalValue];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove any prefix anchors so that the output will match anywhere.
|
|
// ^^ really does mean an anchored match though.
|
|
for (var i = 0; i < n; ++i) {
|
|
if ('^' === parts[i] && '^' !== parts[i + 1]) { parts[i] = ''; }
|
|
}
|
|
|
|
// Expand letters to groups to handle mixing of case-sensitive and
|
|
// case-insensitive patterns if necessary.
|
|
if (regex.ignoreCase && needToFoldCase) {
|
|
for (var i = 0; i < n; ++i) {
|
|
var p = parts[i];
|
|
var ch0 = p.charAt(0);
|
|
if (p.length >= 2 && ch0 === '[') {
|
|
parts[i] = caseFoldCharset(p);
|
|
} else if (ch0 !== '\\') {
|
|
// TODO: handle letters in numeric escapes.
|
|
parts[i] = p.replace(
|
|
/[a-zA-Z]/g,
|
|
function (ch) {
|
|
var cc = ch.charCodeAt(0);
|
|
return '[' + String.fromCharCode(cc & ~32, cc | 32) + ']';
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return parts.join('');
|
|
}
|
|
|
|
var rewritten = [];
|
|
for (var i = 0, n = regexs.length; i < n; ++i) {
|
|
var regex = regexs[i];
|
|
if (regex.global || regex.multiline) { throw new Error('' + regex); }
|
|
rewritten.push(
|
|
'(?:' + allowAnywhereFoldCaseAndRenumberGroups(regex) + ')');
|
|
}
|
|
|
|
return new RegExp(rewritten.join('|'), ignoreCase ? 'gi' : 'g');
|
|
}
|
|
|
|
/**
|
|
* Split markup into a string of source code and an array mapping ranges in
|
|
* that string to the text nodes in which they appear.
|
|
*
|
|
* <p>
|
|
* The HTML DOM structure:</p>
|
|
* <pre>
|
|
* (Element "p"
|
|
* (Element "b"
|
|
* (Text "print ")) ; #1
|
|
* (Text "'Hello '") ; #2
|
|
* (Element "br") ; #3
|
|
* (Text " + 'World';")) ; #4
|
|
* </pre>
|
|
* <p>
|
|
* corresponds to the HTML
|
|
* {@code <p><b>print </b>'Hello '<br> + 'World';</p>}.</p>
|
|
*
|
|
* <p>
|
|
* It will produce the output:</p>
|
|
* <pre>
|
|
* {
|
|
* sourceCode: "print 'Hello '\n + 'World';",
|
|
* // 1 2
|
|
* // 012345678901234 5678901234567
|
|
* spans: [0, #1, 6, #2, 14, #3, 15, #4]
|
|
* }
|
|
* </pre>
|
|
* <p>
|
|
* where #1 is a reference to the {@code "print "} text node above, and so
|
|
* on for the other text nodes.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* The {@code} spans array is an array of pairs. Even elements are the start
|
|
* indices of substrings, and odd elements are the text nodes (or BR elements)
|
|
* that contain the text for those substrings.
|
|
* Substrings continue until the next index or the end of the source.
|
|
* </p>
|
|
*
|
|
* @param {Node} node an HTML DOM subtree containing source-code.
|
|
* @param {boolean} isPreformatted true if white-space in text nodes should
|
|
* be considered significant.
|
|
* @return {Object} source code and the text nodes in which they occur.
|
|
*/
|
|
function extractSourceSpans(node, isPreformatted) {
|
|
var nocode = /(?:^|\s)nocode(?:\s|$)/;
|
|
|
|
var chunks = [];
|
|
var length = 0;
|
|
var spans = [];
|
|
var k = 0;
|
|
|
|
function walk(node) {
|
|
var type = node.nodeType;
|
|
if (type == 1) { // Element
|
|
if (nocode.test(node.className)) { return; }
|
|
for (var child = node.firstChild; child; child = child.nextSibling) {
|
|
walk(child);
|
|
}
|
|
var nodeName = node.nodeName.toLowerCase();
|
|
if ('br' === nodeName || 'li' === nodeName) {
|
|
chunks[k] = '\n';
|
|
spans[k << 1] = length++;
|
|
spans[(k++ << 1) | 1] = node;
|
|
}
|
|
} else if (type == 3 || type == 4) { // Text
|
|
var text = node.nodeValue;
|
|
if (text.length) {
|
|
if (!isPreformatted) {
|
|
text = text.replace(/[ \t\r\n]+/g, ' ');
|
|
} else {
|
|
text = text.replace(/\r\n?/g, '\n'); // Normalize newlines.
|
|
}
|
|
// TODO: handle tabs here?
|
|
chunks[k] = text;
|
|
spans[k << 1] = length;
|
|
length += text.length;
|
|
spans[(k++ << 1) | 1] = node;
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(node);
|
|
|
|
return {
|
|
sourceCode: chunks.join('').replace(/\n$/, ''),
|
|
spans: spans
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Apply the given language handler to sourceCode and add the resulting
|
|
* decorations to out.
|
|
* @param {number} basePos the index of sourceCode within the chunk of source
|
|
* whose decorations are already present on out.
|
|
*/
|
|
function appendDecorations(basePos, sourceCode, langHandler, out) {
|
|
if (!sourceCode) { return; }
|
|
var job = {
|
|
sourceCode: sourceCode,
|
|
basePos: basePos
|
|
};
|
|
langHandler(job);
|
|
out.push.apply(out, job.decorations);
|
|
}
|
|
|
|
var notWs = /\S/;
|
|
|
|
/**
|
|
* Given an element, if it contains only one child element and any text nodes
|
|
* it contains contain only space characters, return the sole child element.
|
|
* Otherwise returns undefined.
|
|
* <p>
|
|
* This is meant to return the CODE element in {@code <pre><code ...>} when
|
|
* there is a single child element that contains all the non-space textual
|
|
* content, but not to return anything where there are multiple child elements
|
|
* as in {@code <pre><code>...</code><code>...</code></pre>} or when there
|
|
* is textual content.
|
|
*/
|
|
function childContentWrapper(element) {
|
|
var wrapper = undefined;
|
|
for (var c = element.firstChild; c; c = c.nextSibling) {
|
|
var type = c.nodeType;
|
|
wrapper = (type === 1) // Element Node
|
|
? (wrapper ? element : c)
|
|
: (type === 3) // Text Node
|
|
? (notWs.test(c.nodeValue) ? element : wrapper)
|
|
: wrapper;
|
|
}
|
|
return wrapper === element ? undefined : wrapper;
|
|
}
|
|
|
|
/** Given triples of [style, pattern, context] returns a lexing function,
|
|
* The lexing function interprets the patterns to find token boundaries and
|
|
* returns a decoration list of the form
|
|
* [index_0, style_0, index_1, style_1, ..., index_n, style_n]
|
|
* where index_n is an index into the sourceCode, and style_n is a style
|
|
* constant like PR_PLAIN. index_n-1 <= index_n, and style_n-1 applies to
|
|
* all characters in sourceCode[index_n-1:index_n].
|
|
*
|
|
* The stylePatterns is a list whose elements have the form
|
|
* [style : string, pattern : RegExp, DEPRECATED, shortcut : string].
|
|
*
|
|
* Style is a style constant like PR_PLAIN, or can be a string of the
|
|
* form 'lang-FOO', where FOO is a language extension describing the
|
|
* language of the portion of the token in $1 after pattern executes.
|
|
* E.g., if style is 'lang-lisp', and group 1 contains the text
|
|
* '(hello (world))', then that portion of the token will be passed to the
|
|
* registered lisp handler for formatting.
|
|
* The text before and after group 1 will be restyled using this decorator
|
|
* so decorators should take care that this doesn't result in infinite
|
|
* recursion. For example, the HTML lexer rule for SCRIPT elements looks
|
|
* something like ['lang-js', /<[s]cript>(.+?)<\/script>/]. This may match
|
|
* '<script>foo()<\/script>', which would cause the current decorator to
|
|
* be called with '<script>' which would not match the same rule since
|
|
* group 1 must not be empty, so it would be instead styled as PR_TAG by
|
|
* the generic tag rule. The handler registered for the 'js' extension would
|
|
* then be called with 'foo()', and finally, the current decorator would
|
|
* be called with '<\/script>' which would not match the original rule and
|
|
* so the generic tag rule would identify it as a tag.
|
|
*
|
|
* Pattern must only match prefixes, and if it matches a prefix, then that
|
|
* match is considered a token with the same style.
|
|
*
|
|
* Context is applied to the last non-whitespace, non-comment token
|
|
* recognized.
|
|
*
|
|
* Shortcut is an optional string of characters, any of which, if the first
|
|
* character, gurantee that this pattern and only this pattern matches.
|
|
*
|
|
* @param {Array} shortcutStylePatterns patterns that always start with
|
|
* a known character. Must have a shortcut string.
|
|
* @param {Array} fallthroughStylePatterns patterns that will be tried in
|
|
* order if the shortcut ones fail. May have shortcuts.
|
|
*
|
|
* @return {function (Object)} a
|
|
* function that takes source code and returns a list of decorations.
|
|
*/
|
|
function createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns) {
|
|
var shortcuts = {};
|
|
var tokenizer;
|
|
(function () {
|
|
var allPatterns = shortcutStylePatterns.concat(fallthroughStylePatterns);
|
|
var allRegexs = [];
|
|
var regexKeys = {};
|
|
for (var i = 0, n = allPatterns.length; i < n; ++i) {
|
|
var patternParts = allPatterns[i];
|
|
var shortcutChars = patternParts[3];
|
|
if (shortcutChars) {
|
|
for (var c = shortcutChars.length; --c >= 0;) {
|
|
shortcuts[shortcutChars.charAt(c)] = patternParts;
|
|
}
|
|
}
|
|
var regex = patternParts[1];
|
|
var k = '' + regex;
|
|
if (!regexKeys.hasOwnProperty(k)) {
|
|
allRegexs.push(regex);
|
|
regexKeys[k] = null;
|
|
}
|
|
}
|
|
allRegexs.push(/[\0-\uffff]/);
|
|
tokenizer = combinePrefixPatterns(allRegexs);
|
|
})();
|
|
|
|
var nPatterns = fallthroughStylePatterns.length;
|
|
|
|
/**
|
|
* Lexes job.sourceCode and produces an output array job.decorations of
|
|
* style classes preceded by the position at which they start in
|
|
* job.sourceCode in order.
|
|
*
|
|
* @param {Object} job an object like <pre>{
|
|
* sourceCode: {string} sourceText plain text,
|
|
* basePos: {int} position of job.sourceCode in the larger chunk of
|
|
* sourceCode.
|
|
* }</pre>
|
|
*/
|
|
var decorate = function (job) {
|
|
var sourceCode = job.sourceCode, basePos = job.basePos;
|
|
/** Even entries are positions in source in ascending order. Odd enties
|
|
* are style markers (e.g., PR_COMMENT) that run from that position until
|
|
* the end.
|
|
* @type {Array.<number|string>}
|
|
*/
|
|
var decorations = [basePos, PR_PLAIN];
|
|
var pos = 0; // index into sourceCode
|
|
var tokens = sourceCode.match(tokenizer) || [];
|
|
var styleCache = {};
|
|
|
|
for (var ti = 0, nTokens = tokens.length; ti < nTokens; ++ti) {
|
|
var token = tokens[ti];
|
|
var style = styleCache[token];
|
|
var match = void 0;
|
|
|
|
var isEmbedded;
|
|
if (typeof style === 'string') {
|
|
isEmbedded = false;
|
|
} else {
|
|
var patternParts = shortcuts[token.charAt(0)];
|
|
if (patternParts) {
|
|
match = token.match(patternParts[1]);
|
|
style = patternParts[0];
|
|
} else {
|
|
for (var i = 0; i < nPatterns; ++i) {
|
|
patternParts = fallthroughStylePatterns[i];
|
|
match = token.match(patternParts[1]);
|
|
if (match) {
|
|
style = patternParts[0];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!match) { // make sure that we make progress
|
|
style = PR_PLAIN;
|
|
}
|
|
}
|
|
|
|
isEmbedded = style.length >= 5 && 'lang-' === style.substring(0, 5);
|
|
if (isEmbedded && !(match && typeof match[1] === 'string')) {
|
|
isEmbedded = false;
|
|
style = PR_SOURCE;
|
|
}
|
|
|
|
if (!isEmbedded) { styleCache[token] = style; }
|
|
}
|
|
|
|
var tokenStart = pos;
|
|
pos += token.length;
|
|
|
|
if (!isEmbedded) {
|
|
decorations.push(basePos + tokenStart, style);
|
|
} else { // Treat group 1 as an embedded block of source code.
|
|
var embeddedSource = match[1];
|
|
var embeddedSourceStart = token.indexOf(embeddedSource);
|
|
var embeddedSourceEnd = embeddedSourceStart + embeddedSource.length;
|
|
if (match[2]) {
|
|
// If embeddedSource can be blank, then it would match at the
|
|
// beginning which would cause us to infinitely recurse on the
|
|
// entire token, so we catch the right context in match[2].
|
|
embeddedSourceEnd = token.length - match[2].length;
|
|
embeddedSourceStart = embeddedSourceEnd - embeddedSource.length;
|
|
}
|
|
var lang = style.substring(5);
|
|
// Decorate the left of the embedded source
|
|
appendDecorations(
|
|
basePos + tokenStart,
|
|
token.substring(0, embeddedSourceStart),
|
|
decorate, decorations);
|
|
// Decorate the embedded source
|
|
appendDecorations(
|
|
basePos + tokenStart + embeddedSourceStart,
|
|
embeddedSource,
|
|
langHandlerForExtension(lang, embeddedSource),
|
|
decorations);
|
|
// Decorate the right of the embedded section
|
|
appendDecorations(
|
|
basePos + tokenStart + embeddedSourceEnd,
|
|
token.substring(embeddedSourceEnd),
|
|
decorate, decorations);
|
|
}
|
|
}
|
|
job.decorations = decorations;
|
|
};
|
|
return decorate;
|
|
}
|
|
|
|
/** returns a function that produces a list of decorations from source text.
|
|
*
|
|
* This code treats ", ', and ` as string delimiters, and \ as a string
|
|
* escape. It does not recognize perl's qq() style strings.
|
|
* It has no special handling for double delimiter escapes as in basic, or
|
|
* the tripled delimiters used in python, but should work on those regardless
|
|
* although in those cases a single string literal may be broken up into
|
|
* multiple adjacent string literals.
|
|
*
|
|
* It recognizes C, C++, and shell style comments.
|
|
*
|
|
* @param {Object} options a set of optional parameters.
|
|
* @return {function (Object)} a function that examines the source code
|
|
* in the input job and builds the decoration list.
|
|
*/
|
|
function sourceDecorator(options) {
|
|
var shortcutStylePatterns = [], fallthroughStylePatterns = [];
|
|
if (options['tripleQuotedStrings']) {
|
|
// '''multi-line-string''', 'single-line-string', and double-quoted
|
|
shortcutStylePatterns.push(
|
|
[PR_STRING, /^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,
|
|
null, '\'"']);
|
|
} else if (options['multiLineStrings']) {
|
|
// 'multi-line-string', "multi-line-string"
|
|
shortcutStylePatterns.push(
|
|
[PR_STRING, /^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,
|
|
null, '\'"`']);
|
|
} else {
|
|
// 'single-line-string', "single-line-string"
|
|
shortcutStylePatterns.push(
|
|
[PR_STRING,
|
|
/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,
|
|
null, '"\'']);
|
|
}
|
|
if (options['verbatimStrings']) {
|
|
// verbatim-string-literal production from the C# grammar. See issue 93.
|
|
fallthroughStylePatterns.push(
|
|
[PR_STRING, /^@\"(?:[^\"]|\"\")*(?:\"|$)/, null]);
|
|
}
|
|
var hc = options['hashComments'];
|
|
if (hc) {
|
|
if (options['cStyleComments']) {
|
|
if (hc > 1) { // multiline hash comments
|
|
shortcutStylePatterns.push(
|
|
[PR_COMMENT, /^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/, null, '#']);
|
|
} else {
|
|
// Stop C preprocessor declarations at an unclosed open comment
|
|
shortcutStylePatterns.push(
|
|
[PR_COMMENT, /^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\r\n]*)/,
|
|
null, '#']);
|
|
}
|
|
// #include <stdio.h>
|
|
fallthroughStylePatterns.push(
|
|
[PR_STRING,
|
|
/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,
|
|
null]);
|
|
} else {
|
|
shortcutStylePatterns.push([PR_COMMENT, /^#[^\r\n]*/, null, '#']);
|
|
}
|
|
}
|
|
if (options['cStyleComments']) {
|
|
fallthroughStylePatterns.push([PR_COMMENT, /^\/\/[^\r\n]*/, null]);
|
|
fallthroughStylePatterns.push(
|
|
[PR_COMMENT, /^\/\*[\s\S]*?(?:\*\/|$)/, null]);
|
|
}
|
|
var regexLiterals = options['regexLiterals'];
|
|
if (regexLiterals) {
|
|
/**
|
|
* @const
|
|
*/
|
|
var regexExcls = regexLiterals > 1
|
|
? '' // Multiline regex literals
|
|
: '\n\r';
|
|
/**
|
|
* @const
|
|
*/
|
|
var regexAny = regexExcls ? '.' : '[\\S\\s]';
|
|
/**
|
|
* @const
|
|
*/
|
|
var REGEX_LITERAL = (
|
|
// A regular expression literal starts with a slash that is
|
|
// not followed by * or / so that it is not confused with
|
|
// comments.
|
|
'/(?=[^/*' + regexExcls + '])'
|
|
// and then contains any number of raw characters,
|
|
+ '(?:[^/\\x5B\\x5C' + regexExcls + ']'
|
|
// escape sequences (\x5C),
|
|
+ '|\\x5C' + regexAny
|
|
// or non-nesting character sets (\x5B\x5D);
|
|
+ '|\\x5B(?:[^\\x5C\\x5D' + regexExcls + ']'
|
|
+ '|\\x5C' + regexAny + ')*(?:\\x5D|$))+'
|
|
// finally closed by a /.
|
|
+ '/');
|
|
fallthroughStylePatterns.push(
|
|
['lang-regex',
|
|
RegExp('^' + REGEXP_PRECEDER_PATTERN + '(' + REGEX_LITERAL + ')')
|
|
]);
|
|
}
|
|
|
|
var types = options['types'];
|
|
if (types) {
|
|
fallthroughStylePatterns.push([PR_TYPE, types]);
|
|
}
|
|
|
|
var keywords = ("" + options['keywords']).replace(/^ | $/g, '');
|
|
if (keywords.length) {
|
|
fallthroughStylePatterns.push(
|
|
[PR_KEYWORD,
|
|
new RegExp('^(?:' + keywords.replace(/[\s,]+/g, '|') + ')\\b'),
|
|
null]);
|
|
}
|
|
|
|
shortcutStylePatterns.push([PR_PLAIN, /^\s+/, null, ' \r\n\t\xA0']);
|
|
|
|
var punctuation =
|
|
// The Bash man page says
|
|
|
|
// A word is a sequence of characters considered as a single
|
|
// unit by GRUB. Words are separated by metacharacters,
|
|
// which are the following plus space, tab, and newline: { }
|
|
// | & $ ; < >
|
|
// ...
|
|
|
|
// A word beginning with # causes that word and all remaining
|
|
// characters on that line to be ignored.
|
|
|
|
// which means that only a '#' after /(?:^|[{}|&$;<>\s])/ starts a
|
|
// comment but empirically
|
|
// $ echo {#}
|
|
// {#}
|
|
// $ echo \$#
|
|
// $#
|
|
// $ echo }#
|
|
// }#
|
|
|
|
// so /(?:^|[|&;<>\s])/ is more appropriate.
|
|
|
|
// http://gcc.gnu.org/onlinedocs/gcc-2.95.3/cpp_1.html#SEC3
|
|
// suggests that this definition is compatible with a
|
|
// default mode that tries to use a single token definition
|
|
// to recognize both bash/python style comments and C
|
|
// preprocessor directives.
|
|
|
|
// This definition of punctuation does not include # in the list of
|
|
// follow-on exclusions, so # will not be broken before if preceeded
|
|
// by a punctuation character. We could try to exclude # after
|
|
// [|&;<>] but that doesn't seem to cause many major problems.
|
|
// If that does turn out to be a problem, we should change the below
|
|
// when hc is truthy to include # in the run of punctuation characters
|
|
// only when not followint [|&;<>].
|
|
'^.[^\\s\\w.$@\'"`/\\\\]*';
|
|
if (options['regexLiterals']) {
|
|
punctuation += '(?!\s*\/)';
|
|
}
|
|
|
|
fallthroughStylePatterns.push(
|
|
// TODO(mikesamuel): recognize non-latin letters and numerals in idents
|
|
[PR_LITERAL, /^@[a-z_$][a-z_$@0-9]*/i, null],
|
|
[PR_TYPE, /^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/, null],
|
|
[PR_PLAIN, /^[a-z_$][a-z_$@0-9]*/i, null],
|
|
[PR_LITERAL,
|
|
new RegExp(
|
|
'^(?:'
|
|
// A hex number
|
|
+ '0x[a-f0-9]+'
|
|
// or an octal or decimal number,
|
|
+ '|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)'
|
|
// possibly in scientific notation
|
|
+ '(?:e[+\\-]?\\d+)?'
|
|
+ ')'
|
|
// with an optional modifier like UL for unsigned long
|
|
+ '[a-z]*', 'i'),
|
|
null, '0123456789'],
|
|
// Don't treat escaped quotes in bash as starting strings.
|
|
// See issue 144.
|
|
[PR_PLAIN, /^\\[\s\S]?/, null],
|
|
[PR_PUNCTUATION, new RegExp(punctuation), null]);
|
|
|
|
return createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns);
|
|
}
|
|
|
|
var decorateSource = sourceDecorator({
|
|
'keywords': ALL_KEYWORDS,
|
|
'hashComments': true,
|
|
'cStyleComments': true,
|
|
'multiLineStrings': true,
|
|
'regexLiterals': true
|
|
});
|
|
|
|
/**
|
|
* Given a DOM subtree, wraps it in a list, and puts each line into its own
|
|
* list item.
|
|
*
|
|
* @param {Node} node modified in place. Its content is pulled into an
|
|
* HTMLOListElement, and each line is moved into a separate list item.
|
|
* This requires cloning elements, so the input might not have unique
|
|
* IDs after numbering.
|
|
* @param {boolean} isPreformatted true iff white-space in text nodes should
|
|
* be treated as significant.
|
|
*/
|
|
function numberLines(node, opt_startLineNum, isPreformatted) {
|
|
var nocode = /(?:^|\s)nocode(?:\s|$)/;
|
|
var lineBreak = /\r\n?|\n/;
|
|
|
|
var document = node.ownerDocument;
|
|
|
|
var li = document.createElement('li');
|
|
while (node.firstChild) {
|
|
li.appendChild(node.firstChild);
|
|
}
|
|
// An array of lines. We split below, so this is initialized to one
|
|
// un-split line.
|
|
var listItems = [li];
|
|
|
|
function walk(node) {
|
|
var type = node.nodeType;
|
|
if (type == 1 && !nocode.test(node.className)) { // Element
|
|
if ('br' === node.nodeName) {
|
|
breakAfter(node);
|
|
// Discard the <BR> since it is now flush against a </LI>.
|
|
if (node.parentNode) {
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
} else {
|
|
for (var child = node.firstChild; child; child = child.nextSibling) {
|
|
walk(child);
|
|
}
|
|
}
|
|
} else if ((type == 3 || type == 4) && isPreformatted) { // Text
|
|
var text = node.nodeValue;
|
|
var match = text.match(lineBreak);
|
|
if (match) {
|
|
var firstLine = text.substring(0, match.index);
|
|
node.nodeValue = firstLine;
|
|
var tail = text.substring(match.index + match[0].length);
|
|
if (tail) {
|
|
var parent = node.parentNode;
|
|
parent.insertBefore(
|
|
document.createTextNode(tail), node.nextSibling);
|
|
}
|
|
breakAfter(node);
|
|
if (!firstLine) {
|
|
// Don't leave blank text nodes in the DOM.
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Split a line after the given node.
|
|
function breakAfter(lineEndNode) {
|
|
// If there's nothing to the right, then we can skip ending the line
|
|
// here, and move root-wards since splitting just before an end-tag
|
|
// would require us to create a bunch of empty copies.
|
|
while (!lineEndNode.nextSibling) {
|
|
lineEndNode = lineEndNode.parentNode;
|
|
if (!lineEndNode) { return; }
|
|
}
|
|
|
|
function breakLeftOf(limit, copy) {
|
|
// Clone shallowly if this node needs to be on both sides of the break.
|
|
var rightSide = copy ? limit.cloneNode(false) : limit;
|
|
var parent = limit.parentNode;
|
|
if (parent) {
|
|
// We clone the parent chain.
|
|
// This helps us resurrect important styling elements that cross lines.
|
|
// E.g. in <i>Foo<br>Bar</i>
|
|
// should be rewritten to <li><i>Foo</i></li><li><i>Bar</i></li>.
|
|
var parentClone = breakLeftOf(parent, 1);
|
|
// Move the clone and everything to the right of the original
|
|
// onto the cloned parent.
|
|
var next = limit.nextSibling;
|
|
parentClone.appendChild(rightSide);
|
|
for (var sibling = next; sibling; sibling = next) {
|
|
next = sibling.nextSibling;
|
|
parentClone.appendChild(sibling);
|
|
}
|
|
}
|
|
return rightSide;
|
|
}
|
|
|
|
var copiedListItem = breakLeftOf(lineEndNode.nextSibling, 0);
|
|
|
|
// Walk the parent chain until we reach an unattached LI.
|
|
for (var parent;
|
|
// Check nodeType since IE invents document fragments.
|
|
(parent = copiedListItem.parentNode) && parent.nodeType === 1;) {
|
|
copiedListItem = parent;
|
|
}
|
|
// Put it on the list of lines for later processing.
|
|
listItems.push(copiedListItem);
|
|
}
|
|
|
|
// Split lines while there are lines left to split.
|
|
for (var i = 0; // Number of lines that have been split so far.
|
|
i < listItems.length; // length updated by breakAfter calls.
|
|
++i) {
|
|
walk(listItems[i]);
|
|
}
|
|
|
|
// Make sure numeric indices show correctly.
|
|
if (opt_startLineNum === (opt_startLineNum|0)) {
|
|
listItems[0].setAttribute('value', opt_startLineNum);
|
|
}
|
|
|
|
var ol = document.createElement('ol');
|
|
ol.className = 'linenums';
|
|
var offset = Math.max(0, ((opt_startLineNum - 1 /* zero index */)) | 0) || 0;
|
|
for (var i = 0, n = listItems.length; i < n; ++i) {
|
|
li = listItems[i];
|
|
// Stick a class on the LIs so that stylesheets can
|
|
// color odd/even rows, or any other row pattern that
|
|
// is co-prime with 10.
|
|
li.className = 'L' + ((i + offset) % 10);
|
|
if (!li.firstChild) {
|
|
li.appendChild(document.createTextNode('\xA0'));
|
|
}
|
|
ol.appendChild(li);
|
|
}
|
|
|
|
node.appendChild(ol);
|
|
}
|
|
/**
|
|
* Breaks {@code job.sourceCode} around style boundaries in
|
|
* {@code job.decorations} and modifies {@code job.sourceNode} in place.
|
|
* @param {Object} job like <pre>{
|
|
* sourceCode: {string} source as plain text,
|
|
* sourceNode: {HTMLElement} the element containing the source,
|
|
* spans: {Array.<number|Node>} alternating span start indices into source
|
|
* and the text node or element (e.g. {@code <BR>}) corresponding to that
|
|
* span.
|
|
* decorations: {Array.<number|string} an array of style classes preceded
|
|
* by the position at which they start in job.sourceCode in order
|
|
* }</pre>
|
|
* @private
|
|
*/
|
|
function recombineTagsAndDecorations(job) {
|
|
var isIE8OrEarlier = /\bMSIE\s(\d+)/.exec(navigator.userAgent);
|
|
isIE8OrEarlier = isIE8OrEarlier && +isIE8OrEarlier[1] <= 8;
|
|
var newlineRe = /\n/g;
|
|
|
|
var source = job.sourceCode;
|
|
var sourceLength = source.length;
|
|
// Index into source after the last code-unit recombined.
|
|
var sourceIndex = 0;
|
|
|
|
var spans = job.spans;
|
|
var nSpans = spans.length;
|
|
// Index into spans after the last span which ends at or before sourceIndex.
|
|
var spanIndex = 0;
|
|
|
|
var decorations = job.decorations;
|
|
var nDecorations = decorations.length;
|
|
// Index into decorations after the last decoration which ends at or before
|
|
// sourceIndex.
|
|
var decorationIndex = 0;
|
|
|
|
// Remove all zero-length decorations.
|
|
decorations[nDecorations] = sourceLength;
|
|
var decPos, i;
|
|
for (i = decPos = 0; i < nDecorations;) {
|
|
if (decorations[i] !== decorations[i + 2]) {
|
|
decorations[decPos++] = decorations[i++];
|
|
decorations[decPos++] = decorations[i++];
|
|
} else {
|
|
i += 2;
|
|
}
|
|
}
|
|
nDecorations = decPos;
|
|
|
|
// Simplify decorations.
|
|
for (i = decPos = 0; i < nDecorations;) {
|
|
var startPos = decorations[i];
|
|
// Conflate all adjacent decorations that use the same style.
|
|
var startDec = decorations[i + 1];
|
|
var end = i + 2;
|
|
while (end + 2 <= nDecorations && decorations[end + 1] === startDec) {
|
|
end += 2;
|
|
}
|
|
decorations[decPos++] = startPos;
|
|
decorations[decPos++] = startDec;
|
|
i = end;
|
|
}
|
|
|
|
nDecorations = decorations.length = decPos;
|
|
|
|
var sourceNode = job.sourceNode;
|
|
var oldDisplay;
|
|
if (sourceNode) {
|
|
oldDisplay = sourceNode.style.display;
|
|
sourceNode.style.display = 'none';
|
|
}
|
|
try {
|
|
var decoration = null;
|
|
while (spanIndex < nSpans) {
|
|
var spanStart = spans[spanIndex];
|
|
var spanEnd = spans[spanIndex + 2] || sourceLength;
|
|
|
|
var decEnd = decorations[decorationIndex + 2] || sourceLength;
|
|
|
|
var end = Math.min(spanEnd, decEnd);
|
|
|
|
var textNode = spans[spanIndex + 1];
|
|
var styledText;
|
|
if (textNode.nodeType !== 1 // Don't muck with <BR>s or <LI>s
|
|
// Don't introduce spans around empty text nodes.
|
|
&& (styledText = source.substring(sourceIndex, end))) {
|
|
// This may seem bizarre, and it is. Emitting LF on IE causes the
|
|
// code to display with spaces instead of line breaks.
|
|
// Emitting Windows standard issue linebreaks (CRLF) causes a blank
|
|
// space to appear at the beginning of every line but the first.
|
|
// Emitting an old Mac OS 9 line separator makes everything spiffy.
|
|
if (isIE8OrEarlier) {
|
|
styledText = styledText.replace(newlineRe, '\r');
|
|
}
|
|
textNode.nodeValue = styledText;
|
|
var document = textNode.ownerDocument;
|
|
var span = document.createElement('span');
|
|
span.className = decorations[decorationIndex + 1];
|
|
var parentNode = textNode.parentNode;
|
|
parentNode.replaceChild(span, textNode);
|
|
span.appendChild(textNode);
|
|
if (sourceIndex < spanEnd) { // Split off a text node.
|
|
spans[spanIndex + 1] = textNode
|
|
// TODO: Possibly optimize by using '' if there's no flicker.
|
|
= document.createTextNode(source.substring(end, spanEnd));
|
|
parentNode.insertBefore(textNode, span.nextSibling);
|
|
}
|
|
}
|
|
|
|
sourceIndex = end;
|
|
|
|
if (sourceIndex >= spanEnd) {
|
|
spanIndex += 2;
|
|
}
|
|
if (sourceIndex >= decEnd) {
|
|
decorationIndex += 2;
|
|
}
|
|
}
|
|
} finally {
|
|
if (sourceNode) {
|
|
sourceNode.style.display = oldDisplay;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Maps language-specific file extensions to handlers. */
|
|
var langHandlerRegistry = {};
|
|
/** Register a language handler for the given file extensions.
|
|
* @param {function (Object)} handler a function from source code to a list
|
|
* of decorations. Takes a single argument job which describes the
|
|
* state of the computation. The single parameter has the form
|
|
* {@code {
|
|
* sourceCode: {string} as plain text.
|
|
* decorations: {Array.<number|string>} an array of style classes
|
|
* preceded by the position at which they start in
|
|
* job.sourceCode in order.
|
|
* The language handler should assigned this field.
|
|
* basePos: {int} the position of source in the larger source chunk.
|
|
* All positions in the output decorations array are relative
|
|
* to the larger source chunk.
|
|
* } }
|
|
* @param {Array.<string>} fileExtensions
|
|
*/
|
|
function registerLangHandler(handler, fileExtensions) {
|
|
for (var i = fileExtensions.length; --i >= 0;) {
|
|
var ext = fileExtensions[i];
|
|
if (!langHandlerRegistry.hasOwnProperty(ext)) {
|
|
langHandlerRegistry[ext] = handler;
|
|
} else if (win['console']) {
|
|
console['warn']('cannot override language handler %s', ext);
|
|
}
|
|
}
|
|
}
|
|
function langHandlerForExtension(extension, source) {
|
|
if (!(extension && langHandlerRegistry.hasOwnProperty(extension))) {
|
|
// Treat it as markup if the first non whitespace character is a < and
|
|
// the last non-whitespace character is a >.
|
|
extension = /^\s*</.test(source)
|
|
? 'default-markup'
|
|
: 'default-code';
|
|
}
|
|
return langHandlerRegistry[extension];
|
|
}
|
|
registerLangHandler(decorateSource, ['default-code']);
|
|
registerLangHandler(
|
|
createSimpleLexer(
|
|
[],
|
|
[
|
|
[PR_PLAIN, /^[^<?]+/],
|
|
[PR_DECLARATION, /^<!\w[^>]*(?:>|$)/],
|
|
[PR_COMMENT, /^<\!--[\s\S]*?(?:-\->|$)/],
|
|
// Unescaped content in an unknown language
|
|
['lang-', /^<\?([\s\S]+?)(?:\?>|$)/],
|
|
['lang-', /^<%([\s\S]+?)(?:%>|$)/],
|
|
[PR_PUNCTUATION, /^(?:<[%?]|[%?]>)/],
|
|
['lang-', /^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],
|
|
// Unescaped content in javascript. (Or possibly vbscript).
|
|
['lang-js', /^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],
|
|
// Contains unescaped stylesheet content
|
|
['lang-css', /^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],
|
|
['lang-in.tag', /^(<\/?[a-z][^<>]*>)/i]
|
|
]),
|
|
['default-markup', 'htm', 'html', 'mxml', 'xhtml', 'xml', 'xsl']);
|
|
registerLangHandler(
|
|
createSimpleLexer(
|
|
[
|
|
[PR_PLAIN, /^[\s]+/, null, ' \t\r\n'],
|
|
[PR_ATTRIB_VALUE, /^(?:\"[^\"]*\"?|\'[^\']*\'?)/, null, '\"\'']
|
|
],
|
|
[
|
|
[PR_TAG, /^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],
|
|
[PR_ATTRIB_NAME, /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],
|
|
['lang-uq.val', /^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],
|
|
[PR_PUNCTUATION, /^[=<>\/]+/],
|
|
['lang-js', /^on\w+\s*=\s*\"([^\"]+)\"/i],
|
|
['lang-js', /^on\w+\s*=\s*\'([^\']+)\'/i],
|
|
['lang-js', /^on\w+\s*=\s*([^\"\'>\s]+)/i],
|
|
['lang-css', /^style\s*=\s*\"([^\"]+)\"/i],
|
|
['lang-css', /^style\s*=\s*\'([^\']+)\'/i],
|
|
['lang-css', /^style\s*=\s*([^\"\'>\s]+)/i]
|
|
]),
|
|
['in.tag']);
|
|
registerLangHandler(
|
|
createSimpleLexer([], [[PR_ATTRIB_VALUE, /^[\s\S]+/]]), ['uq.val']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': CPP_KEYWORDS,
|
|
'hashComments': true,
|
|
'cStyleComments': true,
|
|
'types': C_TYPES
|
|
}), ['c', 'cc', 'cpp', 'cxx', 'cyc', 'm']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': 'null,true,false'
|
|
}), ['json']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': CSHARP_KEYWORDS,
|
|
'hashComments': true,
|
|
'cStyleComments': true,
|
|
'verbatimStrings': true,
|
|
'types': C_TYPES
|
|
}), ['cs']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': JAVA_KEYWORDS,
|
|
'cStyleComments': true
|
|
}), ['java']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': SH_KEYWORDS,
|
|
'hashComments': true,
|
|
'multiLineStrings': true
|
|
}), ['bash', 'bsh', 'csh', 'sh']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': PYTHON_KEYWORDS,
|
|
'hashComments': true,
|
|
'multiLineStrings': true,
|
|
'tripleQuotedStrings': true
|
|
}), ['cv', 'py', 'python']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': PERL_KEYWORDS,
|
|
'hashComments': true,
|
|
'multiLineStrings': true,
|
|
'regexLiterals': 2 // multiline regex literals
|
|
}), ['perl', 'pl', 'pm']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': RUBY_KEYWORDS,
|
|
'hashComments': true,
|
|
'multiLineStrings': true,
|
|
'regexLiterals': true
|
|
}), ['rb', 'ruby']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': JSCRIPT_KEYWORDS,
|
|
'cStyleComments': true,
|
|
'regexLiterals': true
|
|
}), ['javascript', 'js']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': COFFEE_KEYWORDS,
|
|
'hashComments': 3, // ### style block comments
|
|
'cStyleComments': true,
|
|
'multilineStrings': true,
|
|
'tripleQuotedStrings': true,
|
|
'regexLiterals': true
|
|
}), ['coffee']);
|
|
registerLangHandler(sourceDecorator({
|
|
'keywords': RUST_KEYWORDS,
|
|
'cStyleComments': true,
|
|
'multilineStrings': true
|
|
}), ['rc', 'rs', 'rust']);
|
|
registerLangHandler(
|
|
createSimpleLexer([], [[PR_STRING, /^[\s\S]+/]]), ['regex']);
|
|
|
|
function applyDecorator(job) {
|
|
var opt_langExtension = job.langExtension;
|
|
|
|
try {
|
|
// Extract tags, and convert the source code to plain text.
|
|
var sourceAndSpans = extractSourceSpans(job.sourceNode, job.pre);
|
|
/** Plain text. @type {string} */
|
|
var source = sourceAndSpans.sourceCode;
|
|
job.sourceCode = source;
|
|
job.spans = sourceAndSpans.spans;
|
|
job.basePos = 0;
|
|
|
|
// Apply the appropriate language handler
|
|
langHandlerForExtension(opt_langExtension, source)(job);
|
|
|
|
// Integrate the decorations and tags back into the source code,
|
|
// modifying the sourceNode in place.
|
|
recombineTagsAndDecorations(job);
|
|
} catch (e) {
|
|
if (win['console']) {
|
|
console['log'](e && e['stack'] || e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pretty print a chunk of code.
|
|
* @param sourceCodeHtml {string} The HTML to pretty print.
|
|
* @param opt_langExtension {string} The language name to use.
|
|
* Typically, a filename extension like 'cpp' or 'java'.
|
|
* @param opt_numberLines {number|boolean} True to number lines,
|
|
* or the 1-indexed number of the first line in sourceCodeHtml.
|
|
*/
|
|
function $prettyPrintOne(sourceCodeHtml, opt_langExtension, opt_numberLines) {
|
|
var container = document.createElement('div');
|
|
// This could cause images to load and onload listeners to fire.
|
|
// E.g. <img onerror="alert(1337)" src="nosuchimage.png">.
|
|
// We assume that the inner HTML is from a trusted source.
|
|
// The pre-tag is required for IE8 which strips newlines from innerHTML
|
|
// when it is injected into a <pre> tag.
|
|
// http://stackoverflow.com/questions/451486/pre-tag-loses-line-breaks-when-setting-innerhtml-in-ie
|
|
// http://stackoverflow.com/questions/195363/inserting-a-newline-into-a-pre-tag-ie-javascript
|
|
container.innerHTML = '<pre>' + sourceCodeHtml + '</pre>';
|
|
container = container.firstChild;
|
|
if (opt_numberLines) {
|
|
numberLines(container, opt_numberLines, true);
|
|
}
|
|
|
|
var job = {
|
|
langExtension: opt_langExtension,
|
|
numberLines: opt_numberLines,
|
|
sourceNode: container,
|
|
pre: 1
|
|
};
|
|
applyDecorator(job);
|
|
return container.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Find all the {@code <pre>} and {@code <code>} tags in the DOM with
|
|
* {@code class=prettyprint} and prettify them.
|
|
*
|
|
* @param {Function} opt_whenDone called when prettifying is done.
|
|
* @param {HTMLElement|HTMLDocument} opt_root an element or document
|
|
* containing all the elements to pretty print.
|
|
* Defaults to {@code document.body}.
|
|
*/
|
|
function $prettyPrint(opt_whenDone, opt_root) {
|
|
var root = opt_root || document.body;
|
|
var doc = root.ownerDocument || document;
|
|
function byTagName(tn) { return root.getElementsByTagName(tn); }
|
|
// fetch a list of nodes to rewrite
|
|
var codeSegments = [byTagName('pre'), byTagName('code'), byTagName('xmp')];
|
|
var elements = [];
|
|
for (var i = 0; i < codeSegments.length; ++i) {
|
|
for (var j = 0, n = codeSegments[i].length; j < n; ++j) {
|
|
elements.push(codeSegments[i][j]);
|
|
}
|
|
}
|
|
codeSegments = null;
|
|
|
|
var clock = Date;
|
|
if (!clock['now']) {
|
|
clock = { 'now': function () { return +(new Date); } };
|
|
}
|
|
|
|
// The loop is broken into a series of continuations to make sure that we
|
|
// don't make the browser unresponsive when rewriting a large page.
|
|
var k = 0;
|
|
var prettyPrintingJob;
|
|
|
|
var langExtensionRe = /\blang(?:uage)?-([\w.]+)(?!\S)/;
|
|
var prettyPrintRe = /\bprettyprint\b/;
|
|
var prettyPrintedRe = /\bprettyprinted\b/;
|
|
var preformattedTagNameRe = /pre|xmp/i;
|
|
var codeRe = /^code$/i;
|
|
var preCodeXmpRe = /^(?:pre|code|xmp)$/i;
|
|
var EMPTY = {};
|
|
|
|
function doWork() {
|
|
var endTime = (win['PR_SHOULD_USE_CONTINUATION'] ?
|
|
clock['now']() + 250 /* ms */ :
|
|
Infinity);
|
|
for (; k < elements.length && clock['now']() < endTime; k++) {
|
|
var cs = elements[k];
|
|
|
|
// Look for a preceding comment like
|
|
// <?prettify lang="..." linenums="..."?>
|
|
var attrs = EMPTY;
|
|
{
|
|
for (var preceder = cs; (preceder = preceder.previousSibling);) {
|
|
var nt = preceder.nodeType;
|
|
// <?foo?> is parsed by HTML 5 to a comment node (8)
|
|
// like <!--?foo?-->, but in XML is a processing instruction
|
|
var value = (nt === 7 || nt === 8) && preceder.nodeValue;
|
|
if (value
|
|
? !/^\??prettify\b/.test(value)
|
|
: (nt !== 3 || /\S/.test(preceder.nodeValue))) {
|
|
// Skip over white-space text nodes but not others.
|
|
break;
|
|
}
|
|
if (value) {
|
|
attrs = {};
|
|
value.replace(
|
|
/\b(\w+)=([\w:.%+-]+)/g,
|
|
function (_, name, value) { attrs[name] = value; });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var className = cs.className;
|
|
if ((attrs !== EMPTY || prettyPrintRe.test(className))
|
|
// Don't redo this if we've already done it.
|
|
// This allows recalling pretty print to just prettyprint elements
|
|
// that have been added to the page since last call.
|
|
&& !prettyPrintedRe.test(className)) {
|
|
|
|
// make sure this is not nested in an already prettified element
|
|
var nested = false;
|
|
for (var p = cs.parentNode; p; p = p.parentNode) {
|
|
var tn = p.tagName;
|
|
if (preCodeXmpRe.test(tn)
|
|
&& p.className && prettyPrintRe.test(p.className)) {
|
|
nested = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!nested) {
|
|
// Mark done. If we fail to prettyprint for whatever reason,
|
|
// we shouldn't try again.
|
|
cs.className += ' prettyprinted';
|
|
|
|
// If the classes includes a language extensions, use it.
|
|
// Language extensions can be specified like
|
|
// <pre class="prettyprint lang-cpp">
|
|
// the language extension "cpp" is used to find a language handler
|
|
// as passed to PR.registerLangHandler.
|
|
// HTML5 recommends that a language be specified using "language-"
|
|
// as the prefix instead. Google Code Prettify supports both.
|
|
// http://dev.w3.org/html5/spec-author-view/the-code-element.html
|
|
var langExtension = attrs['lang'];
|
|
if (!langExtension) {
|
|
langExtension = className.match(langExtensionRe);
|
|
// Support <pre class="prettyprint"><code class="language-c">
|
|
var wrapper;
|
|
if (!langExtension && (wrapper = childContentWrapper(cs))
|
|
&& codeRe.test(wrapper.tagName)) {
|
|
langExtension = wrapper.className.match(langExtensionRe);
|
|
}
|
|
|
|
if (langExtension) { langExtension = langExtension[1]; }
|
|
}
|
|
|
|
var preformatted;
|
|
if (preformattedTagNameRe.test(cs.tagName)) {
|
|
preformatted = 1;
|
|
} else {
|
|
var currentStyle = cs['currentStyle'];
|
|
var defaultView = doc.defaultView;
|
|
var whitespace = (
|
|
currentStyle
|
|
? currentStyle['whiteSpace']
|
|
: (defaultView
|
|
&& defaultView.getComputedStyle)
|
|
? defaultView.getComputedStyle(cs, null)
|
|
.getPropertyValue('white-space')
|
|
: 0);
|
|
preformatted = whitespace
|
|
&& 'pre' === whitespace.substring(0, 3);
|
|
}
|
|
|
|
// Look for a class like linenums or linenums:<n> where <n> is the
|
|
// 1-indexed number of the first line.
|
|
var lineNums = attrs['linenums'];
|
|
if (!(lineNums = lineNums === 'true' || +lineNums)) {
|
|
lineNums = className.match(/\blinenums\b(?::(\d+))?/);
|
|
lineNums =
|
|
lineNums
|
|
? lineNums[1] && lineNums[1].length
|
|
? +lineNums[1] : true
|
|
: false;
|
|
}
|
|
if (lineNums) { numberLines(cs, lineNums, preformatted); }
|
|
|
|
// do the pretty printing
|
|
prettyPrintingJob = {
|
|
langExtension: langExtension,
|
|
sourceNode: cs,
|
|
numberLines: lineNums,
|
|
pre: preformatted
|
|
};
|
|
applyDecorator(prettyPrintingJob);
|
|
}
|
|
}
|
|
}
|
|
if (k < elements.length) {
|
|
// finish up in a continuation
|
|
setTimeout(doWork, 250);
|
|
} else if ('function' === typeof opt_whenDone) {
|
|
opt_whenDone();
|
|
}
|
|
}
|
|
|
|
doWork();
|
|
}
|
|
|
|
/**
|
|
* Contains functions for creating and registering new language handlers.
|
|
* @type {Object}
|
|
*/
|
|
var PR = win['PR'] = {
|
|
'createSimpleLexer': createSimpleLexer,
|
|
'registerLangHandler': registerLangHandler,
|
|
'sourceDecorator': sourceDecorator,
|
|
'PR_ATTRIB_NAME': PR_ATTRIB_NAME,
|
|
'PR_ATTRIB_VALUE': PR_ATTRIB_VALUE,
|
|
'PR_COMMENT': PR_COMMENT,
|
|
'PR_DECLARATION': PR_DECLARATION,
|
|
'PR_KEYWORD': PR_KEYWORD,
|
|
'PR_LITERAL': PR_LITERAL,
|
|
'PR_NOCODE': PR_NOCODE,
|
|
'PR_PLAIN': PR_PLAIN,
|
|
'PR_PUNCTUATION': PR_PUNCTUATION,
|
|
'PR_SOURCE': PR_SOURCE,
|
|
'PR_STRING': PR_STRING,
|
|
'PR_TAG': PR_TAG,
|
|
'PR_TYPE': PR_TYPE,
|
|
'prettyPrintOne':
|
|
IN_GLOBAL_SCOPE
|
|
? (win['prettyPrintOne'] = $prettyPrintOne)
|
|
: (prettyPrintOne = $prettyPrintOne),
|
|
'prettyPrint': prettyPrint =
|
|
IN_GLOBAL_SCOPE
|
|
? (win['prettyPrint'] = $prettyPrint)
|
|
: (prettyPrint = $prettyPrint)
|
|
};
|
|
|
|
// Make PR available via the Asynchronous Module Definition (AMD) API.
|
|
// Per https://github.com/amdjs/amdjs-api/wiki/AMD:
|
|
// The Asynchronous Module Definition (AMD) API specifies a
|
|
// mechanism for defining modules such that the module and its
|
|
// dependencies can be asynchronously loaded.
|
|
// ...
|
|
// To allow a clear indicator that a global define function (as
|
|
// needed for script src browser loading) conforms to the AMD API,
|
|
// any global define function SHOULD have a property called "amd"
|
|
// whose value is an object. This helps avoid conflict with any
|
|
// other existing JavaScript code that could have defined a define()
|
|
// function that does not conform to the AMD API.
|
|
if (typeof define === "function" && define['amd']) {
|
|
define("google-code-prettify", [], function () {
|
|
return PR;
|
|
});
|
|
}
|
|
})();
|
|
|
|
define("prettify", function(){});
|
|
|
|
define('itemView',[
|
|
'App',
|
|
// Templates
|
|
'text!tpl/item.html',
|
|
'text!tpl/class.html',
|
|
'text!tpl/itemEnd.html',
|
|
// Tools
|
|
'prettify'
|
|
], function(App, itemTpl, classTpl, endTpl) {
|
|
'use strict';
|
|
|
|
var appVersion = App.project.version || 'master';
|
|
|
|
var itemView = Backbone.View.extend({
|
|
el: '#item',
|
|
init: function() {
|
|
this.$html = $('html');
|
|
this.$body = $('body');
|
|
this.$scrollBody = $('html, body'); // hack for Chrome/Firefox scroll
|
|
|
|
this.tpl = _.template(itemTpl);
|
|
this.classTpl = _.template(classTpl);
|
|
this.endTpl = _.template(endTpl);
|
|
|
|
return this;
|
|
},
|
|
getSyntax: function(isMethod, cleanItem) {
|
|
var isConstructor = cleanItem.is_constructor;
|
|
var syntax = '';
|
|
if (isConstructor) {
|
|
syntax += 'new ';
|
|
} else if (cleanItem.static && cleanItem.class) {
|
|
syntax += cleanItem.class + '.';
|
|
}
|
|
syntax += cleanItem.name;
|
|
|
|
if (isMethod || isConstructor) {
|
|
syntax += '(';
|
|
if (cleanItem.params) {
|
|
for (var i = 0; i < cleanItem.params.length; i++) {
|
|
var p = cleanItem.params[i];
|
|
if (p.optional) {
|
|
syntax += '[';
|
|
}
|
|
syntax += p.name;
|
|
if (p.optdefault) {
|
|
syntax += '=' + p.optdefault;
|
|
}
|
|
if (p.optional) {
|
|
syntax += ']';
|
|
}
|
|
if (i !== cleanItem.params.length - 1) {
|
|
syntax += ', ';
|
|
}
|
|
}
|
|
}
|
|
syntax += ')';
|
|
}
|
|
|
|
return syntax;
|
|
},
|
|
// Return a list of valid syntaxes across all overloaded versions of
|
|
// this item.
|
|
//
|
|
// For reference, we ultimately want to replicate something like this:
|
|
//
|
|
// https://processing.org/reference/color_.html
|
|
getSyntaxes: function(isMethod, cleanItem) {
|
|
var overloads = cleanItem.overloads || [cleanItem];
|
|
return overloads.map(this.getSyntax.bind(this, isMethod));
|
|
},
|
|
render: function(item) {
|
|
if (item) {
|
|
var itemHtml = '';
|
|
var cleanItem = this.clean(item);
|
|
var isClass = item.hasOwnProperty('itemtype') ? 0 : 1;
|
|
var collectionName = isClass
|
|
? 'Constructor'
|
|
: this.capitalizeFirst(cleanItem.itemtype),
|
|
isConstructor = cleanItem.is_constructor;
|
|
cleanItem.isMethod = collectionName === 'Method';
|
|
|
|
var syntaxes = this.getSyntaxes(cleanItem.isMethod, cleanItem);
|
|
|
|
// Set the item header (title)
|
|
|
|
// Set item contents
|
|
if (isClass) {
|
|
var constructor = this.tpl({
|
|
item: cleanItem,
|
|
isClass: true,
|
|
isConstructor: isConstructor,
|
|
syntaxes: syntaxes
|
|
});
|
|
cleanItem.constructor = constructor;
|
|
|
|
var contents = _.find(App.classes, function(c) {
|
|
return c.name === cleanItem.name;
|
|
});
|
|
cleanItem.things = contents.items;
|
|
|
|
itemHtml = this.classTpl(cleanItem);
|
|
} else {
|
|
cleanItem.constRefs =
|
|
item.module === 'Constants' && App.data.consts[item.name];
|
|
|
|
itemHtml = this.tpl({
|
|
item: cleanItem,
|
|
isClass: false,
|
|
isConstructor: false,
|
|
syntaxes: syntaxes
|
|
});
|
|
}
|
|
|
|
itemHtml += this.endTpl({ item: cleanItem, appVersion: appVersion });
|
|
|
|
// Insert the view in the dom
|
|
this.$el.html(itemHtml);
|
|
|
|
renderCode(cleanItem.name);
|
|
|
|
// Set the document title based on the item name.
|
|
// If it is a method, add parentheses to the name
|
|
if (item.itemtype === 'method') {
|
|
App.pageView.appendToDocumentTitle(item.name + '()');
|
|
} else {
|
|
App.pageView.appendToDocumentTitle(item.name);
|
|
}
|
|
|
|
// Hook up alt-text for examples
|
|
setTimeout(function() {
|
|
var alts = $('.example-content')[0];
|
|
if (alts) {
|
|
alts = $(alts)
|
|
.data('alt')
|
|
.split('\n');
|
|
|
|
var canvases = $('.cnv_div');
|
|
for (var j = 0; j < alts.length; j++) {
|
|
if (j < canvases.length) {
|
|
$(canvases[j]).append(
|
|
'<span class="sr-only">' + alts[j] + '</span>'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}, 1000);
|
|
Prism.highlightAll();
|
|
}
|
|
|
|
var renderEvent = new Event('reference-rendered');
|
|
window.dispatchEvent(renderEvent);
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Clean item properties: url encode properties containing paths.
|
|
* @param {object} item The item object.
|
|
* @returns {object} Returns the same item object with urlencoded paths.
|
|
*/
|
|
clean: function(item) {
|
|
var cleanItem = item;
|
|
|
|
if (cleanItem.hasOwnProperty('file')) {
|
|
cleanItem.urlencodedfile = encodeURIComponent(item.file);
|
|
}
|
|
return cleanItem;
|
|
},
|
|
/**
|
|
* Show a single item.
|
|
* @param {object} item Item object.
|
|
* @returns {object} This view.
|
|
*/
|
|
show: function(item) {
|
|
if (item) {
|
|
this.render(item);
|
|
}
|
|
|
|
App.pageView.hideContentViews();
|
|
|
|
this.$el.show();
|
|
|
|
this.scrollTop();
|
|
$('#item').focus();
|
|
return this;
|
|
},
|
|
/**
|
|
* Show a message if no item is found.
|
|
* @returns {object} This view.
|
|
*/
|
|
nothingFound: function() {
|
|
this.$el.html(
|
|
'<p><br><br>Ouch. I am unable to find any item that match the current query.</p>'
|
|
);
|
|
App.pageView.hideContentViews();
|
|
this.$el.show();
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Scroll to the top of the window with an animation.
|
|
*/
|
|
scrollTop: function() {
|
|
// Hack for Chrome/Firefox scroll animation
|
|
// Chrome scrolls 'body', Firefox scrolls 'html'
|
|
var scroll = this.$body.scrollTop() > 0 || this.$html.scrollTop() > 0;
|
|
if (scroll) {
|
|
this.$scrollBody.animate({ scrollTop: 0 }, 600);
|
|
}
|
|
},
|
|
/**
|
|
* Helper method to capitalize the first letter of a string
|
|
* @param {string} str
|
|
* @returns {string} Returns the string.
|
|
*/
|
|
capitalizeFirst: function(str) {
|
|
return str.substr(0, 1).toUpperCase() + str.substr(1);
|
|
}
|
|
});
|
|
|
|
return itemView;
|
|
});
|
|
|
|
|
|
define('text!tpl/menu.html',[],function () { return '<div>\n <br>\n <span id="reference-description1">Can\'t find what you\'re looking for? You may want to check out</span>\n <a href="#/libraries/p5.sound">p5.sound</a>.<br><a href=\'https://p5js.org/offline-reference/p5-reference.zip\' target=_blank><span id="reference-description3">You can also download an offline version of the reference.</span></a>\n</div>\n\n<div id=\'collection-list-categories\'>\n<h2 class="sr-only" id="categories">Categories</h2>\n<% var i=0; %>\n<% var max=Math.floor(groups.length/4); %>\n<% var rem=groups.length%4; %>\n\n<% _.each(groups, function(group){ %>\n <% var m = rem > 0 ? 1 : 0 %>\n <% if (i === 0) { %>\n <ul aria-labelledby="categories">\n <% } %>\n <li><a href="#group-<%=group%>"><%=group%></a></li>\n <% if (i === (max+m-1)) { %>\n </ul>\n \t<% rem-- %>\n \t<% i=0 %>\n <% } else { %>\n \t<% i++ %>\n <% } %>\n<% }); %>\n</div>\n';});
|
|
|
|
define('menuView',[
|
|
'App',
|
|
'text!tpl/menu.html'
|
|
], function(App, menuTpl) {
|
|
|
|
var menuView = Backbone.View.extend({
|
|
el: '#collection-list-nav',
|
|
/**
|
|
* Init.
|
|
* @returns {object} This view.
|
|
*/
|
|
init: function() {
|
|
this.menuTpl = _.template(menuTpl);
|
|
return this;
|
|
},
|
|
/**
|
|
* Render.
|
|
* @returns {object} This view.
|
|
*/
|
|
render: function() {
|
|
|
|
var groups = [];
|
|
_.each(App.modules, function (item, i) {
|
|
if (!item.is_submodule) {
|
|
if (!item.file || item.file.indexOf('addons') === -1) { //addons don't get displayed on main page
|
|
groups.push(item.name);
|
|
}
|
|
}
|
|
//}
|
|
});
|
|
|
|
// Sort groups by name A-Z
|
|
groups.sort();
|
|
|
|
var menuHtml = this.menuTpl({
|
|
'groups': groups
|
|
});
|
|
|
|
// Render the view
|
|
this.$el.html(menuHtml);
|
|
},
|
|
|
|
hide: function() {
|
|
this.$el.hide();
|
|
},
|
|
|
|
show: function() {
|
|
this.$el.show();
|
|
},
|
|
|
|
/**
|
|
* Update the menu.
|
|
* @param {string} el The name of the current route.
|
|
*/
|
|
update: function(menuItem) {
|
|
//console.log(menuItem);
|
|
// this.$menuItems.removeClass('active');
|
|
// this.$menuItems.find('a[href=#'+menuItem+']').parent().addClass('active');
|
|
|
|
}
|
|
});
|
|
|
|
return menuView;
|
|
|
|
});
|
|
|
|
|
|
define('text!tpl/library.html',[],function () { return '<h3><%= module.name %> library</h3>\n\n<p><%= module.description %></p>\n\n<div id="library-page" class="reference-group clearfix"> \n\n<% var t = 0; col = 0; %>\n\n<% _.each(groups, function(group){ %>\n <% if (t == 0) { %> \n <div class="column_<%=col%>">\n <% } %>\n <% if (group.name !== module.name && group.name !== \'p5\') { %>\n <% if (group.hash) { %> <a href="<%=group.hash%>" <% if (group.module !== module.name) { %>class="core"<% } %>><% } %> \n <h4 class="group-name <% if (t == 0) { %> first<%}%>"><%=group.name%></h4>\n <% if (group.hash) { %> </a><br> <% } %>\n <% } %>\n <% _.each(group.items.filter(function(item) {return item.access !== \'private\'}), function(item) { %>\n <a href="<%=item.hash%>" <% if (item.module !== module.name) { %>class="core"<% } %>><%=item.name%><% if (item.itemtype === \'method\') { %>()<%}%></a><br>\n <% t++; %>\n <% }); %>\n <% if (t >= Math.floor(totalItems/4)) { col++; t = 0; %>\n </div>\n <% } %>\n<% }); %>\n</div>\n';});
|
|
|
|
define(
|
|
'libraryView',[
|
|
'App',
|
|
// Templates
|
|
'text!tpl/library.html'
|
|
],
|
|
function(App, libraryTpl) {
|
|
var libraryView = Backbone.View.extend({
|
|
el: '#list',
|
|
events: {},
|
|
/**
|
|
* Init.
|
|
*/
|
|
init: function() {
|
|
this.libraryTpl = _.template(libraryTpl);
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Render the list.
|
|
*/
|
|
render: function(m, listCollection) {
|
|
if (m && listCollection) {
|
|
var self = this;
|
|
|
|
// Render items and group them by module
|
|
// module === group
|
|
this.groups = {};
|
|
_.each(m.items, function(item, i) {
|
|
var module = item.module || '_';
|
|
var group;
|
|
// Override default group with a selected category
|
|
// TODO: Overwriting with the first category might not be the best choice
|
|
// We might also want to have links for categories
|
|
if (item.category && item.category[0]) {
|
|
group = item.category[0];
|
|
// Populate item.hash
|
|
App.router.getHash(item);
|
|
|
|
// Create a group list without link hash
|
|
if (!self.groups[group]) {
|
|
self.groups[group] = {
|
|
name: group.replace('_', ' '),
|
|
module: module,
|
|
hash: undefined,
|
|
items: []
|
|
};
|
|
}
|
|
} else {
|
|
group = item.class || '_';
|
|
var hash = App.router.getHash(item);
|
|
|
|
var ind = hash.lastIndexOf('/');
|
|
hash = hash.substring(0, ind);
|
|
|
|
// Create a group list
|
|
if (!self.groups[group]) {
|
|
self.groups[group] = {
|
|
name: group.replace('_', ' '),
|
|
module: module,
|
|
hash: hash,
|
|
items: []
|
|
};
|
|
}
|
|
}
|
|
|
|
self.groups[group].items.push(item);
|
|
});
|
|
|
|
// Sort groups by name A-Z
|
|
self.groups = _.sortBy(self.groups, this.sortByName);
|
|
|
|
// Put the <li> items html into the list <ul>
|
|
var libraryHtml = self.libraryTpl({
|
|
title: self.capitalizeFirst(listCollection),
|
|
module: m.module,
|
|
totalItems: m.items.length,
|
|
groups: self.groups
|
|
});
|
|
|
|
// Render the view
|
|
this.$el.html(libraryHtml);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Show a list of items.
|
|
* @param {array} items Array of item objects.
|
|
* @returns {object} This view.
|
|
*/
|
|
show: function(listGroup) {
|
|
if (App[listGroup]) {
|
|
this.render(App[listGroup], listGroup);
|
|
}
|
|
App.pageView.hideContentViews();
|
|
|
|
this.$el.show();
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Helper method to capitalize the first letter of a string
|
|
* @param {string} str
|
|
* @returns {string} Returns the string.
|
|
*/
|
|
capitalizeFirst: function(str) {
|
|
return str.substr(0, 1).toUpperCase() + str.substr(1);
|
|
},
|
|
/**
|
|
* Sort function (for the Array.prototype.sort() native method): from A to Z.
|
|
* @param {string} a
|
|
* @param {string} b
|
|
* @returns {Array} Returns an array with elements sorted from A to Z.
|
|
*/
|
|
sortAZ: function(a, b) {
|
|
return a.innerHTML.toLowerCase() > b.innerHTML.toLowerCase() ? 1 : -1;
|
|
},
|
|
|
|
sortByName: function(a, b) {
|
|
if (a.name === 'p5') return -1;
|
|
else return 0;
|
|
}
|
|
});
|
|
|
|
return libraryView;
|
|
}
|
|
);
|
|
|
|
define('pageView',[
|
|
'App',
|
|
|
|
// Views
|
|
'searchView',
|
|
'listView',
|
|
'itemView',
|
|
'menuView',
|
|
'libraryView'
|
|
], function(App, searchView, listView, itemView, menuView, libraryView) {
|
|
|
|
// Store the original title parts so we can substitue different endings.
|
|
var _originalDocumentTitle = window.document.title;
|
|
|
|
var pageView = Backbone.View.extend({
|
|
el: 'body',
|
|
/**
|
|
* Init.
|
|
*/
|
|
init: function() {
|
|
App.$container = $('#container');
|
|
App.contentViews = [];
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Render.
|
|
*/
|
|
render: function() {
|
|
|
|
// Menu view
|
|
if (!App.menuView) {
|
|
App.menuView = new menuView();
|
|
App.menuView.init().render();
|
|
}
|
|
|
|
// Item view
|
|
if (!App.itemView) {
|
|
App.itemView = new itemView();
|
|
App.itemView.init().render();
|
|
// Add the item view to the views array
|
|
App.contentViews.push(App.itemView);
|
|
}
|
|
|
|
// List view
|
|
if (!App.listView) {
|
|
App.listView = new listView();
|
|
App.listView.init().render();
|
|
// Add the list view to the views array
|
|
App.contentViews.push(App.listView);
|
|
}
|
|
|
|
// Library view
|
|
if (!App.libraryView) {
|
|
App.libraryView = new libraryView();
|
|
App.libraryView.init().render();
|
|
// Add the list view to the views array
|
|
App.contentViews.push(App.libraryView);
|
|
}
|
|
|
|
// Search
|
|
if (!App.searchView) {
|
|
App.searchView = new searchView();
|
|
App.searchView.init().render();
|
|
}
|
|
return this;
|
|
},
|
|
/**
|
|
* Hide item and list views.
|
|
* @returns {object} This view.
|
|
*/
|
|
hideContentViews: function() {
|
|
_.each(App.contentViews, function(view, i) {
|
|
view.$el.hide();
|
|
});
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* Append the supplied name to the first part of original document title.
|
|
* If no name is supplied, the title will reset to the original one.
|
|
*/
|
|
appendToDocumentTitle: function(name){
|
|
if(name){
|
|
let firstTitlePart = _originalDocumentTitle.split(" | ")[0];
|
|
window.document.title = [firstTitlePart, name].join(" | ");
|
|
} else {
|
|
window.document.title = _originalDocumentTitle;
|
|
}
|
|
}
|
|
});
|
|
|
|
return pageView;
|
|
|
|
});
|
|
|
|
define('router',[
|
|
'App'
|
|
], function(App) {
|
|
|
|
'use strict'; //
|
|
|
|
var Router = Backbone.Router.extend({
|
|
|
|
routes: {
|
|
'': 'list',
|
|
'p5': 'list',
|
|
'p5/': 'list',
|
|
'classes': 'list',
|
|
'search': 'search',
|
|
'libraries/:lib': 'library',
|
|
':searchClass(/:searchItem)': 'get'
|
|
},
|
|
/**
|
|
* Whether the json API data was loaded.
|
|
*/
|
|
_initialized: false,
|
|
/**
|
|
* Initialize the app: load json API data and create searchable arrays.
|
|
*/
|
|
init: function(callback) {
|
|
var self = this;
|
|
require(['pageView'], function(pageView) {
|
|
|
|
// If already initialized, move away from here!
|
|
if (self._initialized) {
|
|
if (callback)
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
// Update initialization state: must be done now to avoid recursive mess
|
|
self._initialized = true;
|
|
|
|
// Render views
|
|
if (!App.pageView) {
|
|
App.pageView = new pageView();
|
|
App.pageView.init().render();
|
|
}
|
|
|
|
// If a callback is set (a route has already been called), run it
|
|
// otherwise, show the default list
|
|
if (callback)
|
|
callback();
|
|
else
|
|
self.list();
|
|
});
|
|
},
|
|
/**
|
|
* Start route. Simply check if initialized.
|
|
*/
|
|
start: function() {
|
|
this.init();
|
|
},
|
|
/**
|
|
* Show item details by searching a class or a class item (method, property or event).
|
|
* @param {string} searchClass The class name (mandatory).
|
|
* @param {string} searchItem The class item name: can be a method, property or event name.
|
|
*/
|
|
get: function(searchClass, searchItem) {
|
|
|
|
// if looking for a library page, redirect
|
|
if (searchClass === 'p5.sound' && !searchItem) {
|
|
window.location.hash = '/libraries/'+searchClass;
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
this.init(function() {
|
|
var item = self.getItem(searchClass, searchItem);
|
|
|
|
App.menuView.hide();
|
|
|
|
if (item) {
|
|
App.itemView.show(item);
|
|
} else {
|
|
//App.itemView.nothingFound();
|
|
|
|
self.list();
|
|
}
|
|
|
|
styleCodeLinks();
|
|
});
|
|
},
|
|
/**
|
|
* Returns one item object by searching a class or a class item (method, property or event).
|
|
* @param {string} searchClass The class name (mandatory).
|
|
* @param {string} searchItem The class item name: can be a method, property or event name.
|
|
* @returns {object} The item found or undefined if nothing was found.
|
|
*/
|
|
getItem: function(searchClass, searchItem) {
|
|
var classes = App.classes,
|
|
items = App.allItems,
|
|
classesCount = classes.length,
|
|
itemsCount = items.length,
|
|
className = searchClass ? searchClass.toLowerCase() : undefined,
|
|
itemName = searchItem ? searchItem : undefined,
|
|
found;
|
|
|
|
// Only search for a class, if itemName is undefined
|
|
if (className && !itemName) {
|
|
for (var i = 0; i < classesCount; i++) {
|
|
if (classes[i].name.toLowerCase() === className) {
|
|
found = classes[i];
|
|
_.each(found.items, function(i, idx) {
|
|
i.hash = App.router.getHash(i);
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
// Search for a class item
|
|
} else if (className && itemName) {
|
|
// Search case sensitively
|
|
for (var i = 0; i < itemsCount; i++) {
|
|
if (items[i].class.toLowerCase() === className &&
|
|
items[i].name === itemName) {
|
|
found = items[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no match was found, fallback to search case insensitively
|
|
if(!found){
|
|
for (var i = 0; i < itemsCount; i++) {
|
|
if(items[i].class.toLowerCase() === className &&
|
|
items[i].name.toLowerCase() === itemName.toLowerCase()){
|
|
found = items[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return found;
|
|
},
|
|
/**
|
|
* List items.
|
|
* @param {string} collection The name of the collection to list.
|
|
*/
|
|
list: function(collection) {
|
|
|
|
collection = 'allItems';
|
|
|
|
// Make sure collection is valid
|
|
if (App.collections.indexOf(collection) < 0) {
|
|
return;
|
|
}
|
|
|
|
this.init(function() {
|
|
App.menuView.show(collection);
|
|
App.menuView.update(collection);
|
|
App.listView.show(collection);
|
|
styleCodeLinks();
|
|
});
|
|
},
|
|
/**
|
|
* Display information for a library.
|
|
* @param {string} collection The name of the collection to list.
|
|
*/
|
|
library: function(collection) {
|
|
this.init(function() {
|
|
App.menuView.hide();
|
|
App.libraryView.show(collection.substring(3)); //remove p5.
|
|
styleCodeLinks();
|
|
});
|
|
},
|
|
/**
|
|
* Close all content views.
|
|
*/
|
|
search: function() {
|
|
this.init(function() {
|
|
App.menuView.hide();
|
|
App.pageView.hideContentViews();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Create an hash/url for the item.
|
|
* @param {Object} item A class, method, property or event object.
|
|
* @returns {String} The hash string, including the '#'.
|
|
*/
|
|
getHash: function(item) {
|
|
|
|
if (!item.hash) {
|
|
|
|
// FIX TO INVISIBLE OBJECTS: DH (see also listView.js)
|
|
|
|
if (item.class) {
|
|
var clsFunc = '#/' + item.class + '.' + item.name;
|
|
var idx = clsFunc.lastIndexOf('.');
|
|
item.hash = clsFunc.substring(0,idx) + '/' + clsFunc.substring(idx+1);
|
|
} else {
|
|
item.hash = '#/' + item.name;
|
|
}
|
|
}
|
|
|
|
return item.hash;
|
|
}
|
|
});
|
|
|
|
|
|
function styleCodeLinks() {
|
|
var links = document.getElementsByTagName("a");
|
|
for (var iLink = 0; iLink < links.length; iLink++) {
|
|
var link = links[iLink];
|
|
if (link.hash.startsWith('#/p5')) {
|
|
link.classList.add('code');
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Get the router
|
|
App.router = new Router();
|
|
|
|
// Start history
|
|
Backbone.history.start();
|
|
|
|
return App.router;
|
|
|
|
});
|
|
|
|
/**
|
|
* Define global App.
|
|
*/
|
|
var App = window.App || {};
|
|
define('App', [],function() {
|
|
return App;
|
|
});
|
|
|
|
/**
|
|
* Load json API data and start the router.
|
|
* @param {module} App
|
|
* @param {module} router
|
|
*/
|
|
require([
|
|
'App',
|
|
'./documented-method'], function(App, DocumentedMethod) {
|
|
|
|
// Set collections
|
|
App.collections = ['allItems', 'classes', 'events', 'methods', 'properties', 'p5.sound'];
|
|
|
|
// Get json API data
|
|
var data = referenceData;
|
|
App.data = data;
|
|
App.classes = [];
|
|
App.methods = [];
|
|
App.properties = [];
|
|
App.events = [];
|
|
App.allItems = [];
|
|
App.sound = { items: [] };
|
|
App.dom = { items: [] };
|
|
App.modules = [];
|
|
App.project = data.project;
|
|
|
|
|
|
var modules = data.modules;
|
|
|
|
// Get class items (methods, properties, events)
|
|
_.each(modules, function(m, idx, array) {
|
|
App.modules.push(m);
|
|
if (m.name == "p5.sound") {
|
|
App.sound.module = m;
|
|
}
|
|
});
|
|
|
|
|
|
var items = data.classitems;
|
|
var classes = data.classes;
|
|
|
|
// Get classes
|
|
_.each(classes, function(c, idx, array) {
|
|
if (!c.private) {
|
|
App.classes.push(c);
|
|
}
|
|
});
|
|
|
|
|
|
// Get class items (methods, properties, events)
|
|
_.each(items, function(el, idx, array) {
|
|
if (el.itemtype) {
|
|
if (el.itemtype === "method") {
|
|
el = new DocumentedMethod(el);
|
|
App.methods.push(el);
|
|
App.allItems.push(el);
|
|
} else if (el.itemtype === "property") {
|
|
App.properties.push(el);
|
|
App.allItems.push(el);
|
|
} else if (el.itemtype === "event") {
|
|
App.events.push(el);
|
|
App.allItems.push(el);
|
|
}
|
|
|
|
// libraries
|
|
if (el.module === "p5.sound") {
|
|
App.sound.items.push(el);
|
|
}
|
|
}
|
|
});
|
|
|
|
_.each(App.classes, function(c, idx) {
|
|
c.items = _.filter(App.allItems, function(it){ return it.class === c.name; });
|
|
});
|
|
|
|
require(['router']);
|
|
});
|
|
|
|
define("main", function(){});
|
|
|
|
}()); |