Skip to content

Instantly share code, notes, and snippets.

@cvan
Created February 5, 2014 05:18
Show Gist options
  • Save cvan/8817786 to your computer and use it in GitHub Desktop.
Save cvan/8817786 to your computer and use it in GitHub Desktop.
/**
* Copyright (c) 2011-2013 Fabien Cazenave, Mozilla.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
/*jshint browser: true, devel: true, es5: true, globalstrict: true */
document.webL10n = (function(window, document, undefined) {
var gL10nData = {};
var gTextData = '';
var gTextProp = 'textContent';
var gLanguage = '';
var gMacros = {};
var gReadyState = 'loading';
/**
* Synchronously loading l10n resources significantly minimizes flickering
* from displaying the app with non-localized strings and then updating the
* strings. Although this will block all script execution on this page, we
* expect that the l10n resources are available locally on flash-storage.
*
* As synchronous XHR is generally considered as a bad idea, we're still
* loading l10n resources asynchronously -- but we keep this in a setting,
* just in case... and applications using this library should hide their
* content until the `localized' event happens.
*/
var gAsyncResourceLoading = true; // read-only
/**
* Debug helpers
*
* gDEBUG == 0: don't display any console message
* gDEBUG == 1: display only warnings, not logs
* gDEBUG == 2: display all console messages
*/
var gDEBUG = 1;
function consoleLog(message) {
if (gDEBUG >= 2) {
console.log('[l10n] ' + message);
}
};
function consoleWarn(message) {
if (gDEBUG) {
console.warn('[l10n] ' + message);
}
};
/**
* DOM helpers for the so-called "HTML API".
*
* These functions are written for modern browsers. For old versions of IE,
* they're overridden in the 'startup' section at the end of this file.
*/
function getL10nResourceLinks() {
return document.querySelectorAll('link[type="application/l10n"]');
}
function getL10nDictionary() {
var script = document.querySelector('script[type="application/l10n"]');
// TODO: support multiple and external JSON dictionaries
return script ? JSON.parse(script.innerHTML) : null;
}
function getTranslatableChildren(element) {
return element ? element.querySelectorAll('*[data-l10n-id]') : [];
}
function getL10nAttributes(element) {
if (!element)
return {};
var l10nId = element.getAttribute('data-l10n-id');
var l10nArgs = element.getAttribute('data-l10n-args');
var args = {};
if (l10nArgs) {
try {
args = JSON.parse(l10nArgs);
} catch (e) {
consoleWarn('could not parse arguments for #' + l10nId);
}
}
return { id: l10nId, args: args };
}
function fireL10nReadyEvent(lang) {
var evtObject = document.createEvent('Event');
evtObject.initEvent('localized', true, false);
evtObject.language = lang;
document.dispatchEvent(evtObject);
}
function xhrLoadText(url, onSuccess, onFailure, asynchronous) {
onSuccess = onSuccess || function _onSuccess(data) {};
onFailure = onFailure || function _onFailure() {
consoleWarn(url + ' not found.');
};
var xhr = new XMLHttpRequest();
xhr.open('GET', url, asynchronous);
if (xhr.overrideMimeType) {
xhr.overrideMimeType('text/plain; charset=utf-8');
}
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status === 0) {
onSuccess(xhr.responseText);
} else {
onFailure();
}
}
};
xhr.onerror = onFailure;
xhr.ontimeout = onFailure;
// in Firefox OS with the app:// protocol, trying to XHR a non-existing
// URL will raise an exception here -- hence this ugly try...catch.
try {
xhr.send(null);
} catch (e) {
onFailure();
}
}
/**
* l10n resource parser:
* - reads (async XHR) the l10n resource matching `lang';
* - imports linked resources (synchronously) when specified;
* - parses the text data (fills `gL10nData' and `gTextData');
* - triggers success/failure callbacks when done.
*
* @param {string} href
* URL of the l10n resource to parse.
*
* @param {string} lang
* locale (language) to parse.
*
* @param {Function} successCallback
* triggered when the l10n resource has been successully parsed.
*
* @param {Function} failureCallback
* triggered when the an error has occured.
*
* @return {void}
* uses the following global variables: gL10nData, gTextData, gTextProp.
*/
function parseResource(href, lang, successCallback, failureCallback) {
var baseURL = href.replace(/[^\/]*$/, '') || './';
// handle escaped characters (backslashes) in a string
function evalString(text) {
if (text.lastIndexOf('\\') < 0)
return text;
return text.replace(/\\\\/g, '\\')
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t')
.replace(/\\b/g, '\b')
.replace(/\\f/g, '\f')
.replace(/\\{/g, '{')
.replace(/\\}/g, '}')
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
}
// parse *.properties text data into an l10n dictionary
function parseProperties(text) {
var dictionary = [];
// token expressions
var reBlank = /^\s*|\s*$/;
var reComment = /^\s*#|^\s*$/;
var reSection = /^\s*\[(.*)\]\s*$/;
var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;
var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\'
// parse the *.properties file into an associative array
function parseRawLines(rawText, extendedSyntax) {
var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
var currentLang = '*';
var genericLang = lang.replace(/-[a-z]+$/i, '');
var skipLang = false;
var match = '';
for (var i = 0; i < entries.length; i++) {
var line = entries[i];
// comment or blank line?
if (reComment.test(line))
continue;
// the extended syntax supports [lang] sections and @import rules
if (extendedSyntax) {
if (reSection.test(line)) { // section start?
match = reSection.exec(line);
currentLang = match[1];
skipLang = (currentLang !== '*') &&
(currentLang !== lang) && (currentLang !== genericLang);
continue;
} else if (skipLang) {
continue;
}
if (reImport.test(line)) { // @import rule?
match = reImport.exec(line);
loadImport(baseURL + match[1]); // load the resource synchronously
}
}
// key-value pair
var tmp = line.match(reSplit);
if (tmp && tmp.length == 3) {
dictionary[tmp[1]] = evalString(tmp[2]);
}
}
}
// import another *.properties file
function loadImport(url) {
xhrLoadText(url, function(content) {
parseRawLines(content, false); // don't allow recursive imports
}, null, false); // load synchronously
}
// fill the dictionary
parseRawLines(text, true);
return dictionary;
}
// load and parse l10n data (warning: global variables are used here)
xhrLoadText(href, function(response) {
gTextData += response; // mostly for debug
// parse *.properties text data into an l10n dictionary
var data = parseProperties(response);
// find attribute descriptions, if any
for (var key in data) {
var id, prop, index = key.lastIndexOf('.');
if (index > 0) { // an attribute has been specified
id = key.substring(0, index);
prop = key.substr(index + 1);
} else { // no attribute: assuming text content by default
id = key;
prop = gTextProp;
}
if (!gL10nData[id]) {
gL10nData[id] = {};
}
gL10nData[id][prop] = data[key];
}
// trigger callback
if (successCallback) {
successCallback();
}
}, failureCallback, gAsyncResourceLoading);
};
// load and parse all resources for the specified locale
function loadLocale(lang, callback) {
callback = callback || function _callback() {};
clear();
gLanguage = lang;
// check all <link type="application/l10n" href="..." /> nodes
// and load the resource files
var langLinks = getL10nResourceLinks();
var langCount = langLinks.length;
if (langCount == 0) {
// we might have a pre-compiled dictionary instead
var dict = getL10nDictionary();
if (dict && dict.locales && dict.default_locale) {
consoleLog('using the embedded JSON directory, early way out');
gL10nData = dict.locales[lang] || dict.locales[dict.default_locale];
callback();
} else {
consoleLog('no resource to load, early way out');
}
// early way out
fireL10nReadyEvent(lang);
gReadyState = 'complete';
return;
}
// start the callback when all resources are loaded
var onResourceLoaded = null;
var gResourceCount = 0;
onResourceLoaded = function() {
gResourceCount++;
if (gResourceCount >= langCount) {
callback();
fireL10nReadyEvent(lang);
gReadyState = 'complete';
}
};
// load all resource files
function l10nResourceLink(link) {
var href = link.href;
var type = link.type;
this.load = function(lang, callback) {
var applied = lang;
parseResource(href, lang, callback, function() {
consoleWarn(href + ' not found.');
applied = '';
});
return applied; // return lang if found, an empty string if not found
};
}
for (var i = 0; i < langCount; i++) {
var resource = new l10nResourceLink(langLinks[i]);
var rv = resource.load(lang, onResourceLoaded);
if (rv != lang) { // lang not found, used default resource instead
consoleWarn('"' + lang + '" resource not found');
gLanguage = '';
}
}
}
// clear all l10n data
function clear() {
gL10nData = {};
gTextData = '';
gLanguage = '';
// TODO: clear all non predefined macros.
// There's no such macro /yet/ but we're planning to have some...
}
/**
* Get rules for plural forms (shared with JetPack), see:
* http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
* https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
*
* @param {string} lang
* locale (language) used.
*
* @return {Function}
* returns a function that gives the plural form name for a given integer:
* var fun = getPluralRules('en');
* fun(1) -> 'one'
* fun(0) -> 'other'
* fun(1000) -> 'other'.
*/
function getPluralRules(lang) {
var locales2rules = {
'af': 3,
'ak': 4,
'am': 4,
'ar': 1,
'asa': 3,
'az': 0,
'be': 11,
'bem': 3,
'bez': 3,
'bg': 3,
'bh': 4,
'bm': 0,
'bn': 3,
'bo': 0,
'br': 20,
'brx': 3,
'bs': 11,
'ca': 3,
'cgg': 3,
'chr': 3,
'cs': 12,
'cy': 17,
'da': 3,
'de': 3,
'dv': 3,
'dz': 0,
'ee': 3,
'el': 3,
'en': 3,
'eo': 3,
'es': 3,
'et': 3,
'eu': 3,
'fa': 0,
'ff': 5,
'fi': 3,
'fil': 4,
'fo': 3,
'fr': 5,
'fur': 3,
'fy': 3,
'ga': 8,
'gd': 24,
'gl': 3,
'gsw': 3,
'gu': 3,
'guw': 4,
'gv': 23,
'ha': 3,
'haw': 3,
'he': 2,
'hi': 4,
'hr': 11,
'hu': 0,
'id': 0,
'ig': 0,
'ii': 0,
'is': 3,
'it': 3,
'iu': 7,
'ja': 0,
'jmc': 3,
'jv': 0,
'ka': 0,
'kab': 5,
'kaj': 3,
'kcg': 3,
'kde': 0,
'kea': 0,
'kk': 3,
'kl': 3,
'km': 0,
'kn': 0,
'ko': 0,
'ksb': 3,
'ksh': 21,
'ku': 3,
'kw': 7,
'lag': 18,
'lb': 3,
'lg': 3,
'ln': 4,
'lo': 0,
'lt': 10,
'lv': 6,
'mas': 3,
'mg': 4,
'mk': 16,
'ml': 3,
'mn': 3,
'mo': 9,
'mr': 3,
'ms': 0,
'mt': 15,
'my': 0,
'nah': 3,
'naq': 7,
'nb': 3,
'nd': 3,
'ne': 3,
'nl': 3,
'nn': 3,
'no': 3,
'nr': 3,
'nso': 4,
'ny': 3,
'nyn': 3,
'om': 3,
'or': 3,
'pa': 3,
'pap': 3,
'pl': 13,
'ps': 3,
'pt': 3,
'rm': 3,
'ro': 9,
'rof': 3,
'ru': 11,
'rwk': 3,
'sah': 0,
'saq': 3,
'se': 7,
'seh': 3,
'ses': 0,
'sg': 0,
'sh': 11,
'shi': 19,
'sk': 12,
'sl': 14,
'sma': 7,
'smi': 7,
'smj': 7,
'smn': 7,
'sms': 7,
'sn': 3,
'so': 3,
'sq': 3,
'sr': 11,
'ss': 3,
'ssy': 3,
'st': 3,
'sv': 3,
'sw': 3,
'syr': 3,
'ta': 3,
'te': 3,
'teo': 3,
'th': 0,
'ti': 4,
'tig': 3,
'tk': 3,
'tl': 4,
'tn': 3,
'to': 0,
'tr': 0,
'ts': 3,
'tzm': 22,
'uk': 11,
'ur': 3,
've': 3,
'vi': 0,
'vun': 3,
'wa': 4,
'wae': 3,
'wo': 0,
'xh': 3,
'xog': 3,
'yo': 0,
'zh': 0,
'zu': 3
};
// utility functions for plural rules methods
function isIn(n, list) {
return list.indexOf(n) !== -1;
}
function isBetween(n, start, end) {
return start <= n && n <= end;
}
// list of all plural rules methods:
// map an integer to the plural form name to use
var pluralRules = {
'0': function(n) {
return 'other';
},
'1': function(n) {
if ((isBetween((n % 100), 3, 10)))
return 'few';
if (n === 0)
return 'zero';
if ((isBetween((n % 100), 11, 99)))
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'2': function(n) {
if (n !== 0 && (n % 10) === 0)
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'3': function(n) {
if (n == 1)
return 'one';
return 'other';
},
'4': function(n) {
if ((isBetween(n, 0, 1)))
return 'one';
return 'other';
},
'5': function(n) {
if ((isBetween(n, 0, 2)) && n != 2)
return 'one';
return 'other';
},
'6': function(n) {
if (n === 0)
return 'zero';
if ((n % 10) == 1 && (n % 100) != 11)
return 'one';
return 'other';
},
'7': function(n) {
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'8': function(n) {
if ((isBetween(n, 3, 6)))
return 'few';
if ((isBetween(n, 7, 10)))
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'9': function(n) {
if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
return 'few';
if (n == 1)
return 'one';
return 'other';
},
'10': function(n) {
if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
return 'few';
if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
return 'one';
return 'other';
},
'11': function(n) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return 'few';
if ((n % 10) === 0 ||
(isBetween((n % 10), 5, 9)) ||
(isBetween((n % 100), 11, 14)))
return 'many';
if ((n % 10) == 1 && (n % 100) != 11)
return 'one';
return 'other';
},
'12': function(n) {
if ((isBetween(n, 2, 4)))
return 'few';
if (n == 1)
return 'one';
return 'other';
},
'13': function(n) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return 'few';
if (n != 1 && (isBetween((n % 10), 0, 1)) ||
(isBetween((n % 10), 5, 9)) ||
(isBetween((n % 100), 12, 14)))
return 'many';
if (n == 1)
return 'one';
return 'other';
},
'14': function(n) {
if ((isBetween((n % 100), 3, 4)))
return 'few';
if ((n % 100) == 2)
return 'two';
if ((n % 100) == 1)
return 'one';
return 'other';
},
'15': function(n) {
if (n === 0 || (isBetween((n % 100), 2, 10)))
return 'few';
if ((isBetween((n % 100), 11, 19)))
return 'many';
if (n == 1)
return 'one';
return 'other';
},
'16': function(n) {
if ((n % 10) == 1 && n != 11)
return 'one';
return 'other';
},
'17': function(n) {
if (n == 3)
return 'few';
if (n === 0)
return 'zero';
if (n == 6)
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'18': function(n) {
if (n === 0)
return 'zero';
if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
return 'one';
return 'other';
},
'19': function(n) {
if ((isBetween(n, 2, 10)))
return 'few';
if ((isBetween(n, 0, 1)))
return 'one';
return 'other';
},
'20': function(n) {
if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
isBetween((n % 100), 10, 19) ||
isBetween((n % 100), 70, 79) ||
isBetween((n % 100), 90, 99)
))
return 'few';
if ((n % 1000000) === 0 && n !== 0)
return 'many';
if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
return 'two';
if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
return 'one';
return 'other';
},
'21': function(n) {
if (n === 0)
return 'zero';
if (n == 1)
return 'one';
return 'other';
},
'22': function(n) {
if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
return 'one';
return 'other';
},
'23': function(n) {
if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
return 'one';
return 'other';
},
'24': function(n) {
if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
return 'few';
if (isIn(n, [2, 12]))
return 'two';
if (isIn(n, [1, 11]))
return 'one';
return 'other';
}
};
// return a function that gives the plural form name for a given integer
var index = locales2rules[lang.replace(/-.*$/, '')];
if (!(index in pluralRules)) {
consoleWarn('plural form unknown for [' + lang + ']');
return function() { return 'other'; };
}
return pluralRules[index];
}
// pre-defined 'plural' macro
gMacros.plural = function(str, param, key, prop) {
var n = parseFloat(param);
if (isNaN(n))
return str;
// TODO: support other properties (l20n still doesn't...)
if (prop != gTextProp)
return str;
// initialize _pluralRules
if (!gMacros._pluralRules) {
gMacros._pluralRules = getPluralRules(gLanguage);
}
var index = '[' + gMacros._pluralRules(n) + ']';
// try to find a [zero|one|two] key if it's defined
if (n === 0 && (key + '[zero]') in gL10nData) {
str = gL10nData[key + '[zero]'][prop];
} else if (n == 1 && (key + '[one]') in gL10nData) {
str = gL10nData[key + '[one]'][prop];
} else if (n == 2 && (key + '[two]') in gL10nData) {
str = gL10nData[key + '[two]'][prop];
} else if ((key + index) in gL10nData) {
str = gL10nData[key + index][prop];
} else if ((key + '[other]') in gL10nData) {
str = gL10nData[key + '[other]'][prop];
}
return str;
};
/**
* l10n dictionary functions
*/
// fetch an l10n object, warn if not found, apply `args' if possible
function getL10nData(key, args) {
var data = gL10nData[key];
if (!data) {
consoleWarn('#' + key + ' is undefined.');
}
/** This is where l10n expressions should be processed.
* The plan is to support C-style expressions from the l20n project;
* until then, only two kinds of simple expressions are supported:
* {[ index ]} and {{ arguments }}.
*/
var rv = {};
for (var prop in data) {
var str = data[prop];
str = substIndexes(str, args, key, prop);
str = substArguments(str, args, key);
rv[prop] = str;
}
return rv;
}
// replace {[macros]} with their values
function substIndexes(str, args, key, prop) {
var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;
var reMatch = reIndex.exec(str);
if (!reMatch || !reMatch.length)
return str;
// an index/macro has been found
// Note: at the moment, only one parameter is supported
var macroName = reMatch[1];
var paramName = reMatch[2];
var param;
if (args && paramName in args) {
param = args[paramName];
} else if (paramName in gL10nData) {
param = gL10nData[paramName];
}
// there's no macro parser yet: it has to be defined in gMacros
if (macroName in gMacros) {
var macro = gMacros[macroName];
str = macro(str, param, key, prop);
}
return str;
}
// replace {{arguments}} with their values
function substArguments(str, args, key) {
var reArgs = /\{\{\s*(.+?)\s*\}\}/;
var match = reArgs.exec(str);
while (match) {
if (!match || match.length < 2)
return str; // argument key not found
var arg = match[1];
var sub = '';
if (args && arg in args) {
sub = args[arg];
} else if (arg in gL10nData) {
sub = gL10nData[arg][gTextProp];
} else {
consoleLog('argument {{' + arg + '}} for #' + key + ' is undefined.');
return str;
}
str = str.substring(0, match.index) + sub +
str.substr(match.index + match[0].length);
match = reArgs.exec(str);
}
return str;
}
// translate an HTML element
function translateElement(element) {
var l10n = getL10nAttributes(element);
if (!l10n.id)
return;
// get the related l10n object
var data = getL10nData(l10n.id, l10n.args);
if (!data) {
consoleWarn('#' + l10n.id + ' is undefined.');
return;
}
// translate element (TODO: security checks?)
if (data[gTextProp]) { // XXX
if (getChildElementCount(element) === 0) {
element[gTextProp] = data[gTextProp];
} else {
// this element has element children: replace the content of the first
// (non-empty) child textNode and clear other child textNodes
var children = element.childNodes;
var found = false;
for (var i = 0, l = children.length; i < l; i++) {
if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) {
if (found) {
children[i].nodeValue = '';
} else {
children[i].nodeValue = data[gTextProp];
found = true;
}
}
}
// if no (non-empty) textNode is found, insert a textNode before the
// first element child.
if (!found) {
var textNode = document.createTextNode(data[gTextProp]);
element.insertBefore(textNode, element.firstChild);
}
}
delete data[gTextProp];
}
for (var k in data) {
element[k] = data[k];
}
}
// webkit browsers don't currently support 'children' on SVG elements...
function getChildElementCount(element) {
if (element.children) {
return element.children.length;
}
if (typeof element.childElementCount !== 'undefined') {
return element.childElementCount;
}
var count = 0;
for (var i = 0; i < element.childNodes.length; i++) {
count += element.nodeType === 1 ? 1 : 0;
}
return count;
}
// translate an HTML subtree
function translateFragment(element) {
element = element || document.documentElement;
// check all translatable children (= w/ a `data-l10n-id' attribute)
var children = getTranslatableChildren(element);
var elementCount = children.length;
for (var i = 0; i < elementCount; i++) {
translateElement(children[i]);
}
// translate element itself if necessary
translateElement(element);
}
/**
* Startup & Public API
*
* Warning: this part of the code contains browser-specific chunks --
* that's where obsolete browsers, namely IE8 and earlier, are handled.
*
* Unlike the rest of the lib, this section is not shared with FirefoxOS/Gaia.
*/
// load the default locale on startup
function l10nStartup() {
gReadyState = 'interactive';
// most browsers expose the UI language as `navigator.language'
// but IE uses `navigator.userLanguage' instead
var userLocale = navigator.language || navigator.userLanguage;
consoleLog('loading [' + userLocale + '] resources, ' +
(gAsyncResourceLoading ? 'asynchronously.' : 'synchronously.'));
// load the default locale and translate the document if required
if (document.documentElement.lang === userLocale) {
loadLocale(userLocale);
} else {
loadLocale(userLocale, translateFragment);
}
}
// browser-specific startup
if (document.addEventListener) { // modern browsers and IE9+
if (document.readyState === 'loading') {
// the document is not fully loaded yet: wait for DOMContentLoaded.
document.addEventListener('DOMContentLoaded', l10nStartup);
} else {
// l10n.js is being loaded with <script defer> or <script async>,
// the DOM is ready for parsing.
window.setTimeout(l10nStartup);
}
} else if (window.attachEvent) { // IE8 and before (= oldIE)
// TODO: check if jQuery is loaded (CSS selector + JSON + events)
// dummy `console.log' and `console.warn' functions
if (!window.console) {
consoleLog = function(message) {}; // just ignore console.log calls
consoleWarn = function(message) {
if (gDEBUG) {
alert('[l10n] ' + message); // vintage debugging, baby!
}
};
}
// XMLHttpRequest for IE6
if (!window.XMLHttpRequest) {
xhrLoadText = function(url, onSuccess, onFailure, asynchronous) {
onSuccess = onSuccess || function _onSuccess(data) {};
onFailure = onFailure || function _onFailure() {
consoleWarn(url + ' not found.');
};
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
xhr.open('GET', url, asynchronous);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
onSuccess(xhr.responseText);
} else {
onFailure();
}
}
};
xhr.send(null);
}
}
// worst hack ever for IE6 and IE7
if (!window.JSON) {
getL10nAttributes = function(element) {
if (!element)
return {};
var l10nId = element.getAttribute('data-l10n-id'),
l10nArgs = element.getAttribute('data-l10n-args'),
args = {};
if (l10nArgs) try {
args = eval(l10nArgs); // XXX yeah, I know...
} catch (e) {
consoleWarn('could not parse arguments for #' + l10nId);
}
return { id: l10nId, args: args };
};
}
// override `getTranslatableChildren' and `getL10nResourceLinks'
if (!document.querySelectorAll) {
getTranslatableChildren = function(element) {
if (!element)
return [];
var nodes = element.getElementsByTagName('*'),
l10nElements = [],
n = nodes.length;
for (var i = 0; i < n; i++) {
if (nodes[i].getAttribute('data-l10n-id'))
l10nElements.push(nodes[i]);
}
return l10nElements;
};
getL10nResourceLinks = function() {
var links = document.getElementsByTagName('link'),
l10nLinks = [],
n = links.length;
for (var i = 0; i < n; i++) {
if (links[i].type == 'application/l10n')
l10nLinks.push(links[i]);
}
return l10nLinks;
};
}
// override `getL10nDictionary'
if (!window.JSON || !document.querySelectorAll) {
getL10nDictionary = function() {
var scripts = document.getElementsByName('script');
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].type == 'application/l10n') {
return eval(scripts[i].innerHTML);
}
}
return null;
};
}
// fire non-standard `localized' DOM events
if (document.createEventObject && !document.createEvent) {
fireL10nReadyEvent = function(lang) {
// hack to simulate a custom event in IE:
// to catch this event, add an event handler to `onpropertychange'
document.documentElement.localized = 1;
};
}
// startup for IE<9
window.attachEvent('onload', function() {
gTextProp = document.body.textContent ? 'textContent' : 'innerText';
l10nStartup();
});
}
// cross-browser API (sorry, oldIE doesn't support getters & setters)
return {
// get a localized string
get: function(key, args, fallback) {
var data = getL10nData(key, args) || fallback;
if (data) { // XXX double-check this
return gTextProp in data ? data[gTextProp] : '';
}
return '{{' + key + '}}';
},
// debug
getData: function() { return gL10nData; },
getText: function() { return gTextData; },
// get|set the document language
getLanguage: function() { return gLanguage; },
setLanguage: function(lang) { loadLocale(lang, translateFragment); },
// get the direction (ltr|rtl) of the current language
getDirection: function() {
// http://www.w3.org/International/questions/qa-scripts
// Arabic, Hebrew, Farsi, Pashto, Urdu
var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
},
// translate an element or document fragment
translate: translateFragment,
// this can be used to prevent race conditions
getReadyState: function() { return gReadyState; },
ready: function(callback) {
if (!callback) {
return;
} else if (gReadyState == 'complete' || gReadyState == 'interactive') {
window.setTimeout(callback);
} else if (document.addEventListener) {
document.addEventListener('localized', callback);
} else if (document.attachEvent) {
document.documentElement.attachEvent('onpropertychange', function(e) {
if (e.propertyName === 'localized') {
callback();
}
});
}
}
};
}) (window, document);
// gettext-like shortcut for document.webL10n.get
if (window._ === undefined) {
var _ = document.webL10n.get;
}
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["browse.html"] = (function() {function root(env, context, frame, runtime, cb) {
var lineno = null;
var colno = null;
var output = "";
try {
output += "<header></header>\n<ol></ol>\n";
cb(null, output);
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
}
return {
root: root
};
})();
})();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["header.html"] = (function() {function root(env, context, frame, runtime, cb) {
var lineno = null;
var colno = null;
var output = "";
try {
output += "<nav>\n <a href=\"/submit\" class=\"submit\">";
output += runtime.suppressValue((lineno = 1, colno = 37, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "_"), "_", ["Submit a site","navSubmit"])), env.autoesc);
output += "</a>\n <a href=\"/\" class=\"browse\">";
output += runtime.suppressValue((lineno = 2, colno = 31, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "_"), "_", ["Browse sites","navBrowse"])), env.autoesc);
output += "</a>\n <a class=\"button only-signed-out sign-in\">";
output += runtime.suppressValue((lineno = 3, colno = 46, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "_"), "_", ["Sign in","navSignIn"])), env.autoesc);
output += "</a>\n <a class=\"button only-signed-in sign-out\">";
output += runtime.suppressValue((lineno = 4, colno = 46, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "_"), "_", ["Sign out","navSignOut"])), env.autoesc);
output += "</a>\n</nav>\n<form class=\"form-search\" action=\"/\">\n <input type=\"search\" name=\"q\" title=\"\" x-inputmode=\"verbatim\" autocapitalize=\"off\" autocomplete=\"off\" autocorrect=\"off\" placeholder=\"";
output += runtime.suppressValue((lineno = 7, colno = 137, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "_"), "_", ["Search by keyword","searchPlaceholder"])), env.autoesc);
output += "\">\n</form>\n";
cb(null, output);
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
}
return {
root: root
};
})();
})();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["heading.html"] = (function() {function root(env, context, frame, runtime, cb) {
var lineno = null;
var colno = null;
var output = "";
try {
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "data")),"query", env.autoesc)) {
output += "\n <h1>\n ";
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "data")),"results", env.autoesc)),"length", env.autoesc), env.autoesc);
output += " ";
output += runtime.suppressValue((lineno = 2, colno = 36, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "pluralise"), "pluralise", ["result",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "data")),"results", env.autoesc)])), env.autoesc);
output += "\n for <b>";
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "data")),"query", env.autoesc), env.autoesc);
output += "</b>\n ";
var t_1;
t_1 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "data")),"timing", env.autoesc) / 1000;
frame.set("timing", t_1);
if(!frame.parent) {
context.setVariable("timing", t_1);
context.addExport("timing");
}
output += "\n <span class=\"speed\">(took ";
output += runtime.suppressValue(env.getFilter("round").call(context, t_1,6,"floor"), env.autoesc);
output += "s)</span>\n </h1>\n";
;
}
output += "\n";
cb(null, output);
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
}
return {
root: root
};
})();
})();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["results-header.html"] = (function() {function root(env, context, frame, runtime, cb) {
var lineno = null;
var colno = null;
var output = "";
try {
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "data")),"query", env.autoesc)) {
output += "\n <h1>\n ";
output += runtime.suppressValue((lineno = 2, colno = 6, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "_"), "_", ["{n} results","resultsNum",{"n": runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "data")),"results", env.autoesc)),"length", env.autoesc)}])), env.autoesc);
output += "\n ";
var t_1;
t_1 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "data")),"timing", env.autoesc) / 1000;
frame.set("timing", t_1);
if(!frame.parent) {
context.setVariable("timing", t_1);
context.addExport("timing");
}
output += "\n <span class=\"speed\">";
output += runtime.suppressValue((lineno = 4, colno = 26, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "_"), "_", ["(took {time}s)","resultsTime",{"time": env.getFilter("round").call(context, t_1,6,"floor")}])), env.autoesc);
output += "</span>\n </h1>\n";
;
}
output += "\n";
cb(null, output);
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
}
return {
root: root
};
})();
})();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["results.html"] = (function() {function root(env, context, frame, runtime, cb) {
var lineno = null;
var colno = null;
var output = "";
try {
frame = frame.push();
var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "data")),"results", env.autoesc);
if(t_3) {for(var t_1=0; t_1 < t_3.length; t_1++) {
var t_4 = t_3[t_1];
frame.set("data", t_4);
output += "\n <li";
if(runtime.memberLookup((t_4),"score", env.autoesc)) {
output += " title=\"";
output += runtime.suppressValue((lineno = 1, colno = 30, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "_"), "_", ["Score: {score}","resultsScore",{"score": (lineno = 1, colno = 91, runtime.callWrap(runtime.memberLookup((runtime.memberLookup((t_4),"score", env.autoesc)),"toFixed", env.autoesc), "data[\"score\"][\"toFixed\"]", [8]))}])), env.autoesc);
output += "\"";
;
}
output += ">\n ";
var t_5;
t_5 = runtime.memberLookup((t_4),"doc", env.autoesc);
frame.set("doc", t_5);
if(!frame.parent) {
context.setVariable("doc", t_5);
context.addExport("doc");
}
output += "\n <a href=\"";
output += runtime.suppressValue(runtime.memberLookup((t_5),"app_url", env.autoesc), env.autoesc);
output += "\" target=\"_blank\">\n <div class=\"screenshot\" style=\"background-image: url(http://localhost:7000/screenshot?url=";
output += runtime.suppressValue(runtime.memberLookup((t_5),"app_url", env.autoesc), env.autoesc);
output += ")\"></div>\n <span class=\"name\">";
output += runtime.suppressValue(runtime.memberLookup((t_5),"name", env.autoesc), env.autoesc);
output += "</span>\n </a>\n </li>\n";
;
}
}
frame = frame.pop();
output += "\n";
cb(null, output);
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
}
return {
root: root
};
})();
})();
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["submit.html"] = (function() {function root(env, context, frame, runtime, cb) {
var lineno = null;
var colno = null;
var output = "";
try {
output += "<h1>";
output += runtime.suppressValue((lineno = 0, colno = 6, runtime.callWrap(runtime.contextOrFrameLookup(context, frame, "_"), "_", ["Submit a site","navSubmit"])), env.autoesc);
output += "</h1>\n";
cb(null, output);
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
}
return {
root: root
};
})();
})();
// Browser bundle of nunjucks 1.0.0 (slim, only works with precompiled templates)
(function() {
var modules = {};
(function() {
// A simple class system, more documentation to come
function extend(cls, name, props) {
// This does that same thing as Object.create, but with support for IE8
var F = function() {};
F.prototype = cls.prototype;
var prototype = new F();
var fnTest = /xyz/.test(function(){ xyz; }) ? /\bparent\b/ : /.*/;
props = props || {};
for(var k in props) {
var src = props[k];
var parent = prototype[k];
if(typeof parent == "function" &&
typeof src == "function" &&
fnTest.test(src)) {
prototype[k] = (function (src, parent) {
return function() {
// Save the current parent method
var tmp = this.parent;
// Set parent to the previous method, call, and restore
this.parent = parent;
var res = src.apply(this, arguments);
this.parent = tmp;
return res;
};
})(src, parent);
}
else {
prototype[k] = src;
}
}
prototype.typename = name;
var new_cls = function() {
if(prototype.init) {
prototype.init.apply(this, arguments);
}
};
new_cls.prototype = prototype;
new_cls.prototype.constructor = new_cls;
new_cls.extend = function(name, props) {
if(typeof name == "object") {
props = name;
name = "anonymous";
}
return extend(new_cls, name, props);
};
return new_cls;
}
modules['object'] = extend(Object, "Object", {});
})();
(function() {
var ArrayProto = Array.prototype;
var ObjProto = Object.prototype;
var escapeMap = {
'&': '&amp;',
'"': '&quot;',
"'": '&#39;',
"<": '&lt;',
">": '&gt;'
};
var lookupEscape = function(ch) {
return escapeMap[ch];
};
var exports = modules['lib'] = {};
exports.withPrettyErrors = function(path, withInternals, func) {
try {
return func();
} catch (e) {
if (!e.Update) {
// not one of ours, cast it
e = new exports.TemplateError(e);
}
e.Update(path);
// Unless they marked the dev flag, show them a trace from here
if (!withInternals) {
var old = e;
e = new Error(old.message);
e.name = old.name;
}
throw e;
}
};
exports.TemplateError = function(message, lineno, colno) {
var err = this;
if (message instanceof Error) { // for casting regular js errors
err = message;
message = message.name + ": " + message.message;
} else {
if(Error.captureStackTrace) {
Error.captureStackTrace(err);
}
}
err.name = 'Template render error';
err.message = message;
err.lineno = lineno;
err.colno = colno;
err.firstUpdate = true;
err.Update = function(path) {
var message = "(" + (path || "unknown path") + ")";
// only show lineno + colno next to path of template
// where error occurred
if (this.firstUpdate) {
if(this.lineno && this.colno) {
message += ' [Line ' + this.lineno + ', Column ' + this.colno + ']';
}
else if(this.lineno) {
message += ' [Line ' + this.lineno + ']';
}
}
message += '\n ';
if (this.firstUpdate) {
message += ' ';
}
this.message = message + (this.message || '');
this.firstUpdate = false;
return this;
};
return err;
};
exports.TemplateError.prototype = Error.prototype;
exports.escape = function(val) {
return val.replace(/[&"'<>]/g, lookupEscape);
};
exports.isFunction = function(obj) {
return ObjProto.toString.call(obj) == '[object Function]';
};
exports.isArray = Array.isArray || function(obj) {
return ObjProto.toString.call(obj) == '[object Array]';
};
exports.isString = function(obj) {
return ObjProto.toString.call(obj) == '[object String]';
};
exports.isObject = function(obj) {
return obj === Object(obj);
};
exports.groupBy = function(obj, val) {
var result = {};
var iterator = exports.isFunction(val) ? val : function(obj) { return obj[val]; };
for(var i=0; i<obj.length; i++) {
var value = obj[i];
var key = iterator(value, i);
(result[key] || (result[key] = [])).push(value);
}
return result;
};
exports.toArray = function(obj) {
return Array.prototype.slice.call(obj);
};
exports.without = function(array) {
var result = [];
if (!array) {
return result;
}
var index = -1,
length = array.length,
contains = exports.toArray(arguments).slice(1);
while(++index < length) {
if(contains.indexOf(array[index]) === -1) {
result.push(array[index]);
}
}
return result;
};
exports.extend = function(obj, obj2) {
for(var k in obj2) {
obj[k] = obj2[k];
}
return obj;
};
exports.repeat = function(char_, n) {
var str = '';
for(var i=0; i<n; i++) {
str += char_;
}
return str;
};
exports.each = function(obj, func, context) {
if(obj == null) {
return;
}
if(ArrayProto.each && obj.each == ArrayProto.each) {
obj.forEach(func, context);
}
else if(obj.length === +obj.length) {
for(var i=0, l=obj.length; i<l; i++) {
func.call(context, obj[i], i, obj);
}
}
};
exports.map = function(obj, func) {
var results = [];
if(obj == null) {
return results;
}
if(ArrayProto.map && obj.map === ArrayProto.map) {
return obj.map(func);
}
for(var i=0; i<obj.length; i++) {
results[results.length] = func(obj[i], i);
}
if(obj.length === +obj.length) {
results.length = obj.length;
}
return results;
};
exports.asyncParallel = function(funcs, done) {
var count = funcs.length,
result = new Array(count),
current = 0;
var makeNext = function(i) {
return function(res) {
result[i] = res;
current += 1;
if (current === count) {
done(result);
}
};
};
for (var i = 0; i < count; i++) {
funcs[i](makeNext(i));
}
};
exports.asyncIter = function(arr, iter, cb) {
var i = -1;
function next() {
i++;
if(i < arr.length) {
iter(arr[i], i, next, cb);
}
else {
cb();
}
}
next();
};
exports.asyncFor = function(obj, iter, cb) {
var keys = exports.keys(obj);
var len = keys.length;
var i = -1;
function next() {
i++;
var k = keys[i];
if(i < len) {
iter(k, obj[k], i, len, next);
}
else {
cb();
}
}
next();
};
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function(array, searchElement /*, fromIndex */) {
if (array == null) {
throw new TypeError();
}
var t = Object(array);
var len = t.length >>> 0;
if (len === 0) {
return -1;
}
var n = 0;
if (arguments.length > 2) {
n = Number(arguments[2]);
if (n != n) { // shortcut for verifying if it's NaN
n = 0;
} else if (n != 0 && n != Infinity && n != -Infinity) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
}
if (n >= len) {
return -1;
}
var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
for (; k < len; k++) {
if (k in t && t[k] === searchElement) {
return k;
}
}
return -1;
};
}
if(!Array.prototype.map) {
Array.prototype.map = function() {
throw new Error("map is unimplemented for this js engine");
};
}
exports.keys = function(obj) {
if(Object.prototype.keys) {
return obj.keys();
}
else {
var keys = [];
for(var k in obj) {
if(obj.hasOwnProperty(k)) {
keys.push(k);
}
}
return keys;
}
}
})();
(function() {
var lib = modules["lib"];
var Object = modules["object"];
// Frames keep track of scoping both at compile-time and run-time so
// we know how to access variables. Block tags can introduce special
// variables, for example.
var Frame = Object.extend({
init: function(parent) {
this.variables = {};
this.parent = parent;
},
set: function(name, val) {
// Allow variables with dots by automatically creating the
// nested structure
var parts = name.split('.');
var obj = this.variables;
for(var i=0; i<parts.length - 1; i++) {
var id = parts[i];
if(!obj[id]) {
obj[id] = {};
}
obj = obj[id];
}
obj[parts[parts.length - 1]] = val;
},
get: function(name) {
var val = this.variables[name];
if(val !== undefined && val !== null) {
return val;
}
return null;
},
lookup: function(name) {
var p = this.parent;
var val = this.variables[name];
if(val !== undefined && val !== null) {
return val;
}
return p && p.lookup(name);
},
push: function() {
return new Frame(this);
},
pop: function() {
return this.parent;
}
});
function makeMacro(argNames, kwargNames, func) {
return function() {
var argCount = numArgs(arguments);
var args;
var kwargs = getKeywordArgs(arguments);
if(argCount > argNames.length) {
args = Array.prototype.slice.call(arguments, 0, argNames.length);
// Positional arguments that should be passed in as
// keyword arguments (essentially default values)
var vals = Array.prototype.slice.call(arguments, args.length, argCount);
for(var i=0; i<vals.length; i++) {
if(i < kwargNames.length) {
kwargs[kwargNames[i]] = vals[i];
}
}
args.push(kwargs);
}
else if(argCount < argNames.length) {
args = Array.prototype.slice.call(arguments, 0, argCount);
for(var i=argCount; i<argNames.length; i++) {
var arg = argNames[i];
// Keyword arguments that should be passed as
// positional arguments, i.e. the caller explicitly
// used the name of a positional arg
args.push(kwargs[arg]);
delete kwargs[arg];
}
args.push(kwargs);
}
else {
args = arguments;
}
return func.apply(this, args);
};
}
function makeKeywordArgs(obj) {
obj.__keywords = true;
return obj;
}
function getKeywordArgs(args) {
var len = args.length;
if(len) {
var lastArg = args[len - 1];
if(lastArg && lastArg.hasOwnProperty('__keywords')) {
return lastArg;
}
}
return {};
}
function numArgs(args) {
var len = args.length;
if(len === 0) {
return 0;
}
var lastArg = args[len - 1];
if(lastArg && lastArg.hasOwnProperty('__keywords')) {
return len - 1;
}
else {
return len;
}
}
// A SafeString object indicates that the string should not be
// autoescaped. This happens magically because autoescaping only
// occurs on primitive string objects.
function SafeString(val) {
if(typeof val != 'string') {
return val;
}
this.toString = function() {
return val;
};
this.length = val.length;
var methods = [
'charAt', 'charCodeAt', 'concat', 'contains',
'endsWith', 'fromCharCode', 'indexOf', 'lastIndexOf',
'length', 'localeCompare', 'match', 'quote', 'replace',
'search', 'slice', 'split', 'startsWith', 'substr',
'substring', 'toLocaleLowerCase', 'toLocaleUpperCase',
'toLowerCase', 'toUpperCase', 'trim', 'trimLeft', 'trimRight'
];
for(var i=0; i<methods.length; i++) {
this[methods[i]] = markSafe(val[methods[i]]);
}
}
function copySafeness(dest, target) {
if(dest instanceof SafeString) {
return new SafeString(target);
}
return target.toString();
}
function markSafe(val) {
var type = typeof val;
if(type === 'string') {
return new SafeString(val);
}
else if(type !== 'function') {
return val;
}
else {
return function() {
var ret = val.apply(this, arguments);
if(typeof ret === 'string') {
return new SafeString(ret);
}
return ret;
};
}
}
function suppressValue(val, autoescape) {
val = (val !== undefined && val !== null) ? val : "";
if(autoescape && typeof val === "string") {
val = lib.escape(val);
}
return val;
}
function memberLookup(obj, val) {
obj = obj || {};
if(typeof obj[val] === 'function') {
return function() {
return obj[val].apply(obj, arguments);
};
}
return obj[val];
}
function callWrap(obj, name, args) {
if(!obj) {
throw new Error('Unable to call `' + name + '`, which is undefined or falsey');
}
else if(typeof obj !== 'function') {
throw new Error('Unable to call `' + name + '`, which is not a function');
}
return obj.apply(this, args);
}
function contextOrFrameLookup(context, frame, name) {
var val = frame.lookup(name);
return (val !== undefined && val !== null) ?
val :
context.lookup(name);
}
function handleError(error, lineno, colno) {
if(error.lineno) {
return error;
}
else {
return new lib.TemplateError(error, lineno, colno);
}
}
function asyncEach(arr, dimen, iter, cb) {
if(lib.isArray(arr)) {
var len = arr.length;
lib.asyncIter(arr, function(item, i, next) {
switch(dimen) {
case 1: iter(item, i, len, next); break;
case 2: iter(item[0], item[1], i, len, next); break;
case 3: iter(item[0], item[1], item[2], i, len, next); break;
default:
item.push(i, next);
iter.apply(this, item);
}
}, cb);
}
else {
lib.asyncFor(arr, function(key, val, i, len, next) {
iter(key, val, i, len, next);
}, cb);
}
}
function asyncAll(arr, dimen, func, cb) {
var finished = 0;
var len;
var outputArr;
function done(i, output) {
finished++;
outputArr[i] = output;
if(finished == len) {
cb(null, outputArr.join(''));
}
}
if(lib.isArray(arr)) {
len = arr.length;
outputArr = new Array(len);
if(len == 0) {
cb(null, '');
}
else {
for(var i=0; i<arr.length; i++) {
var item = arr[i];
switch(dimen) {
case 1: func(item, i, len, done); break;
case 2: func(item[0], item[1], i, len, done); break;
case 3: func(item[0], item[1], item[2], i, len, done); break;
default:
item.push(i, done);
func.apply(this, item);
}
}
}
}
else {
var keys = lib.keys(arr);
len = keys.length;
outputArr = new Array(len);
if(len == 0) {
cb(null, '');
}
else {
for(var i=0; i<keys.length; i++) {
var k = keys[i];
func(k, arr[k], i, len, done);
}
}
}
}
modules['runtime'] = {
Frame: Frame,
makeMacro: makeMacro,
makeKeywordArgs: makeKeywordArgs,
numArgs: numArgs,
suppressValue: suppressValue,
memberLookup: memberLookup,
contextOrFrameLookup: contextOrFrameLookup,
callWrap: callWrap,
handleError: handleError,
isArray: lib.isArray,
asyncEach: lib.asyncEach,
keys: lib.keys,
SafeString: SafeString,
copySafeness: copySafeness,
markSafe: markSafe,
asyncEach: asyncEach,
asyncAll: asyncAll
};
})();
(function() {
var Obj = modules["object"];
var lib = modules["lib"];
var Loader = Obj.extend({
on: function(name, func) {
this.listeners = this.listeners || {};
this.listeners[name] = this.listeners[name] || [];
this.listeners[name].push(func);
},
emit: function(name /*, arg1, arg2, ...*/) {
var args = Array.prototype.slice.call(arguments, 1);
if(this.listeners && this.listeners[name]) {
lib.each(this.listeners[name], function(listener) {
listener.apply(null, args);
});
}
}
});
modules['loader'] = Loader;
})();
(function() {
var Loader = modules["loader"];
var WebLoader = Loader.extend({
init: function(baseURL, neverUpdate) {
// It's easy to use precompiled templates: just include them
// before you configure nunjucks and this will automatically
// pick it up and use it
this.precompiled = window.nunjucksPrecompiled || {};
this.baseURL = baseURL || '';
this.neverUpdate = neverUpdate;
},
getSource: function(name) {
if(this.precompiled[name]) {
return {
src: { type: "code",
obj: this.precompiled[name] },
path: name
};
}
else {
var src = this.fetch(this.baseURL + '/' + name);
if(!src) {
return null;
}
return { src: src,
path: name,
noCache: this.neverUpdate };
}
},
fetch: function(url, callback) {
// Only in the browser please
var ajax;
var loading = true;
var src;
if(window.XMLHttpRequest) { // Mozilla, Safari, ...
ajax = new XMLHttpRequest();
}
else if(window.ActiveXObject) { // IE 8 and older
ajax = new ActiveXObject("Microsoft.XMLHTTP");
}
ajax.onreadystatechange = function() {
if(ajax.readyState == 4 && ajax.status == 200 && loading) {
loading = false;
src = ajax.responseText;
}
};
url += (url.indexOf('?') === -1 ? '?' : '&') + 's=' +
(new Date().getTime());
// Synchronous because this API shouldn't be used in
// production (pre-load compiled templates instead)
ajax.open('GET', url, false);
ajax.send();
return src;
}
});
modules['web-loaders'] = {
WebLoader: WebLoader
};
})();
(function() {
if(typeof window === 'undefined') {
modules['loaders'] = modules["node-loaders"];
}
else {
modules['loaders'] = modules["web-loaders"];
}
})();
(function() {
var lib = modules["lib"];
var r = modules["runtime"];
var filters = {
abs: function(n) {
return Math.abs(n);
},
batch: function(arr, linecount, fill_with) {
var res = [];
var tmp = [];
for(var i=0; i<arr.length; i++) {
if(i % linecount === 0 && tmp.length) {
res.push(tmp);
tmp = [];
}
tmp.push(arr[i]);
}
if(tmp.length) {
if(fill_with) {
for(var i=tmp.length; i<linecount; i++) {
tmp.push(fill_with);
}
}
res.push(tmp);
}
return res;
},
capitalize: function(str) {
var ret = str.toLowerCase();
return r.copySafeness(str, ret.charAt(0).toUpperCase() + ret.slice(1));
},
center: function(str, width) {
width = width || 80;
if(str.length >= width) {
return str;
}
var spaces = width - str.length;
var pre = lib.repeat(" ", spaces/2 - spaces % 2);
var post = lib.repeat(" ", spaces/2);
return r.copySafeness(str, pre + str + post);
},
'default': function(val, def) {
return val ? val : def;
},
dictsort: function(val, case_sensitive, by) {
if (!lib.isObject(val)) {
throw new lib.TemplateError("dictsort filter: val must be an object");
}
var array = [];
for (var k in val) {
// deliberately include properties from the object's prototype
array.push([k,val[k]]);
}
var si;
if (by === undefined || by === "key") {
si = 0;
} else if (by === "value") {
si = 1;
} else {
throw new lib.TemplateError(
"dictsort filter: You can only sort by either key or value");
}
array.sort(function(t1, t2) {
var a = t1[si];
var b = t2[si];
if (!case_sensitive) {
if (lib.isString(a)) {
a = a.toUpperCase();
}
if (lib.isString(b)) {
b = b.toUpperCase();
}
}
return a > b ? 1 : (a == b ? 0 : -1);
});
return array;
},
escape: function(str) {
if(typeof str == 'string' ||
str instanceof r.SafeString) {
return lib.escape(str);
}
return str;
},
safe: function(str) {
return r.markSafe(str);
},
first: function(arr) {
return arr[0];
},
groupby: function(arr, attr) {
return lib.groupBy(arr, attr);
},
indent: function(str, width, indentfirst) {
width = width || 4;
var res = '';
var lines = str.split('\n');
var sp = lib.repeat(' ', width);
for(var i=0; i<lines.length; i++) {
if(i == 0 && !indentfirst) {
res += lines[i] + '\n';
}
else {
res += sp + lines[i] + '\n';
}
}
return r.copySafeness(str, res);
},
join: function(arr, del, attr) {
del = del || '';
if(attr) {
arr = lib.map(arr, function(v) {
return v[attr];
});
}
return arr.join(del);
},
last: function(arr) {
return arr[arr.length-1];
},
length: function(arr) {
return arr.length;
},
list: function(val) {
if(lib.isString(val)) {
return val.split('');
}
else if(lib.isObject(val)) {
var keys = [];
if(Object.keys) {
keys = Object.keys(val);
}
else {
for(var k in val) {
keys.push(k);
}
}
return lib.map(keys, function(k) {
return { key: k,
value: val[k] };
});
}
else {
throw new lib.TemplateError("list filter: type not iterable");
}
},
lower: function(str) {
return str.toLowerCase();
},
random: function(arr) {
return arr[Math.floor(Math.random() * arr.length)];
},
replace: function(str, old, new_, maxCount) {
var res = str;
var last = res;
var count = 1;
res = res.replace(old, new_);
while(last != res) {
if(count >= maxCount) {
break;
}
last = res;
res = res.replace(old, new_);
count++;
}
return r.copySafeness(str, res);
},
reverse: function(val) {
var arr;
if(lib.isString(val)) {
arr = filters.list(val);
}
else {
// Copy it
arr = lib.map(val, function(v) { return v; });
}
arr.reverse();
if(lib.isString(val)) {
return r.copySafeness(val, arr.join(''));
}
return arr;
},
round: function(val, precision, method) {
precision = precision || 0;
var factor = Math.pow(10, precision);
var rounder;
if(method == 'ceil') {
rounder = Math.ceil;
}
else if(method == 'floor') {
rounder = Math.floor;
}
else {
rounder = Math.round;
}
return rounder(val * factor) / factor;
},
slice: function(arr, slices, fillWith) {
var sliceLength = Math.floor(arr.length / slices);
var extra = arr.length % slices;
var offset = 0;
var res = [];
for(var i=0; i<slices; i++) {
var start = offset + i * sliceLength;
if(i < extra) {
offset++;
}
var end = offset + (i + 1) * sliceLength;
var slice = arr.slice(start, end);
if(fillWith && i >= extra) {
slice.push(fillWith);
}
res.push(slice);
}
return res;
},
sort: function(arr, reverse, caseSens, attr) {
// Copy it
arr = lib.map(arr, function(v) { return v; });
arr.sort(function(a, b) {
var x, y;
if(attr) {
x = a[attr];
y = b[attr];
}
else {
x = a;
y = b;
}
if(!caseSens && lib.isString(x) && lib.isString(y)) {
x = x.toLowerCase();
y = y.toLowerCase();
}
if(x < y) {
return reverse ? 1 : -1;
}
else if(x > y) {
return reverse ? -1: 1;
}
else {
return 0;
}
});
return arr;
},
string: function(obj) {
return r.copySafeness(obj, obj);
},
title: function(str) {
var words = str.split(' ');
for(var i = 0; i < words.length; i++) {
words[i] = filters.capitalize(words[i]);
}
return r.copySafeness(str, words.join(' '));
},
trim: function(str) {
return r.copySafeness(str, str.replace(/^\s*|\s*$/g, ''));
},
truncate: function(input, length, killwords, end) {
var orig = input;
length = length || 255;
if (input.length <= length)
return input;
if (killwords) {
input = input.substring(0, length);
} else {
var idx = input.lastIndexOf(' ', length);
if(idx === -1) {
idx = length;
}
input = input.substring(0, idx);
}
input += (end !== undefined && end !== null) ? end : '...';
return r.copySafeness(orig, input);
},
upper: function(str) {
return str.toUpperCase();
},
urlencode: function(obj) {
var enc = encodeURIComponent;
if (lib.isString(obj)) {
return enc(obj);
} else {
var parts;
if (lib.isArray(obj)) {
parts = obj.map(function(item) {
return enc(item[0]) + '=' + enc(item[1]);
})
} else {
parts = [];
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
parts.push(enc(k) + '=' + enc(obj[k]));
}
}
}
return parts.join('&');
}
},
wordcount: function(str) {
return str.match(/\w+/g).length;
},
'float': function(val, def) {
var res = parseFloat(val);
return isNaN(res) ? def : res;
},
'int': function(val, def) {
var res = parseInt(val, 10);
return isNaN(res) ? def : res;
}
};
// Aliases
filters.d = filters['default'];
filters.e = filters.escape;
modules['filters'] = filters;
})();
(function() {
function cycler(items) {
var index = -1;
var current = null;
return {
reset: function() {
index = -1;
current = null;
},
next: function() {
index++;
if(index >= items.length) {
index = 0;
}
current = items[index];
return current;
}
};
}
function joiner(sep) {
sep = sep || ',';
var first = true;
return function() {
var val = first ? '' : sep;
first = false;
return val;
};
}
var globals = {
range: function(start, stop, step) {
if(!stop) {
stop = start;
start = 0;
step = 1;
}
else if(!step) {
step = 1;
}
var arr = [];
for(var i=start; i<stop; i+=step) {
arr.push(i);
}
return arr;
},
// lipsum: function(n, html, min, max) {
// },
cycler: function() {
return cycler(Array.prototype.slice.call(arguments));
},
joiner: function(sep) {
return joiner(sep);
}
}
modules['globals'] = globals;
})();
(function() {
var lib = modules["lib"];
var Obj = modules["object"];
var lexer = modules["lexer"];
var compiler = modules["compiler"];
var builtin_filters = modules["filters"];
var builtin_loaders = modules["loaders"];
var runtime = modules["runtime"];
var globals = modules["globals"];
var Frame = runtime.Frame;
var Environment = Obj.extend({
init: function(loaders, opts) {
// The dev flag determines the trace that'll be shown on errors.
// If set to true, returns the full trace from the error point,
// otherwise will return trace starting from Template.render
// (the full trace from within nunjucks may confuse developers using
// the library)
// defaults to false
opts = opts || {};
this.dev = !!opts.dev;
// The autoescape flag sets global autoescaping. If true,
// every string variable will be escaped by default.
// If false, strings can be manually escaped using the `escape` filter.
// defaults to false
this.autoesc = !!opts.autoescape;
if(!loaders) {
// The filesystem loader is only available client-side
if(builtin_loaders.FileSystemLoader) {
this.loaders = [new builtin_loaders.FileSystemLoader('views')];
}
else {
this.loaders = [new builtin_loaders.WebLoader('/views')];
}
}
else {
this.loaders = lib.isArray(loaders) ? loaders : [loaders];
}
this.initCache();
this.filters = {};
this.asyncFilters = [];
this.extensions = {};
this.extensionsList = [];
if(opts.tags) {
lexer.setTags(opts.tags);
}
for(var name in builtin_filters) {
this.addFilter(name, builtin_filters[name]);
}
},
initCache: function() {
// Caching and cache busting
var cache = {};
lib.each(this.loaders, function(loader) {
loader.on('update', function(template) {
cache[template] = null;
});
});
this.cache = cache;
},
addExtension: function(name, extension) {
extension._name = name;
this.extensions[name] = extension;
this.extensionsList.push(extension);
},
getExtension: function(name) {
return this.extensions[name];
},
addFilter: function(name, func, async) {
var wrapped = func;
if(async) {
this.asyncFilters.push(name);
}
this.filters[name] = wrapped;
},
getFilter: function(name) {
if(!this.filters[name]) {
throw new Error('filter not found: ' + name);
}
return this.filters[name];
},
getTemplate: function(name, eagerCompile, cb) {
if(name && name.raw) {
// this fixes autoescape for templates referenced in symbols
name = name.raw;
}
if(lib.isFunction(eagerCompile)) {
cb = eagerCompile;
eagerCompile = false;
}
if(typeof name !== 'string') {
throw new Error('template names must be a string: ' + name);
}
var tmpl = this.cache[name];
if(tmpl) {
if(eagerCompile) {
tmpl.compile();
}
if(cb) {
cb(null, tmpl);
}
else {
return tmpl;
}
} else {
var syncResult;
lib.asyncIter(this.loaders, function(loader, i, next, done) {
function handle(src) {
if(src) {
done(src);
}
else {
next();
}
}
if(loader.async) {
loader.getSource(name, function(err, src) {
if(err) { throw err; }
handle(src);
});
}
else {
handle(loader.getSource(name));
}
}, function(info) {
if(!info) {
var err = new Error('template not found: ' + name);
if(cb) {
cb(err);
}
else {
throw err;
}
}
else {
var tmpl = new Template(info.src, this,
info.path, eagerCompile);
if(!info.noCache) {
this.cache[name] = tmpl;
}
if(cb) {
cb(null, tmpl);
}
else {
syncResult = tmpl;
}
}
}.bind(this));
return syncResult;
}
},
express: function(app) {
var env = this;
function NunjucksView(name, opts) {
this.name = name;
this.path = name;
}
NunjucksView.prototype.render = function(opts, cb) {
env.render(this.name, opts, cb);
};
app.set('view', NunjucksView);
},
render: function(name, ctx, cb) {
if(lib.isFunction(ctx)) {
cb = ctx;
ctx = null;
}
// We support a synchronous API to make it easier to migrate
// existing code to async. This works because if you don't do
// anything async work, the whole thing is actually run
// synchronously.
var syncResult = null;
this.getTemplate(name, function(err, tmpl) {
if(err && cb) {
cb(err);
}
else if(err) {
throw err;
}
else {
tmpl.render(ctx, cb || function(err, res) {
if(err) { throw err; }
syncResult = res;
});
}
});
return syncResult;
},
renderString: function(src, ctx, cb) {
var tmpl = new Template(src, this);
return tmpl.render(ctx, cb);
}
});
var Context = Obj.extend({
init: function(ctx, blocks) {
this.ctx = ctx;
this.blocks = {};
this.exported = [];
for(var name in blocks) {
this.addBlock(name, blocks[name]);
}
},
lookup: function(name) {
// This is one of the most called functions, so optimize for
// the typical case where the name isn't in the globals
if(name in globals && !(name in this.ctx)) {
return globals[name];
}
else {
return this.ctx[name];
}
},
setVariable: function(name, val) {
this.ctx[name] = val;
},
getVariables: function() {
return this.ctx;
},
addBlock: function(name, block) {
this.blocks[name] = this.blocks[name] || [];
this.blocks[name].push(block);
},
getBlock: function(name) {
if(!this.blocks[name]) {
throw new Error('unknown block "' + name + '"');
}
return this.blocks[name][0];
},
getSuper: function(env, name, block, frame, runtime, cb) {
var idx = (this.blocks[name] || []).indexOf(block);
var blk = this.blocks[name][idx + 1];
var context = this;
if(idx == -1 || !blk) {
throw new Error('no super block available for "' + name + '"');
}
blk(env, context, frame, runtime, cb);
},
addExport: function(name) {
this.exported.push(name);
},
getExported: function() {
var exported = {};
for(var i=0; i<this.exported.length; i++) {
var name = this.exported[i];
exported[name] = this.ctx[name];
}
return exported;
}
});
var Template = Obj.extend({
init: function (src, env, path, eagerCompile) {
this.env = env || new Environment();
if(lib.isObject(src)) {
switch(src.type) {
case 'code': this.tmplProps = src.obj; break;
case 'string': this.tmplStr = src.obj; break;
}
}
else if(lib.isString(src)) {
this.tmplStr = src;
}
else {
throw new Error("src must be a string or an object describing " +
"the source");
}
this.path = path;
if(eagerCompile) {
lib.withPrettyErrors(this.path,
this.env.dev,
this._compile.bind(this));
}
else {
this.compiled = false;
}
},
render: function(ctx, frame, cb) {
if (typeof ctx === 'function') {
cb = ctx;
ctx = {};
}
else if (typeof frame === 'function') {
cb = frame;
frame = null;
}
return lib.withPrettyErrors(this.path, this.env.dev, function() {
this.compile();
var context = new Context(ctx || {}, this.blocks);
var syncResult = null;
this.rootRenderFunc(this.env,
context,
frame || new Frame(),
runtime,
cb || function(err, res) {
if(err) { throw err; }
syncResult = res;
});
return syncResult;
}.bind(this));
},
getExported: function(cb) {
this.compile();
// Run the rootRenderFunc to populate the context with exported vars
var context = new Context({}, this.blocks);
this.rootRenderFunc(this.env,
context,
new Frame(),
runtime,
function() {
cb(null, context.getExported());
});
},
compile: function() {
if(!this.compiled) {
this._compile();
}
},
_compile: function() {
var props;
if(this.tmplProps) {
props = this.tmplProps;
}
else {
var source = compiler.compile(this.tmplStr,
this.env.asyncFilters,
this.env.extensionsList,
this.path);
var func = new Function(source);
props = func();
}
this.blocks = this._getBlocks(props);
this.rootRenderFunc = props.root;
this.compiled = true;
},
_getBlocks: function(props) {
var blocks = {};
for(var k in props) {
if(k.slice(0, 2) == 'b_') {
blocks[k.slice(2)] = props[k];
}
}
return blocks;
}
});
// test code
// var src = 'hello {% foo baz | bar %}hi{% endfoo %} end';
// var env = new Environment(new builtin_loaders.FileSystemLoader('tests/templates', true), { dev: true });
// function FooExtension() {
// this.tags = ['foo'];
// this._name = 'FooExtension';
// this.parse = function(parser, nodes) {
// var tok = parser.nextToken();
// var args = parser.parseSignature(null, true);
// parser.advanceAfterBlockEnd(tok.value);
// var body = parser.parseUntilBlocks('endfoo');
// parser.advanceAfterBlockEnd();
// return new nodes.CallExtensionAsync(this, 'run', args, [body]);
// };
// this.run = function(context, baz, body, cb) {
// cb(null, baz + '--' + body());
// };
// }
// env.addExtension('FooExtension', new FooExtension());
// env.addFilter('bar', function(val, cb) {
// cb(null, val + '22222');
// }, true);
// var ctx = {};
// var tmpl = new Template(src, env, null, null, true);
// console.log("OUTPUT ---");
// tmpl.render(ctx, function(err, res) {
// if(err) {
// throw err;
// }
// console.log(res);
// });
modules['environment'] = {
Environment: Environment,
Template: Template
};
})();
var nunjucks;
var lib = modules["lib"];
var env = modules["environment"];
var compiler = modules["compiler"];
var parser = modules["parser"];
var lexer = modules["lexer"];
var runtime = modules["runtime"];
var Loader = modules["loader"];
var loaders = modules["loaders"];
var precompile = modules["precompile"];
nunjucks = {};
nunjucks.Environment = env.Environment;
nunjucks.Template = env.Template;
nunjucks.Loader = env.Loader;
nunjucks.FileSystemLoader = loaders.FileSystemLoader;
nunjucks.WebLoader = loaders.WebLoader;
nunjucks.compiler = compiler;
nunjucks.parser = parser;
nunjucks.lexer = lexer;
nunjucks.runtime = runtime;
// A single instance of an environment, since this is so commonly used
var e;
nunjucks.configure = function(templatesPath, opts) {
opts = opts || {};
if(lib.isObject(templatesPath)) {
opts = templatesPath;
templatesPath = null;
}
var loader = loaders.FileSystemLoader || loaders.WebLoader;
e = new env.Environment(new loader(templatesPath, opts.watch), opts);
if(opts && opts.express) {
e.express(opts.express);
}
return e;
};
nunjucks.render = function(name, ctx, cb) {
if(!e) {
nunjucks.configure();
}
return e.render(name, ctx, cb);
};
nunjucks.renderString = function(src, ctx, cb) {
if(!e) {
nunjucks.configure();
}
return e.renderString(src, ctx, cb);
};
if(precompile) {
nunjucks.precompile = precompile.precompile;
nunjucks.precompileString = precompile.precompileString;
}
nunjucks.require = function(name) { return modules[name]; };
if(typeof define === 'function' && define.amd) {
define(function() { return nunjucks; });
}
else {
window.nunjucks = nunjucks;
}
})();
/*
Copyright 2011 Paul Kinlan
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.
*/
var routes = function() {
var _routes = [];
var me = this;
this.parseRoute = function(path) {
this.parseGroups = function(loc) {
var nameRegexp = new RegExp(":([^/.\\\\]+)", "g");
var newRegexp = "" + loc;
var groups = {};
var matches = null;
var i = 0;
// Find the places to edit.
while(matches = nameRegexp.exec(loc)) {
groups[matches[1]] = i++;
newRegexp = newRegexp.replace(matches[0], "([^/.\\\\]+)");
}
newRegexp += "$"; // Only do a full string match
return { "groups" : groups, "regexp": new RegExp(newRegexp)};
};
return this.parseGroups(path);
};
var matchRoute = function(url, e) {
var route = null;
for(var i = 0; route = _routes[i]; i ++) {
var routeMatch = route.regex.regexp.exec(url);
if(!!routeMatch == false) continue;
var params = {};
for(var g in route.regex.groups) {
var group = route.regex.groups[g];
params[g] = routeMatch[group + 1];
}
var values = {};
if(e && e.target instanceof HTMLFormElement) {
var form = e.target;
var items = form.length;
var item;
for(var j = 0; item = form[j]; j++) {
if(!!item.name) values[item.name] = item.value;
}
}
route.callback({"url": url, "params": params, "values" : values, "e": e});
return true;
}
return false;
};
this.get = function(route, callback) {
_routes.push({regex: this.parseRoute(route), "callback": callback, method: "get"});
};
this.post = function(route, callback) {
_routes.push({regex: this.parseRoute(route), "callback": callback, method: "post"});
};
this.test = function(url) {
matchRoute(url);
};
this.getRoutes = function() {
return _routes;
};
var attach = function() {
var triggered = false;
var cancelHashChange = false;
var cancelPopstate = false;
// Add a new event to HTML5 History
if(!!window.history && !!window.history.pushState) {
var pushStateProxy = history.__proto__.pushState;
history.__proto__.pushState = function(state, title, url) {
pushStateProxy.apply(history, arguments);
//var evt = document.createEvent("PopStateEvent");
//evt.initPopStateEvent("statechange", false, false, state);
var evt = document.createEvent("Event");
evt.initEvent("statechanged",false, false);
evt.state = state;
window.dispatchEvent(evt);
return;
};
}
me.run = function() {
if(!triggered) {
matchRoute(document.location.pathname);
triggered = true;
}
};
// Intercept FORM submissions.
window.addEventListener("submit", function(e) {
if(e.target.method == "post") {
if (matchRoute(e.target.action, e)) {
e.preventDefault();
return false;
}
}
// If we haven't matched a URL let the normal request happen.
return true;
});
window.addEventListener("popstate", function(e) {
if(cancelPopstate) {
cancelPopstate = false;
cancelHashChange = false;
return;
}
matchRoute(document.location.pathname);
// popstate fires before a hash change, don't fire twice.
cancelHashChange = true;
}, false);
window.addEventListener("load", function(e) {
if(!triggered) {
matchRoute(document.location.pathname);
triggered = true;
}
cancelHashChange = true;
cancelPopstate = true;
}, false);
window.addEventListener("hashchange", function(e) {
if(cancelHashChange) {
cancelHashChange = false;
cancelPopstate = false;
return;
}
matchRoute(document.location.pathname);
}, false);
};
attach();
};
(function() {
var define, requireModule, require, requirejs;
(function() {
var registry = {}, seen = {};
define = function(name, deps, callback) {
registry[name] = { deps: deps, callback: callback };
};
requirejs = require = requireModule = function(name) {
requirejs._eak_seen = registry;
if (seen[name]) { return seen[name]; }
seen[name] = {};
if (!registry[name]) {
throw new Error("Could not find module " + name);
}
var mod = registry[name],
deps = mod.deps,
callback = mod.callback,
reified = [],
exports;
for (var i=0, l=deps.length; i<l; i++) {
if (deps[i] === 'exports') {
reified.push(exports = {});
} else {
reified.push(requireModule(resolve(deps[i])));
}
}
var value = callback.apply(this, reified);
return seen[name] = exports || value;
function resolve(child) {
if (child.charAt(0) !== '.') { return child; }
var parts = child.split("/");
var parentBase = name.split("/").slice(0, -1);
for (var i=0, l=parts.length; i<l; i++) {
var part = parts[i];
if (part === '..') { parentBase.pop(); }
else if (part === '.') { continue; }
else { parentBase.push(part); }
}
return parentBase.join("/");
}
};
})();
define("promise/all",
["./utils","exports"],
function(__dependency1__, __exports__) {
/* global toString */
var isArray = __dependency1__.isArray;
var isFunction = __dependency1__.isFunction;
/**
Returns a promise that is fulfilled when all the given promises have been
fulfilled, or rejected if any of them become rejected. The return promise
is fulfilled with an array that gives all the values in the order they were
passed in the `promises` array argument.
Example:
```javascript
var promise1 = RSVP.resolve(1);
var promise2 = RSVP.resolve(2);
var promise3 = RSVP.resolve(3);
var promises = [ promise1, promise2, promise3 ];
RSVP.all(promises).then(function(array){
// The array here would be [ 1, 2, 3 ];
});
```
If any of the `promises` given to `RSVP.all` are rejected, the first promise
that is rejected will be given as an argument to the returned promises's
rejection handler. For example:
Example:
```javascript
var promise1 = RSVP.resolve(1);
var promise2 = RSVP.reject(new Error("2"));
var promise3 = RSVP.reject(new Error("3"));
var promises = [ promise1, promise2, promise3 ];
RSVP.all(promises).then(function(array){
// Code here never runs because there are rejected promises!
}, function(error) {
// error.message === "2"
});
```
@method all
@for RSVP
@param {Array} promises
@param {String} label
@return {Promise} promise that is fulfilled when all `promises` have been
fulfilled, or rejected if any of them become rejected.
*/
function all(promises) {
/*jshint validthis:true */
var Promise = this;
if (!isArray(promises)) {
throw new TypeError('You must pass an array to all.');
}
return new Promise(function(resolve, reject) {
var results = [], remaining = promises.length,
promise;
if (remaining === 0) {
resolve([]);
}
function resolver(index) {
return function(value) {
resolveAll(index, value);
};
}
function resolveAll(index, value) {
results[index] = value;
if (--remaining === 0) {
resolve(results);
}
}
for (var i = 0; i < promises.length; i++) {
promise = promises[i];
if (promise && isFunction(promise.then)) {
promise.then(resolver(i), reject);
} else {
resolveAll(i, promise);
}
}
});
}
__exports__.all = all;
});
define("promise/asap",
["exports"],
function(__exports__) {
var browserGlobal = (typeof window !== 'undefined') ? window : {};
var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
var local = (typeof global !== 'undefined') ? global : this;
// node
function useNextTick() {
return function() {
process.nextTick(flush);
};
}
function useMutationObserver() {
var iterations = 0;
var observer = new BrowserMutationObserver(flush);
var node = document.createTextNode('');
observer.observe(node, { characterData: true });
return function() {
node.data = (iterations = ++iterations % 2);
};
}
function useSetTimeout() {
return function() {
local.setTimeout(flush, 1);
};
}
var queue = [];
function flush() {
for (var i = 0; i < queue.length; i++) {
var tuple = queue[i];
var callback = tuple[0], arg = tuple[1];
callback(arg);
}
queue = [];
}
var scheduleFlush;
// Decide what async method to use to triggering processing of queued callbacks:
if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') {
scheduleFlush = useNextTick();
} else if (BrowserMutationObserver) {
scheduleFlush = useMutationObserver();
} else {
scheduleFlush = useSetTimeout();
}
function asap(callback, arg) {
var length = queue.push([callback, arg]);
if (length === 1) {
// If length is 1, that means that we need to schedule an async flush.
// If additional callbacks are queued before the queue is flushed, they
// will be processed by this flush that we are scheduling.
scheduleFlush();
}
}
__exports__.asap = asap;
});
define("promise/cast",
["exports"],
function(__exports__) {
/**
`RSVP.Promise.cast` returns the same promise if that promise shares a constructor
with the promise being casted.
Example:
```javascript
var promise = RSVP.resolve(1);
var casted = RSVP.Promise.cast(promise);
console.log(promise === casted); // true
```
In the case of a promise whose constructor does not match, it is assimilated.
The resulting promise will fulfill or reject based on the outcome of the
promise being casted.
In the case of a non-promise, a promise which will fulfill with that value is
returned.
Example:
```javascript
var value = 1; // could be a number, boolean, string, undefined...
var casted = RSVP.Promise.cast(value);
console.log(value === casted); // false
console.log(casted instanceof RSVP.Promise) // true
casted.then(function(val) {
val === value // => true
});
```
`RSVP.Promise.cast` is similar to `RSVP.resolve`, but `RSVP.Promise.cast` differs in the
following ways:
* `RSVP.Promise.cast` serves as a memory-efficient way of getting a promise, when you
have something that could either be a promise or a value. RSVP.resolve
will have the same effect but will create a new promise wrapper if the
argument is a promise.
* `RSVP.Promise.cast` is a way of casting incoming thenables or promise subclasses to
promises of the exact class specified, so that the resulting object's `then` is
ensured to have the behavior of the constructor you are calling cast on (i.e., RSVP.Promise).
@method cast
@for RSVP
@param {Object} object to be casted
@return {Promise} promise that is fulfilled when all properties of `promises`
have been fulfilled, or rejected if any of them become rejected.
*/
function cast(object) {
/*jshint validthis:true */
if (object && typeof object === 'object' && object.constructor === this) {
return object;
}
var Promise = this;
return new Promise(function(resolve) {
resolve(object);
});
}
__exports__.cast = cast;
});
define("promise/config",
["exports"],
function(__exports__) {
var config = {
instrument: false
};
function configure(name, value) {
if (arguments.length === 2) {
config[name] = value;
} else {
return config[name];
}
}
__exports__.config = config;
__exports__.configure = configure;
});
define("promise/polyfill",
["./promise","./utils","exports"],
function(__dependency1__, __dependency2__, __exports__) {
var RSVPPromise = __dependency1__.Promise;
var isFunction = __dependency2__.isFunction;
function polyfill() {
var es6PromiseSupport =
"Promise" in window &&
// Some of these methods are missing from
// Firefox/Chrome experimental implementations
"cast" in window.Promise &&
"resolve" in window.Promise &&
"reject" in window.Promise &&
"all" in window.Promise &&
"race" in window.Promise &&
// Older version of the spec had a resolver object
// as the arg rather than a function
(function() {
var resolve;
new window.Promise(function(r) { resolve = r; });
return isFunction(resolve);
}());
if (!es6PromiseSupport) {
window.Promise = RSVPPromise;
}
}
__exports__.polyfill = polyfill;
});
define("promise/promise",
["./config","./utils","./cast","./all","./race","./resolve","./reject","./asap","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __exports__) {
var config = __dependency1__.config;
var configure = __dependency1__.configure;
var objectOrFunction = __dependency2__.objectOrFunction;
var isFunction = __dependency2__.isFunction;
var now = __dependency2__.now;
var cast = __dependency3__.cast;
var all = __dependency4__.all;
var race = __dependency5__.race;
var staticResolve = __dependency6__.resolve;
var staticReject = __dependency7__.reject;
var asap = __dependency8__.asap;
var counter = 0;
config.async = asap; // default async is asap;
function Promise(resolver) {
if (!isFunction(resolver)) {
throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
}
if (!(this instanceof Promise)) {
throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
}
this._subscribers = [];
invokeResolver(resolver, this);
}
function invokeResolver(resolver, promise) {
function resolvePromise(value) {
resolve(promise, value);
}
function rejectPromise(reason) {
reject(promise, reason);
}
try {
resolver(resolvePromise, rejectPromise);
} catch(e) {
rejectPromise(e);
}
}
function invokeCallback(settled, promise, callback, detail) {
var hasCallback = isFunction(callback),
value, error, succeeded, failed;
if (hasCallback) {
try {
value = callback(detail);
succeeded = true;
} catch(e) {
failed = true;
error = e;
}
} else {
value = detail;
succeeded = true;
}
if (handleThenable(promise, value)) {
return;
} else if (hasCallback && succeeded) {
resolve(promise, value);
} else if (failed) {
reject(promise, error);
} else if (settled === FULFILLED) {
resolve(promise, value);
} else if (settled === REJECTED) {
reject(promise, value);
}
}
var PENDING = void 0;
var SEALED = 0;
var FULFILLED = 1;
var REJECTED = 2;
function subscribe(parent, child, onFulfillment, onRejection) {
var subscribers = parent._subscribers;
var length = subscribers.length;
subscribers[length] = child;
subscribers[length + FULFILLED] = onFulfillment;
subscribers[length + REJECTED] = onRejection;
}
function publish(promise, settled) {
var child, callback, subscribers = promise._subscribers, detail = promise._detail;
for (var i = 0; i < subscribers.length; i += 3) {
child = subscribers[i];
callback = subscribers[i + settled];
invokeCallback(settled, child, callback, detail);
}
promise._subscribers = null;
}
Promise.prototype = {
constructor: Promise,
_state: undefined,
_detail: undefined,
_subscribers: undefined,
then: function(onFulfillment, onRejection) {
var promise = this;
var thenPromise = new this.constructor(function() {});
if (this._state) {
var callbacks = arguments;
config.async(function invokePromiseCallback() {
invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail);
});
} else {
subscribe(this, thenPromise, onFulfillment, onRejection);
}
return thenPromise;
},
'catch': function(onRejection) {
return this.then(null, onRejection);
}
};
Promise.all = all;
Promise.cast = cast;
Promise.race = race;
Promise.resolve = staticResolve;
Promise.reject = staticReject;
function handleThenable(promise, value) {
var then = null,
resolved;
try {
if (promise === value) {
throw new TypeError("A promises callback cannot return that same promise.");
}
if (objectOrFunction(value)) {
then = value.then;
if (isFunction(then)) {
then.call(value, function(val) {
if (resolved) { return true; }
resolved = true;
if (value !== val) {
resolve(promise, val);
} else {
fulfill(promise, val);
}
}, function(val) {
if (resolved) { return true; }
resolved = true;
reject(promise, val);
});
return true;
}
}
} catch (error) {
if (resolved) { return true; }
reject(promise, error);
return true;
}
return false;
}
function resolve(promise, value) {
if (promise === value) {
fulfill(promise, value);
} else if (!handleThenable(promise, value)) {
fulfill(promise, value);
}
}
function fulfill(promise, value) {
if (promise._state !== PENDING) { return; }
promise._state = SEALED;
promise._detail = value;
config.async(publishFulfillment, promise);
}
function reject(promise, reason) {
if (promise._state !== PENDING) { return; }
promise._state = SEALED;
promise._detail = reason;
config.async(publishRejection, promise);
}
function publishFulfillment(promise) {
publish(promise, promise._state = FULFILLED);
}
function publishRejection(promise) {
publish(promise, promise._state = REJECTED);
}
__exports__.Promise = Promise;
});
define("promise/race",
["./utils","exports"],
function(__dependency1__, __exports__) {
/* global toString */
var isArray = __dependency1__.isArray;
/**
`RSVP.race` allows you to watch a series of promises and act as soon as the
first promise given to the `promises` argument fulfills or rejects.
Example:
```javascript
var promise1 = new RSVP.Promise(function(resolve, reject){
setTimeout(function(){
resolve("promise 1");
}, 200);
});
var promise2 = new RSVP.Promise(function(resolve, reject){
setTimeout(function(){
resolve("promise 2");
}, 100);
});
RSVP.race([promise1, promise2]).then(function(result){
// result === "promise 2" because it was resolved before promise1
// was resolved.
});
```
`RSVP.race` is deterministic in that only the state of the first completed
promise matters. For example, even if other promises given to the `promises`
array argument are resolved, but the first completed promise has become
rejected before the other promises became fulfilled, the returned promise
will become rejected:
```javascript
var promise1 = new RSVP.Promise(function(resolve, reject){
setTimeout(function(){
resolve("promise 1");
}, 200);
});
var promise2 = new RSVP.Promise(function(resolve, reject){
setTimeout(function(){
reject(new Error("promise 2"));
}, 100);
});
RSVP.race([promise1, promise2]).then(function(result){
// Code here never runs because there are rejected promises!
}, function(reason){
// reason.message === "promise2" because promise 2 became rejected before
// promise 1 became fulfilled
});
```
@method race
@for RSVP
@param {Array} promises array of promises to observe
@param {String} label optional string for describing the promise returned.
Useful for tooling.
@return {Promise} a promise that becomes fulfilled with the value the first
completed promises is resolved with if the first completed promise was
fulfilled, or rejected with the reason that the first completed promise
was rejected with.
*/
function race(promises) {
/*jshint validthis:true */
var Promise = this;
if (!isArray(promises)) {
throw new TypeError('You must pass an array to race.');
}
return new Promise(function(resolve, reject) {
var results = [], promise;
for (var i = 0; i < promises.length; i++) {
promise = promises[i];
if (promise && typeof promise.then === 'function') {
promise.then(resolve, reject);
} else {
resolve(promise);
}
}
});
}
__exports__.race = race;
});
define("promise/reject",
["exports"],
function(__exports__) {
/**
`RSVP.reject` returns a promise that will become rejected with the passed
`reason`. `RSVP.reject` is essentially shorthand for the following:
```javascript
var promise = new RSVP.Promise(function(resolve, reject){
reject(new Error('WHOOPS'));
});
promise.then(function(value){
// Code here doesn't run because the promise is rejected!
}, function(reason){
// reason.message === 'WHOOPS'
});
```
Instead of writing the above, your code now simply becomes the following:
```javascript
var promise = RSVP.reject(new Error('WHOOPS'));
promise.then(function(value){
// Code here doesn't run because the promise is rejected!
}, function(reason){
// reason.message === 'WHOOPS'
});
```
@method reject
@for RSVP
@param {Any} reason value that the returned promise will be rejected with.
@param {String} label optional string for identifying the returned promise.
Useful for tooling.
@return {Promise} a promise that will become rejected with the given
`reason`.
*/
function reject(reason) {
/*jshint validthis:true */
var Promise = this;
return new Promise(function (resolve, reject) {
reject(reason);
});
}
__exports__.reject = reject;
});
define("promise/resolve",
["exports"],
function(__exports__) {
/**
`RSVP.resolve` returns a promise that will become fulfilled with the passed
`value`. `RSVP.resolve` is essentially shorthand for the following:
```javascript
var promise = new RSVP.Promise(function(resolve, reject){
resolve(1);
});
promise.then(function(value){
// value === 1
});
```
Instead of writing the above, your code now simply becomes the following:
```javascript
var promise = RSVP.resolve(1);
promise.then(function(value){
// value === 1
});
```
@method resolve
@for RSVP
@param {Any} value value that the returned promise will be resolved with
@param {String} label optional string for identifying the returned promise.
Useful for tooling.
@return {Promise} a promise that will become fulfilled with the given
`value`
*/
function resolve(value) {
/*jshint validthis:true */
var Promise = this;
return new Promise(function(resolve, reject) {
resolve(value);
});
}
__exports__.resolve = resolve;
});
define("promise/utils",
["exports"],
function(__exports__) {
function objectOrFunction(x) {
return isFunction(x) || (typeof x === "object" && x !== null);
}
function isFunction(x) {
return typeof x === "function";
}
function isArray(x) {
return Object.prototype.toString.call(x) === "[object Array]";
}
// Date.now is not available in browsers < IE9
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility
var now = Date.now || function() { return new Date().getTime(); };
__exports__.objectOrFunction = objectOrFunction;
__exports__.isFunction = isFunction;
__exports__.isArray = isArray;
__exports__.now = now;
});
requireModule('promise/polyfill').polyfill();
}());
// Minimal AMD (not really AMD) support.
// Define a module:
//
// define('name', ['underscore', 'dep2'], function(_, dep2) {
// exports = {};
// ...
// return exports;
// });
// Require a module explicitly:
//
// var _ = require('underscore');
(function(window, document, navigator, undefined) {
var defined = {};
var resolved = {};
var amdConsole = window.console;
function define(id, deps, module) {
defined[id] = [deps, module];
}
function require(id) {
if (!resolved[id]) {
var definition = defined[id];
if (!definition) {
throw 'Attempted to resolve undefined module: ' + id;
}
var deps = definition[0];
var module = definition[1];
if (typeof deps == 'function' && module === undefined) {
module = deps;
deps = [];
}
try {
deps = deps.map(require);
} catch(e) {
amdConsole.error('Error initializing dependencies: ' + id);
throw e;
}
try {
resolved[id] = module.apply(window, deps);
} catch(e) {
amdConsole.error('Error initializing module: ' + id);
throw e;
}
}
return resolved[id];
}
require.config = function() {};
window.require = resolved.require = require;
window.define = define;
})(window, document, navigator, void 0);
define('log', [], function() {
function log() {
console.log(Array.prototype.slice.call(arguments, 0).join(' '));
}
return {
log: log
};
});
define('dom', [], function() {
function $(sel) {
if (!sel) {
return document.body;
}
var r = document.querySelectorAll(sel);
return r.length == 1 ? r[0] : Array.prototype.slice.call(r);
}
$.matches = function(el, sel) {
var matchesSelector = el.webkitMatchesSelector || el.mozMatchesSelector ||
el.oMatchesSelector || el.matchesSelector;
return matchesSelector.call(el, sel);
};
$.delegate = function(type, sel, handler) {
document.addEventListener(type, function(e) {
var parent = e.target;
while (parent && parent !== document) {
if ($.matches(parent, sel)) {
handler(e);
}
parent = parent.parentNode;
}
}, false);
};
function reqResponse(xhr) {
var data = xhr.responseText;
if ((xhr.getResponseHeader('Content-Type') || '').split(';', 1)[0].indexOf('json') !== -1) {
try {
return JSON.parse(data);
} catch(e) {
// Oh well.
return {};
}
}
return data || null;
}
$.post = function(url, params) {
return new Promise(function(resolve, reject) {
params = serialize(params || {});
var xhr = new XMLHttpRequest();
xhr.open('post', url, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(params);
xhr.addEventListener('load', function() {
var res = reqResponse(xhr);
var statusCode = xhr.status;
if (statusCode < 200 || statusCode > 300) {
return reject(res, xhr);
}
return resolve(res, xhr);
}, false);
});
};
return $;
});
define('utils', [], function() {
var formatRe = /\{([^}]+)\}/g;
function format(s, args) {
if (!s) {
throw new Error('Format string is empty');
}
if (!args) {
return;
}
if (!(args instanceof Array || args instanceof Object)) {
args = Array.prototype.slice.call(arguments, 1);
}
return s.replace(formatRe, function(_, match) {
return args[match];
});
}
function parseLink(url) {
var a = document.createElement('a');
a.href = url;
return a;
}
function parseQueryString(qs) {
if (!qs) {
qs = window.location.search.substr(1);
}
var chunks;
var result = {};
qs.split('&').forEach(function(val) {
chunks = val.split('=');
if (chunks[0]) {
result[chunks[0]] = decodeURIComponent(chunks[1] || '');
}
});
return result;
}
function serialize(obj) {
var qs = [];
Object.keys(obj).forEach(function(key) {
if (obj[key]) {
qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
}
});
return qs.join('&');
}
return {
format: format,
parseLink: parseLink,
parseQueryString: parseQueryString,
serialize: serialize
};
});
define('cache', [], function() {
function Cache() {
var _cache = {};
function _key(value) {
return encodeURIComponent(value);
}
return {
get: function(key) {
return _cache[_key(key)];
},
exists: function(key) {
return _key(key) in _cache;
},
set: function(key, value) {
return _cache[_key(key)] = value;
}
};
}
return Cache;
});
define('templating', ['utils'], function(utils) {
function render(name, ctx, cb) {
if (typeof ctx === 'function') {
cb = ctx;
ctx = {};
}
return env.render(name + '.html', ctx, function(err, res) {
if (err) {
return console.error(err);
}
cb(res);
});
}
var SafeString = nunjucks.require('runtime').SafeString;
var env = nunjucks.configure('src/templates', {autoescape: true});
var envGlobals = nunjucks.require('globals');
var filters = nunjucks.require('filters');
env.addFunction = function(name, func) {
envGlobals[name] = func;
};
env.makeSafe = function(func) {
return function() {
return new SafeString(func.apply(this, arguments));
};
};
filters.format = utils.format;
function _l(str, id, opts) {
// For pluralisation.
var pluralOpts = {};
if (opts && 'n' in opts) {
pluralOpts = {n: opts.n};
}
// Use webL10n to localise.
str = _(id, pluralOpts) || str;
return opts ? filters.format(str, opts) : str;
}
env.addFunction('_', env.makeSafe(_l));
return {
_l: _l,
env: env,
render: render
};
});
define('worker', ['log'], function(log) {
var methods = {
'log': log.log
};
var worker = new Worker('lib/worker.js');
worker.addEventListener('message', function(e) {
if (e.data.type in methods) {
methods[e.data.type](e.data.data);
}
});
return worker;
});
define('pages', ['templating', 'utils'], function(templating, utils) {
var titles = {
'/': templating._l('Mobile sites', 'titleDefault'),
'/search': templating._l('Search', 'titleSearch'),
'/submit': templating._l('Submit', 'titleSubmit')
};
function getPath(url) {
return utils.parseLink(url).pathname;
}
function getTitle(pathname) {
return (titles[pathname] || titles['/']) + ' | loma';
}
return {
getTitle: getTitle,
getPath: getPath,
routes: routes,
titles: titles
};
});
define('login', ['dom'], function($) {
console.log('login');
var $signIn = $('.sign-in');
var $signOut = $('.sign-out');
var loginOpts = {
oncancel: function() {
console.log('Persona login cancelled');
}
};
$.delegate('click', '.sign-in', function() {
navigator.id.request(loginOpts);
});
$.delegate('click', '.sign-in', function() {
navigator.id.logout();
});
navigator.id.watch({
loggedInUser: localStorage.email,
onlogin: function(assertion) {
verifyAssertion(assertion);
},
onlogout: function() {
logoutUser();
}
});
function loginUser(loggedInUser) {
// update your UI to show that the user is logged in
console.log('login OK', loggedInUser);
}
function logoutUser() {
// update your UI to show that the user is logged out
console.log('logout');
}
function handleVerificationResponse(xhr) {
// if successfully verified, log the user in
console.log('assertion OK OK');
}
function verifyAssertion(assertion) {
console.log(assertion)
$.post('http://localhost:5000/verify', {assertion: assertion}).then(function(xhr) {
handleVerificationResponse(xhr);
});
}
});
define('views/search',
['cache', 'dom', 'pages', 'templating', 'utils', 'worker'],
function(cache, $, pages, templating, utils, worker) {
cache = new cache();
var GET;
var indexed = index();
var q;
var previousQuery = null;
var previousResults = null;
var timeStart;
if (document.body.classList.contains('results')) {
search();
}
function eq(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
function index() {
var promise = new Promise(function(resolve, reject) {
worker.addEventListener('message', function(e) {
switch (e.data.type) {
case 'indexed':
return resolve();
case 'results':
return renderResults(e.data.data);
}
});
worker.postMessage({
type: 'index',
data: {
url: '../data/app-processed-docs.json',
fields: {
app_url: {boost: 25},
slug: {boost: 20},
name: {boost: 20},
html_title: {boost: 17},
meta_keywords: {boost: 15},
keywords: {boost: 14},
category: {boost: 10},
meta_description: {boost: 10}
},
ref: '_id'
}
});
});
return promise;
}
function search() {
timeStart = performance.now();
var query = q.value || '';
if (previousQuery === query) {
// Bail if the query hasn't changed.
return;
}
previousQuery = query;
console.log('Queueing search for "' + query + '"');
if (cache.exists(query)) {
console.log('Searching cache for "' + query + '"');
var results = cache.get(query);
results.timeStart = timeStart;
renderResults(results);
} else {
worker.postMessage({
type: 'search',
data: {
query: query,
timeStart: timeStart
}
});
}
}
function reset() {
if (!q) {
q = $('input[name=q');
}
q.value = previousQuery = previousResults = null;
}
function renderResults(data) {
console.log('Rendering results');
data.timing = performance.now() - data.timeStart;
q = $('input[name=q');
// Update location bar based on search term.
GET = utils.parseQueryString();
GET.q = q.value || '';
var serialized = utils.serialize(GET);
var dest = serialized ? ('/?' + serialized) : '/';
if (window.location.href !== dest) {
window.history.replaceState({}, pages.getTitle('/'), dest);
}
templating.render('results-header', {data: data}, function(res) {
$('main header').innerHTML = res;
});
var current = data.results.map(function(x) {
return x.doc._id;
});
var previous = previousResults ? previousResults.results.map(function(x) {
return x.doc._id;
}) : [];
if (!eq(current, previous)) {
// Only re-render results if results have changed.
templating.render('results', {data: data}, function(res) {
$('main ol').innerHTML = res;
});
}
if (!cache.exists(data.query)) {
console.log('Caching "' + data.query + '"');
cache.set(data.query, data);
}
previousResults = data;
}
function init() {
GET = utils.parseQueryString();
reset();
if (GET.q) {
q.value = GET.q;
}
if (document.body.classList.contains('results')) {
search();
}
templating.render('browse', function(res) {
$('main').innerHTML = res;
indexed.then(function() {
document.body.setAttribute('class', 'results');
search();
});
});
}
$.delegate('input', 'input[name=q]', function() {
search();
}, false);
$.delegate('submit', '.form-search', function(e) {
e.preventDefault();
search();
});
return {
index: index,
init: init,
renderResults: renderResults,
reset: reset
};
});
define('views/submit',
['dom', 'views/search', 'templating'],
function($, search, templating) {
function init() {
search.reset();
document.body.setAttribute('class', 'submit');
templating.render('submit', function(res) {
$('main').innerHTML = res;
});
}
return {
init: init
};
});
(function() {
var $ = require('dom');
var GET = require('utils').parseQueryString();
if (GET.debug) {
window.location.href = 'debug.html';
}
if (GET.lang) {
document.webL10n.setLanguage(GET.lang);
}
document.webL10n.ready(function() {
document.documentElement.dir = document.webL10n.getDirection();
document.documentElement.lang = document.webL10n.getLanguage();
var $ = require('dom');
var pages = require('pages');
var templating = require('templating');
var app = new routes();
var views = {
'/': 'search',
'/submit': 'submit'
};
Object.keys(views).forEach(function(path) {
app.get(path, function(req) {
document.title = pages.getTitle(req.url);
var view = require('views/' + views[path]).init();
});
});
app.load = function(url) {
window.history.pushState(undefined, pages.getTitle(url), url);
// We must have the exact pathname (no querystring parameters, etc.).
var path = pages.getPath(url);
if (url !== path) {
url = path;
}
app.test(url);
};
$.delegate('click', 'a[href^="/"]', function(e) {
e.preventDefault();
app.load(e.target.getAttribute('href'));
});
$.delegate('keypress', 'body:not(.results) input[name=q]', function(e) {
app.load('/');
});
templating.render('header', function(res) {
$('.header').innerHTML = res;
// Because `routes.js` listens for `window.load` event, explicitly load view.
app.load(document.location.href);
});
// Initialise login logic when Persona loads.
var personaLoaded = false;
personaLoaded = setInterval(function() {
if ('id' in navigator) {
require('login');
clearInterval(personaLoaded);
}
}, 100);
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment