assetgraph-builder-esprima
Version:
Build system for web sites and applications
440 lines (388 loc) • 16.7 kB
JavaScript
var vm = require('vm'),
_ = require('lodash'),
esanimate = require('esanimate'),
esprima = require('esprima'),
escodegen = require('escodegen'),
i18nTools = require('./i18nTools'),
bootstrapper = module.exports = {};
// Maintained as a function so syntax highlighting works:
/* global bootstrapperCode */
/* jshint ignore:start */
/* istanbul ignore next */
function bootstrapperCode() {
window.INCLUDE = function () {};
window.GETSTATICURL = function (url) { // , placeHolderValue1, placeHolderValue2, ...
var placeHolderValues = Array.prototype.slice.call(arguments, 1);
return url.replace(/\*\*?|\{[^\}]*\}/g, function () {
return placeHolderValues.shift();
});
};
var documentElement = document && document.documentElement,
documentElementLang = documentElement && i18nTools.normalizeLocaleId(documentElement.getAttribute('lang'));
function findActiveLocaleId() {
var isSupportedByLocaleId = {};
if (window.SUPPORTEDLOCALEIDS) {
for (var i = 0 ; i < SUPPORTEDLOCALEIDS.length ; i += 1) {
isSupportedByLocaleId[i18nTools.normalizeLocaleId(SUPPORTEDLOCALEIDS[i])] = true;
}
}
function isSupportedLocaleId(localeId) {
// Assume that all locales are supported if window.SUPPORTEDLOCALEIDS isn't defined
return !window.SUPPORTEDLOCALEIDS || isSupportedByLocaleId[i18nTools.normalizeLocaleId(localeId)];
}
if (documentElementLang && isSupportedLocaleId(documentElementLang)) {
return documentElementLang;
}
// Check the query string for a locale= parameter.
if (window.location && location.href) {
var matchLocationHref = location.href.match(/\?(?:|[^#]*&)locale=([\w\-_]+)(?:[&#]|$)/);
if (matchLocationHref) {
var queryStringLocaleId = i18nTools.normalizeLocaleId(matchLocationHref[1]);
if (isSupportedLocaleId(queryStringLocaleId)) {
return queryStringLocaleId;
}
}
}
if (window.LOCALECOOKIENAME) {
var matchLocaleCookieValue = document.cookie && document.cookie.match(new RegExp("\\b" + LOCALECOOKIENAME.replace(/[\.\+\*\{\}\[\]\(\)\?\^\$]/g, '\\$&') + "=([\\w\\-]+)"));
if (matchLocaleCookieValue) {
var cookieLocaleId = i18nTools.normalizeLocaleId(matchLocaleCookieValue[1]);
if (isSupportedLocaleId(cookieLocaleId)) {
return cookieLocaleId;
}
}
}
if (window.navigator && navigator.language && isSupportedLocaleId(navigator.language)) {
return i18nTools.normalizeLocaleId(navigator.language);
}
if (window.DEFAULTLOCALEID) {
return DEFAULTLOCALEID;
}
if (window.SUPPORTEDLOCALEIDS && SUPPORTEDLOCALEIDS.length > 0) {
return SUPPORTEDLOCALEIDS[0];
}
return 'en_us';
}
window.LOCALEID = findActiveLocaleId();
// Set <html lang="..."> to the actual value so per-locale CSS can work, eg.: html[lang='en'] .myClass {...}
if (!window.BUILDDEVELOPMENT && documentElement && documentElementLang !== LOCALEID) {
documentElement.setAttribute('lang', LOCALEID);
}
// Helper for getting a prioritized, normalized list of relevant locale ids from a specific locale id.
// For instance, "en_US" produces ["en_us", "en"]
function expandLocaleIdToPrioritizedList(localeId) {
if (!localeId) {
return [];
}
localeId = i18nTools.normalizeLocaleId(localeId);
var localeIds = [localeId];
while (/_[^_]+$/.test(localeId)) {
localeId = localeId.replace(/_[^_]+$/, '');
localeIds.push(localeId);
}
return localeIds;
}
// Returns the canonical id of the best matching supported locale, or
// false if no suitable supported locale could be found
function resolveLocaleId(localeId) {
localeId = i18nTools.normalizeLocaleId(localeId);
for (var i = 0 ; i < SUPPORTEDLOCALEIDS.length ; i += 1) {
var supportedLocaleId = SUPPORTEDLOCALEIDS[i];
if (supportedLocaleId === localeId) {
// Exact match
return supportedLocaleId;
}
}
// No exact match found, if the locale id contains variants, try looking for a more general variant:
var prioritizedLocaleIds = expandLocaleIdToPrioritizedList(localeId);
if (prioritizedLocaleIds.length > 1) {
return resolveLocaleId(prioritizedLocaleIds[1]);
}
return false;
}
// Compute on the first use so the application has a chance to change LOCALEID before TR is used for the first time:
var allKeysForLocale;
function getAllKeysForLocale() {
if (!allKeysForLocale) {
allKeysForLocale = {};
var prioritizedLocaleIds = expandLocaleIdToPrioritizedList(LOCALEID);
for (var key in I18NKEYS) {
if (I18NKEYS.hasOwnProperty(key)) {
for (var i = 0 ; i < prioritizedLocaleIds.length ; i += 1) {
if (prioritizedLocaleIds[i] in I18NKEYS[key]) {
allKeysForLocale[key] = I18NKEYS[key][prioritizedLocaleIds[i]];
break;
}
}
}
}
}
return allKeysForLocale;
}
window.LOCALIZE = true;
window.TR = function (key, defaultValue) {
return getAllKeysForLocale()[key] || defaultValue || '[!' + key + '!]';
};
window.TRPAT = function (key, defaultPattern) {
var pattern = TR(key, defaultPattern),
tokens = i18nTools.tokenizePattern(pattern);
return function () { // placeHolderValue, ...
var placeHolderValues = arguments,
renderedString = '';
for (var i = 0 ; i < tokens.length ; i += 1) {
var token = tokens[i];
if (token.type === 'placeHolder') {
renderedString += placeHolderValues[token.value];
} else {
// token.type === 'text'
renderedString += token.value;
}
}
return renderedString;
};
};
window.TRHTML = function (htmlString) {
var div = document.createElement('div');
div.innerHTML = htmlString;
i18nTools.eachI18nTagInHtmlDocument(div, i18nTools.createI18nTagReplacer({
allKeysForLocale: getAllKeysForLocale(),
localeId: LOCALEID,
keepI18nAttributes: true,
keepSpans: true
}), function nestedTemplateHandler(node) {
if (node.firstChild && node.firstChild.nodeType === node.TEXT_NODE) {
// Use window.TRHTML instead of TRHTML to prevent the recursive call from being recognized as a relation:
node.firstChild.nodeValue = window.TRHTML(node.firstChild.nodeValue);
}
});
return div.innerHTML;
};
window.GETTEXT = function (url) {
// Do a synchronous XHR in development mode:
var xhr;
try {
xhr = new XMLHttpRequest();
} catch (e) {
try {
xhr = new ActiveXObject('Microsoft.XmlHTTP');
} catch (e) {}
}
if (!xhr) {
throw new Error("GETTEXT: Couldn't initialize an XMLHttpRequest object.");
}
xhr.open('GET', url, false);
xhr.send();
if (xhr.status && xhr.status >= 200 && xhr.status < 400) {
return xhr.responseText;
} else {
throw new Error("GETTEXT: Unexpected response from the server: " + (xhr && xhr.status));
}
};
// Taken from jQuery 1.8.3:
var isReady = false, // Is the DOM ready to be used? Set to true once it occurs.
readyWait = 1, // A counter to track how many items to wait for before the ready event fires. See #6781
readyList;
// The ready event handler and self cleanup method
function DOMContentLoaded() {
if (document.addEventListener) {
document.removeEventListener("DOMContentLoaded", DOMContentLoaded, false);
ready();
} else if (document.readyState === "complete") {
// we're here because readyState === "complete" in oldIE
// which is good enough for us to call the dom ready!
document.detachEvent("onreadystatechange", DOMContentLoaded);
ready();
}
}
// Handle when the DOM is ready
function ready(wait) {
// Abort if there are pending holds or we're already ready
if (wait === true ? --readyWait : isReady) {
return;
}
// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
if (!document.body) {
return setTimeout(ready, 1);
}
// Remember that the DOM is ready
isReady = true;
// If a normal DOM Ready event fired, decrement, and wait if need be
if (wait !== true && --readyWait > 0) {
return;
}
if (readyList) {
for (var i = 0 ; i < readyList.length ; i += 1) {
readyList[i]();
}
readyList = [];
}
}
function onReady(fn) {
if (!readyList) {
readyList = [];
// Catch cases where $(document).ready() is called after the browser event has already occurred.
// we once tried to use readyState "interactive" here, but it caused issues like the one
// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
if (document.readyState === "complete") {
// Handle it asynchronously to allow scripts the opportunity to delay ready
setTimeout(ready, 1);
// Standards-based browsers support DOMContentLoaded
} else if (document.addEventListener) {
// Use the handy event callback
document.addEventListener("DOMContentLoaded", DOMContentLoaded, false);
// A fallback to window.onload, that will always work
window.addEventListener("load", ready, false);
// If IE event model is used
} else {
// Ensure firing before onload, maybe late but safe also for iframes
document.attachEvent("onreadystatechange", DOMContentLoaded);
// A fallback to window.onload, that will always work
window.attachEvent("onload", ready);
// If IE and not a frame
// continually check to see if the document is ready
var top = false;
try {
top = window.frameElement == null && document.documentElement;
} catch(e) {}
if (top && top.doScroll) {
(function doScrollCheck() {
if (!isReady) {
try {
// Use the trick by Diego Perini
// http://javascript.nwbox.com/IEContentLoaded/
top.doScroll("left");
} catch(e) {
return setTimeout(doScrollCheck, 50);
}
// and execute any waiting functions
ready();
}
})();
}
}
}
readyList.push(fn);
}
function translateDocument() {
i18nTools.eachI18nTagInHtmlDocument(document, i18nTools.createI18nTagReplacer({
allKeysForLocale: getAllKeysForLocale(),
keepI18nAttributes: true,
keepSpans: true
}));
}
// Give scripts a chance to turn off translation altogether:
if (document && document.childNodes && window.TRANSLATE !== false && !window.BUILDDEVELOPMENT) {
if (!window.setTimeout || (!document.addEventListener && !document.attachEvent)) {
// Assume we're running in an environment where the document is already loaded (jsdom?)
translateDocument();
} else {
onReady(translateDocument);
}
}
}
/* jshint ignore:end */
bootstrapper.createAst = function (initialAsset, assetGraph, options) {
options = options || {};
if (initialAsset.type !== 'Html' && initialAsset.type !== 'JavaScript') {
throw new Error('bootstrapper.createAst: initialAsset must be Html or JavaScript, but got ' + initialAsset);
}
var statementAsts = [],
globalValueByName = {
I18NKEYS: i18nTools.extractAllKeys(assetGraph)
};
// Add window.SUPPORTEDLOCALEIDS, window.DEFAULTLOCALEID, and window.LOCALECOOKIENAME if provided in the options object:
['supportedLocaleIds', 'defaultLocaleId', 'localeCookieName'].forEach(function (optionName) {
if (options[optionName]) {
globalValueByName[optionName.toUpperCase()] = options[optionName];
}
});
Object.keys(globalValueByName).forEach(function (globalName) {
statementAsts.push({
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
operator: '=',
left: {
type: 'MemberExpression',
computed: false,
property: { type: 'Identifier', name: globalName },
object: { type: 'Identifier', name: 'window' }
},
right: esanimate.astify(globalValueByName[globalName])
}
});
});
statementAsts.push({
type: 'VariableDeclaration',
kind: 'var',
declarations: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: 'i18nTools' },
init: {
type: 'ObjectExpression',
properties: []
}
}
]
});
['normalizeLocaleId', 'tokenizePattern', 'eachI18nTagInHtmlDocument', 'createI18nTagReplacer'].forEach(function (i18nToolsFunctionName) {
statementAsts.push({
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
operator: '=',
left: {
type: 'MemberExpression',
property: { type: 'Identifier', name: i18nToolsFunctionName },
object: { type: 'Identifier', name: 'i18nTools' }
},
right: esanimate.astify(i18nTools[i18nToolsFunctionName])
}
});
});
Array.prototype.push.apply(statementAsts, esprima.parse('!' + bootstrapperCode.toString()).body[0].expression.argument.body.body);
// Wrap in immediately invoked function:
return {
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'FunctionExpression',
id: null,
params: [],
body: {
type: 'BlockStatement',
body: statementAsts
}
},
arguments: []
}
}
]
};
};
bootstrapper.createContext = function (initialAsset, assetGraph, contextProperties) {
var context = vm.createContext();
context.window = context;
context.assetGraph = assetGraph;
context.console = console;
if (contextProperties) {
_.extend(context, contextProperties);
}
if (initialAsset.type === 'Html') {
context.initialAsset = initialAsset;
context.__defineSetter__('document', function () {});
context.__defineGetter__('document', function () {
initialAsset.markDirty();
return initialAsset.parseTree;
});
}
vm.runInContext(escodegen.generate(bootstrapper.createAst(initialAsset, assetGraph)),
context,
'bootstrap code for ' + initialAsset.urlOrDescription);
return context;
};