3525 lines
108 KiB
JavaScript
Executable File
3525 lines
108 KiB
JavaScript
Executable File
(function(window, undefined) {
|
|
|
|
function define(name, payload) {
|
|
define.modules[name] = payload;
|
|
};
|
|
|
|
// un-instantiated modules
|
|
define.modules = {};
|
|
// instantiated modules
|
|
define.exports = {};
|
|
|
|
function normalize(path) {
|
|
var parts = path.split('/');
|
|
var normalized = [];
|
|
for (var i = 0; i < parts.length; i++) {
|
|
if (parts[i] == '.') {
|
|
// don't add it to `normalized`
|
|
} else if (parts[i] == '..') {
|
|
normalized.pop();
|
|
} else {
|
|
normalized.push(parts[i]);
|
|
}
|
|
}
|
|
return normalized.join('/');
|
|
}
|
|
|
|
function join(a, b) {
|
|
return a ? a.trim().replace(/\/*$/, '/') + b.trim() : b.trim();
|
|
}
|
|
|
|
function dirname(path) {
|
|
return path ? path.split('/').slice(0, -1).join('/') : null;
|
|
}
|
|
|
|
function req(leaf, name) {
|
|
name = normalize(join(dirname(leaf), name));
|
|
if (name in define.exports) {
|
|
return define.exports[name];
|
|
}
|
|
if (!(name in define.modules)) {
|
|
throw new Error("Module not defined: " + name);
|
|
}
|
|
|
|
var module = define.modules[name];
|
|
if (typeof module == "function") {
|
|
var exports = {};
|
|
var reply = module(req.bind(null, name), exports, { id: name, uri: "" });
|
|
module = (reply !== undefined) ? reply : exports;
|
|
}
|
|
return define.exports[name] = module;
|
|
};
|
|
|
|
// for the top-level required modules, leaf is null
|
|
var require = req.bind(null, null);
|
|
define('l20n', function(require, exports) {
|
|
'use strict';
|
|
|
|
var Context = require('./l20n/context').Context;
|
|
var Parser = require('./l20n/parser').Parser;
|
|
var Compiler = require('./l20n/compiler').Compiler;
|
|
|
|
exports.Context = Context;
|
|
exports.Parser = Parser;
|
|
exports.Compiler = Compiler;
|
|
exports.getContext = function L20n_getContext(id) {
|
|
return new Context(id);
|
|
};
|
|
|
|
});
|
|
define('l20n/context', function(require, exports) {
|
|
'use strict';
|
|
|
|
var EventEmitter = require('./events').EventEmitter;
|
|
var Parser = require('./parser').Parser;
|
|
var Compiler = require('./compiler').Compiler;
|
|
var RetranslationManager = require('./retranslation').RetranslationManager;
|
|
|
|
// register globals with RetranslationManager
|
|
require('./platform/globals');
|
|
var io = require('./platform/io');
|
|
|
|
function Resource(id, parser) {
|
|
var self = this;
|
|
|
|
this.id = id;
|
|
this.resources = [];
|
|
this.source = null;
|
|
this.ast = {
|
|
type: 'LOL',
|
|
body: []
|
|
};
|
|
|
|
this.build = build;
|
|
|
|
var _imports_positions = [];
|
|
// absolute URLs start with a slash or contain a colon (for schema)
|
|
var reAbsolute = /^\/|:/;
|
|
|
|
function build(nesting, callback, sync) {
|
|
if (nesting >= 7) {
|
|
return callback(new ContextError('Too many nested imports.'));
|
|
}
|
|
|
|
if (self.source) {
|
|
// Bug 908826 - Don't artificially force asynchronicity when only using
|
|
// addResource
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=908826
|
|
return setTimeout(function() {
|
|
parse();
|
|
});
|
|
} else {
|
|
io.load(self.id, parse, sync);
|
|
}
|
|
|
|
function parse(err, text) {
|
|
if (err) {
|
|
return callback(err);
|
|
} else if (text !== undefined) {
|
|
self.source = text;
|
|
}
|
|
self.ast = parser.parse(self.source);
|
|
buildImports();
|
|
}
|
|
|
|
function buildImports() {
|
|
var imports = self.ast.body.filter(function(elem, i) {
|
|
if (elem.type === 'ImportStatement') {
|
|
_imports_positions.push(i);
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
imports.forEach(function(imp) {
|
|
var uri = relativeToSelf(imp.uri.content);
|
|
var res = new Resource(uri, parser);
|
|
self.resources.push(res);
|
|
});
|
|
|
|
var importsToBuild = self.resources.length;
|
|
if (importsToBuild === 0) {
|
|
return callback();
|
|
}
|
|
|
|
self.resources.forEach(function(res) {
|
|
res.build(nesting + 1, resourceBuilt, sync);
|
|
});
|
|
|
|
function resourceBuilt(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
importsToBuild--;
|
|
if (importsToBuild === 0) {
|
|
flatten();
|
|
}
|
|
}
|
|
}
|
|
|
|
function flatten() {
|
|
for (var i = self.resources.length - 1; i >= 0; i--) {
|
|
var pos = _imports_positions[i] || 0;
|
|
Array.prototype.splice.apply(self.ast.body,
|
|
[pos, 1].concat(self.resources[i].ast.body));
|
|
}
|
|
callback();
|
|
}
|
|
}
|
|
|
|
function relativeToSelf(url) {
|
|
if (self.id === null || reAbsolute.test(url)) {
|
|
return url;
|
|
}
|
|
var dirs = self.id.split('/')
|
|
.slice(0, -1)
|
|
.concat(url.split('/'))
|
|
.filter(function(elem) {
|
|
return elem !== '.';
|
|
});
|
|
|
|
return dirs.join('/');
|
|
}
|
|
|
|
}
|
|
|
|
function Locale(id, parser, compiler, emitter) {
|
|
this.id = id;
|
|
this.resources = [];
|
|
this.entries = null;
|
|
this.ast = {
|
|
type: 'LOL',
|
|
body: []
|
|
};
|
|
this.isReady = false;
|
|
|
|
this.build = build;
|
|
this.getEntry = getEntry;
|
|
this.hasResource = hasResource;
|
|
|
|
var self = this;
|
|
|
|
function build(callback) {
|
|
if (!callback) {
|
|
var sync = true;
|
|
}
|
|
|
|
var resourcesToBuild = self.resources.length;
|
|
if (resourcesToBuild === 0) {
|
|
throw new ContextError('Locale has no resources');
|
|
}
|
|
|
|
var resourcesWithErrors = 0;
|
|
self.resources.forEach(function(res) {
|
|
res.build(0, resourceBuilt, sync);
|
|
});
|
|
|
|
function resourceBuilt(err) {
|
|
if (err) {
|
|
resourcesWithErrors++;
|
|
emitter.emit(err instanceof ContextError ? 'error' : 'warning', err);
|
|
}
|
|
resourcesToBuild--;
|
|
if (resourcesToBuild === 0) {
|
|
if (resourcesWithErrors === self.resources.length) {
|
|
// XXX Bug 908780 - Decide what to do when all resources in
|
|
// a locale are missing or broken
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=908780
|
|
emitter.emit('error',
|
|
new ContextError('Locale has no valid resources'));
|
|
}
|
|
flatten();
|
|
}
|
|
}
|
|
|
|
function flatten() {
|
|
self.ast.body = self.resources.reduce(function(prev, curr) {
|
|
return prev.concat(curr.ast.body);
|
|
}, self.ast.body);
|
|
compile();
|
|
}
|
|
|
|
function compile() {
|
|
self.entries = compiler.compile(self.ast);
|
|
self.isReady = true;
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
}
|
|
|
|
function getEntry(id) {
|
|
/* jshint validthis: true */
|
|
if (this.entries.hasOwnProperty(id)) {
|
|
return this.entries[id];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function hasResource(uri) {
|
|
/* jshint validthis: true */
|
|
return this.resources.some(function(res) {
|
|
return res.id === uri;
|
|
});
|
|
}
|
|
}
|
|
|
|
function Context(id) {
|
|
|
|
this.id = id;
|
|
|
|
this.registerLocales = registerLocales;
|
|
this.registerLocaleNegotiator = registerLocaleNegotiator;
|
|
this.requestLocales = requestLocales;
|
|
this.addResource = addResource;
|
|
this.linkResource = linkResource;
|
|
this.updateData = updateData;
|
|
|
|
this.getSync = getSync;
|
|
this.getEntitySync = getEntitySync;
|
|
this.localize = localize;
|
|
this.ready = ready;
|
|
|
|
this.addEventListener = addEvent;
|
|
this.removeEventListener = removeEvent;
|
|
|
|
Object.defineProperty(this, 'supportedLocales', {
|
|
get: function() { return _fallbackChain.slice(); },
|
|
enumerable: true
|
|
});
|
|
|
|
var _data = {};
|
|
|
|
// language negotiator function
|
|
var _negotiator;
|
|
|
|
// registered and available languages
|
|
var _default = 'i-default';
|
|
var _registered = [_default];
|
|
var _requested = [];
|
|
var _fallbackChain = [];
|
|
// Locale objects corresponding to the registered languages
|
|
var _locales = {};
|
|
|
|
// URLs or text of resources (with information about the type) added via
|
|
// linkResource and addResource
|
|
var _reslinks = [];
|
|
|
|
var _isReady = false;
|
|
var _isFrozen = false;
|
|
var _emitter = new EventEmitter();
|
|
var _parser = new Parser();
|
|
var _compiler = new Compiler();
|
|
|
|
var _retr = new RetranslationManager();
|
|
|
|
var self = this;
|
|
|
|
_parser.addEventListener('error', error);
|
|
_compiler.addEventListener('error', warn);
|
|
_compiler.setGlobals(_retr.globals);
|
|
|
|
function extend(dst, src) {
|
|
Object.keys(src).forEach(function(key) {
|
|
if (src[key] === undefined) {
|
|
// un-define (remove) the property from dst
|
|
delete dst[key];
|
|
} else if (typeof src[key] !== 'object') {
|
|
// if the source property is a primitive, just copy it overwriting
|
|
// whatever the destination property is
|
|
dst[key] = src[key];
|
|
} else {
|
|
// if the source property is an object, deep-copy it recursively
|
|
if (typeof dst[key] !== 'object') {
|
|
dst[key] = {};
|
|
}
|
|
extend(dst[key], src[key]);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateData(obj) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
throw new ContextError('Context data must be a non-null object');
|
|
}
|
|
extend(_data, obj);
|
|
}
|
|
|
|
function getSync(id, data) {
|
|
if (!_isReady) {
|
|
throw new ContextError('Context not ready');
|
|
}
|
|
return getFromLocale.call(self, 0, id, data).value;
|
|
}
|
|
|
|
function getEntitySync(id, data) {
|
|
if (!_isReady) {
|
|
throw new ContextError('Context not ready');
|
|
}
|
|
return getFromLocale.call(self, 0, id, data);
|
|
}
|
|
|
|
function localize(ids, callback) {
|
|
if (!callback) {
|
|
throw new ContextError('No callback passed');
|
|
}
|
|
return bindLocalize.call(self, ids, callback);
|
|
}
|
|
|
|
function ready(callback) {
|
|
if (_isReady) {
|
|
setTimeout(callback);
|
|
}
|
|
addEvent('ready', callback);
|
|
}
|
|
|
|
function bindLocalize(ids, callback, reason) {
|
|
/* jshint validthis: true */
|
|
|
|
var bound = {
|
|
// stop: fn
|
|
extend: function extend(newIds) {
|
|
for (var i = 0; i < newIds.length; i++) {
|
|
if (ids.indexOf(newIds[i]) === -1) {
|
|
ids.push(newIds[i]);
|
|
}
|
|
}
|
|
if (!_isReady) {
|
|
return;
|
|
}
|
|
var newMany = getMany.call(this, newIds);
|
|
// rebind the callback in `_retr`: append new globals seen used in
|
|
// `newIds` and overwrite the callback with a new one which has the
|
|
// updated `ids`
|
|
_retr.bindGet({
|
|
id: callback,
|
|
callback: bindLocalize.bind(this, ids, callback),
|
|
globals: Object.keys(newMany.globalsUsed)
|
|
}, true);
|
|
return newMany;
|
|
}.bind(this),
|
|
stop: function stop() {
|
|
_retr.unbindGet(callback);
|
|
}.bind(this)
|
|
};
|
|
|
|
|
|
// if the ctx isn't ready, bind the callback and return
|
|
if (!_isReady) {
|
|
_retr.bindGet({
|
|
id: callback,
|
|
callback: bindLocalize.bind(this, ids, callback),
|
|
globals: []
|
|
});
|
|
return bound;
|
|
}
|
|
|
|
// if the ctx is ready, retrieve the entities
|
|
var many = getMany.call(this, ids);
|
|
var l10n = {
|
|
entities: many.entities,
|
|
// `reason` might be undefined if context was ready before `localize`
|
|
// was called; in that case, we pass `locales` so that this scenario
|
|
// is transparent for the callback
|
|
reason: reason || { locales: _fallbackChain.slice() },
|
|
stop: function() {
|
|
_retr.unbindGet(callback);
|
|
}
|
|
};
|
|
_retr.bindGet({
|
|
id: callback,
|
|
callback: bindLocalize.bind(this, ids, callback),
|
|
globals: Object.keys(many.globalsUsed)
|
|
});
|
|
// callback may call bound.extend which will rebind it if needed; for
|
|
// this to work it needs to be called after _retr.bindGet above;
|
|
// otherwise bindGet would listen to globals passed initially in
|
|
// many.globalsUsed
|
|
callback(l10n);
|
|
return bound;
|
|
}
|
|
|
|
function getMany(ids) {
|
|
/* jshint validthis: true */
|
|
var many = {
|
|
entities: {},
|
|
globalsUsed: {}
|
|
};
|
|
for (var i = 0, id; id = ids[i]; i++) {
|
|
many.entities[id] = getEntitySync.call(this, id);
|
|
for (var global in many.entities[id].globals) {
|
|
if (many.entities[id].globals.hasOwnProperty(global)) {
|
|
many.globalsUsed[global] = true;
|
|
}
|
|
}
|
|
}
|
|
return many;
|
|
}
|
|
|
|
function getFromLocale(cur, id, data, prevSource) {
|
|
/* jshint validthis: true */
|
|
var loc = _fallbackChain[cur];
|
|
if (!loc) {
|
|
error(new RuntimeError('Unable to get translation', id,
|
|
_fallbackChain));
|
|
// imitate the return value of Compiler.Entity.get
|
|
return {
|
|
value: prevSource ? prevSource.source : id,
|
|
attributes: {},
|
|
globals: {},
|
|
locale: prevSource ? prevSource.loc : null
|
|
};
|
|
}
|
|
var locale = getLocale(loc);
|
|
|
|
if (!locale.isReady) {
|
|
// build without a callback, synchronously
|
|
locale.build(null);
|
|
}
|
|
|
|
var entry = locale.getEntry(id);
|
|
|
|
// if the entry is missing, just go to the next locale immediately
|
|
if (entry === undefined) {
|
|
warn(new TranslationError('Not found', id, _fallbackChain, locale));
|
|
return getFromLocale.call(this, cur + 1, id, data, prevSource);
|
|
}
|
|
|
|
// otherwise, try to get the value of the entry
|
|
var value;
|
|
try {
|
|
value = entry.get(getArgs.call(this, data));
|
|
} catch (e) {
|
|
if (e instanceof Compiler.RuntimeError) {
|
|
error(new TranslationError(e.message, id, _fallbackChain, locale));
|
|
if (e instanceof Compiler.ValueError) {
|
|
// salvage the source string which the compiler wasn't able to
|
|
// evaluate completely; this is still better than returning the
|
|
// identifer; prefer a source string from locales earlier in the
|
|
// fallback chain, if available
|
|
var source = prevSource || { source: e.source, loc: locale.id };
|
|
return getFromLocale.call(this, cur + 1, id, data, source);
|
|
}
|
|
return getFromLocale.call(this, cur + 1, id, data, prevSource);
|
|
} else {
|
|
throw error(e);
|
|
}
|
|
}
|
|
value.locale = locale.id;
|
|
return value;
|
|
}
|
|
|
|
function getArgs(extra) {
|
|
if (!extra) {
|
|
return _data;
|
|
}
|
|
var args = {};
|
|
// deep-clone _data first
|
|
extend(args, _data);
|
|
// overwrite args with the extra args passed to the `get` call
|
|
extend(args, extra);
|
|
return args;
|
|
}
|
|
|
|
function addResource(text) {
|
|
if (_isFrozen) {
|
|
throw new ContextError('Context is frozen');
|
|
}
|
|
_reslinks.push(['text', text]);
|
|
}
|
|
|
|
function add(text, locale) {
|
|
var res = new Resource(null, _parser);
|
|
res.source = text;
|
|
locale.resources.push(res);
|
|
}
|
|
|
|
function linkResource(uri) {
|
|
if (_isFrozen) {
|
|
throw new ContextError('Context is frozen');
|
|
}
|
|
_reslinks.push([typeof uri === 'function' ? 'template' : 'uri', uri]);
|
|
}
|
|
|
|
function link(uri, locale) {
|
|
if (!locale.hasResource(uri)) {
|
|
var res = new Resource(uri, _parser);
|
|
locale.resources.push(res);
|
|
}
|
|
}
|
|
|
|
function registerLocales(defLocale, available) {
|
|
if (_isFrozen) {
|
|
throw new ContextError('Context is frozen');
|
|
}
|
|
|
|
if (defLocale === undefined) {
|
|
return;
|
|
}
|
|
|
|
_default = defLocale;
|
|
_registered = [];
|
|
|
|
if (!available) {
|
|
available = [];
|
|
}
|
|
available.push(defLocale);
|
|
|
|
// uniquify `available` into `_registered`
|
|
available.forEach(function(loc) {
|
|
if (typeof loc !== 'string') {
|
|
throw new ContextError('Language codes must be strings');
|
|
}
|
|
if (_registered.indexOf(loc) === -1) {
|
|
_registered.push(loc);
|
|
}
|
|
});
|
|
}
|
|
|
|
function registerLocaleNegotiator(negotiator) {
|
|
if (_isFrozen) {
|
|
throw new ContextError('Context is frozen');
|
|
}
|
|
_negotiator = negotiator;
|
|
}
|
|
|
|
function getLocale(loc) {
|
|
if (_locales[loc]) {
|
|
return _locales[loc];
|
|
}
|
|
var locale = new Locale(loc, _parser, _compiler, _emitter);
|
|
_locales[loc] = locale;
|
|
// populate the locale with resources
|
|
for (var j = 0; j < _reslinks.length; j++) {
|
|
var res = _reslinks[j];
|
|
if (res[0] === 'text') {
|
|
// a resource added via addResource(String)
|
|
add(res[1], locale);
|
|
} else if (res[0] === 'uri') {
|
|
// a resource added via linkResource(String)
|
|
link(res[1], locale);
|
|
} else {
|
|
// a resource added via linkResource(Function); the function
|
|
// passed is a URL template and it takes the current locale's code
|
|
// as an argument
|
|
link(res[1](locale.id), locale);
|
|
}
|
|
}
|
|
return locale;
|
|
}
|
|
|
|
function requestLocales() {
|
|
if (_isFrozen && !_isReady) {
|
|
throw new ContextError('Context not ready');
|
|
}
|
|
|
|
if (_reslinks.length === 0) {
|
|
warn(new ContextError('Context has no resources; not freezing'));
|
|
return;
|
|
}
|
|
|
|
_isFrozen = true;
|
|
_requested = Array.prototype.slice.call(arguments);
|
|
|
|
if (_requested.length) {
|
|
_requested.forEach(function(loc) {
|
|
if (typeof loc !== 'string') {
|
|
throw new ContextError('Language codes must be strings');
|
|
}
|
|
});
|
|
}
|
|
|
|
// the whole language negotiation process can be asynchronous; for now
|
|
// we just use _registered as the list of all available locales, but in
|
|
// the future we might asynchronously try to query a language pack
|
|
// service of sorts for its own list of locales supported for this
|
|
// context
|
|
if (!_negotiator) {
|
|
var Intl = require('./intl').Intl;
|
|
_negotiator = Intl.prioritizeLocales;
|
|
}
|
|
var fallbackChain = _negotiator(_registered, _requested, _default,
|
|
freeze);
|
|
// if the negotiator returned something, freeze synchronously
|
|
if (fallbackChain) {
|
|
freeze(fallbackChain);
|
|
}
|
|
}
|
|
|
|
function freeze(fallbackChain) {
|
|
_fallbackChain = fallbackChain;
|
|
var locale = getLocale(_fallbackChain[0]);
|
|
if (locale.isReady) {
|
|
setReady();
|
|
} else {
|
|
locale.build(setReady);
|
|
}
|
|
}
|
|
|
|
function setReady() {
|
|
_isReady = true;
|
|
_retr.all(_fallbackChain.slice());
|
|
_emitter.emit('ready');
|
|
}
|
|
|
|
function addEvent(type, listener) {
|
|
_emitter.addEventListener(type, listener);
|
|
}
|
|
|
|
function removeEvent(type, listener) {
|
|
_emitter.removeEventListener(type, listener);
|
|
}
|
|
|
|
function warn(e) {
|
|
_emitter.emit('warning', e);
|
|
return e;
|
|
}
|
|
|
|
function error(e) {
|
|
_emitter.emit('error', e);
|
|
return e;
|
|
}
|
|
}
|
|
|
|
Context.Error = ContextError;
|
|
Context.RuntimeError = RuntimeError;
|
|
Context.TranslationError = TranslationError;
|
|
|
|
function ContextError(message) {
|
|
this.name = 'ContextError';
|
|
this.message = message;
|
|
}
|
|
ContextError.prototype = Object.create(Error.prototype);
|
|
ContextError.prototype.constructor = ContextError;
|
|
|
|
function RuntimeError(message, id, supported) {
|
|
ContextError.call(this, message);
|
|
this.name = 'RuntimeError';
|
|
this.entity = id;
|
|
this.supportedLocales = supported.slice();
|
|
this.message = id + ': ' + message + '; tried ' + supported.join(', ');
|
|
}
|
|
RuntimeError.prototype = Object.create(ContextError.prototype);
|
|
RuntimeError.prototype.constructor = RuntimeError;
|
|
|
|
function TranslationError(message, id, supported, locale) {
|
|
RuntimeError.call(this, message, id, supported);
|
|
this.name = 'TranslationError';
|
|
this.locale = locale.id;
|
|
this.message = '[' + this.locale + '] ' + id + ': ' + message;
|
|
}
|
|
TranslationError.prototype = Object.create(RuntimeError.prototype);
|
|
TranslationError.prototype.constructor = TranslationError;
|
|
|
|
exports.Context = Context;
|
|
|
|
});
|
|
define('l20n/events', function(require, exports) {
|
|
'use strict';
|
|
|
|
function EventEmitter() {
|
|
this._listeners = {};
|
|
}
|
|
|
|
EventEmitter.prototype.emit = function ee_emit() {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var type = args.shift();
|
|
if (!this._listeners[type]) {
|
|
return false;
|
|
}
|
|
var typeListeners = this._listeners[type].slice();
|
|
for (var i = 0; i < typeListeners.length; i++) {
|
|
typeListeners[i].apply(this, args);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
EventEmitter.prototype.addEventListener = function ee_add(type, listener) {
|
|
if (!this._listeners[type]) {
|
|
this._listeners[type] = [];
|
|
}
|
|
this._listeners[type].push(listener);
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.removeEventListener = function ee_rm(type, listener) {
|
|
var typeListeners = this._listeners[type];
|
|
var pos = typeListeners.indexOf(listener);
|
|
if (pos === -1) {
|
|
return this;
|
|
}
|
|
typeListeners.splice(pos, 1);
|
|
return this;
|
|
};
|
|
|
|
exports.EventEmitter = EventEmitter;
|
|
|
|
});
|
|
define('l20n/parser', function(require, exports) {
|
|
'use strict';
|
|
|
|
var EventEmitter = require('./events').EventEmitter;
|
|
|
|
function Parser(throwOnErrors) {
|
|
|
|
/* Public */
|
|
|
|
this.parse = parse;
|
|
this.addEventListener = addEvent;
|
|
this.removeEventListener = removeEvent;
|
|
|
|
/* Private */
|
|
|
|
var MAX_PLACEABLES = 100;
|
|
|
|
var _source, _index, _length, _emitter;
|
|
|
|
var getLOL;
|
|
if (throwOnErrors) {
|
|
getLOL = getLOLPlain;
|
|
} else {
|
|
_emitter = new EventEmitter();
|
|
getLOL = getLOLWithRecover;
|
|
}
|
|
|
|
function getComment() {
|
|
_index += 2;
|
|
var start = _index;
|
|
var end = _source.indexOf('*/', start);
|
|
if (end === -1) {
|
|
throw error('Comment without closing tag');
|
|
}
|
|
_index = end + 2;
|
|
return {
|
|
type: 'Comment',
|
|
content: _source.slice(start, end)
|
|
};
|
|
}
|
|
|
|
function getAttributes() {
|
|
var attrs = [];
|
|
var attr, ws1, ch;
|
|
|
|
while (true) {
|
|
attr = getKVPWithIndex('Attribute');
|
|
attr.local = attr.key.name.charAt(0) === '_';
|
|
attrs.push(attr);
|
|
ws1 = getRequiredWS();
|
|
ch = _source.charAt(_index);
|
|
if (ch === '>') {
|
|
break;
|
|
} else if (!ws1) {
|
|
throw error('Expected ">"');
|
|
}
|
|
}
|
|
return attrs;
|
|
}
|
|
|
|
function getKVP(type) {
|
|
var key = getIdentifier();
|
|
getWS();
|
|
if (_source.charAt(_index) !== ':') {
|
|
throw error('Expected ":"');
|
|
}
|
|
++_index;
|
|
getWS();
|
|
return {
|
|
type: type,
|
|
key: key,
|
|
value: getValue()
|
|
};
|
|
}
|
|
|
|
function getKVPWithIndex(type) {
|
|
var key = getIdentifier();
|
|
var index = [];
|
|
|
|
if (_source.charAt(_index) === '[') {
|
|
++_index;
|
|
getWS();
|
|
index = getItemList(getExpression, ']');
|
|
}
|
|
getWS();
|
|
if (_source.charAt(_index) !== ':') {
|
|
throw error('Expected ":"');
|
|
}
|
|
++_index;
|
|
getWS();
|
|
return {
|
|
type: type,
|
|
key: key,
|
|
value: getValue(),
|
|
index: index
|
|
};
|
|
}
|
|
|
|
function getHash() {
|
|
++_index;
|
|
getWS();
|
|
var defItem, hi, comma, hash = [];
|
|
var hasDefItem = false;
|
|
while (true) {
|
|
defItem = false;
|
|
if (_source.charAt(_index) === '*') {
|
|
++_index;
|
|
if (hasDefItem) {
|
|
throw error('Default item redefinition forbidden');
|
|
}
|
|
defItem = true;
|
|
hasDefItem = true;
|
|
}
|
|
hi = getKVP('HashItem');
|
|
hi['default'] = defItem;
|
|
hash.push(hi);
|
|
getWS();
|
|
|
|
comma = _source.charAt(_index) === ',';
|
|
if (comma) {
|
|
++_index;
|
|
getWS();
|
|
}
|
|
if (_source.charAt(_index) === '}') {
|
|
++_index;
|
|
break;
|
|
}
|
|
if (!comma) {
|
|
throw error('Expected "}"');
|
|
}
|
|
}
|
|
return {
|
|
type: 'Hash',
|
|
content: hash
|
|
};
|
|
}
|
|
|
|
function _unescapeString() {
|
|
var ch = _source.charAt(++_index);
|
|
var cc;
|
|
if (ch === 'u') { // special case for unicode
|
|
var ucode = '';
|
|
for (var i = 0; i < 4; i++) {
|
|
ch = _source[++_index];
|
|
cc = ch.charCodeAt(0);
|
|
if ((cc > 96 && cc < 103) || // a-f
|
|
(cc > 64 && cc < 71) || // A-F
|
|
(cc > 47 && cc < 58)) { // 0-9
|
|
ucode += ch;
|
|
} else {
|
|
throw error('Illegal unicode escape sequence');
|
|
}
|
|
}
|
|
return String.fromCharCode(parseInt(ucode, 16));
|
|
}
|
|
return ch;
|
|
}
|
|
|
|
function getComplexString(opchar, opcharLen) {
|
|
var body = null;
|
|
var buf = '';
|
|
var placeables = 0;
|
|
var ch;
|
|
|
|
_index += opcharLen - 1;
|
|
|
|
var start = _index + 1;
|
|
|
|
walkChars:
|
|
while (true) {
|
|
ch = _source.charAt(++_index);
|
|
switch (ch) {
|
|
case '\\':
|
|
buf += _unescapeString();
|
|
break;
|
|
case '{':
|
|
/* We want to go to default unless {{ */
|
|
/* jshint -W086 */
|
|
if (_source.charAt(_index + 1) === '{') {
|
|
if (body === null) {
|
|
body = [];
|
|
}
|
|
if (placeables > MAX_PLACEABLES - 1) {
|
|
throw error('Too many placeables, maximum allowed is ' +
|
|
MAX_PLACEABLES);
|
|
}
|
|
if (buf) {
|
|
body.push({
|
|
type: 'String',
|
|
content: buf
|
|
});
|
|
}
|
|
_index += 2;
|
|
getWS();
|
|
body.push(getExpression());
|
|
getWS();
|
|
if (_source.charAt(_index) !== '}' ||
|
|
_source.charAt(_index + 1) !== '}') {
|
|
throw error('Expected "}}"');
|
|
}
|
|
_index += 1;
|
|
placeables++;
|
|
|
|
buf = '';
|
|
break;
|
|
}
|
|
default:
|
|
if (opcharLen === 1) {
|
|
if (ch === opchar) {
|
|
_index++;
|
|
break walkChars;
|
|
}
|
|
} else {
|
|
if (ch === opchar[0] &&
|
|
_source.charAt(_index + 1) === ch &&
|
|
_source.charAt(_index + 2) === ch) {
|
|
_index += 3;
|
|
break walkChars;
|
|
}
|
|
}
|
|
buf += ch;
|
|
if (_index + 1 >= _length) {
|
|
throw error('Unclosed string literal');
|
|
}
|
|
}
|
|
}
|
|
if (body === null) {
|
|
return {
|
|
type: 'String',
|
|
content: buf
|
|
};
|
|
}
|
|
if (buf.length) {
|
|
body.push({
|
|
type: 'String',
|
|
content: buf
|
|
});
|
|
}
|
|
return {
|
|
type: 'ComplexString',
|
|
content: body,
|
|
source: _source.slice(start, _index - opcharLen)
|
|
};
|
|
}
|
|
|
|
function getString(opchar, opcharLen) {
|
|
var opcharPos = _source.indexOf(opchar, _index + opcharLen);
|
|
var placeablePos, escPos, buf;
|
|
|
|
if (opcharPos === -1) {
|
|
throw error('Unclosed string literal');
|
|
}
|
|
buf = _source.slice(_index + opcharLen, opcharPos);
|
|
|
|
placeablePos = buf.indexOf('{{');
|
|
if (placeablePos !== -1) {
|
|
return getComplexString(opchar, opcharLen);
|
|
} else {
|
|
escPos = buf.indexOf('\\');
|
|
if (escPos !== -1) {
|
|
return getComplexString(opchar, opcharLen);
|
|
}
|
|
}
|
|
|
|
_index = opcharPos + opcharLen;
|
|
|
|
return {
|
|
type: 'String',
|
|
content: buf
|
|
};
|
|
}
|
|
|
|
function getValue(optional, ch) {
|
|
if (ch === undefined) {
|
|
ch = _source.charAt(_index);
|
|
}
|
|
if (ch === '\'' || ch === '"') {
|
|
if (ch === _source.charAt(_index + 1) &&
|
|
ch === _source.charAt(_index + 2)) {
|
|
return getString(ch + ch + ch, 3);
|
|
}
|
|
return getString(ch, 1);
|
|
}
|
|
if (ch === '{') {
|
|
return getHash();
|
|
}
|
|
if (!optional) {
|
|
throw error('Unknown value type');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
function getRequiredWS() {
|
|
var pos = _index;
|
|
var cc = _source.charCodeAt(pos);
|
|
// space, \n, \t, \r
|
|
while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
|
|
cc = _source.charCodeAt(++_index);
|
|
}
|
|
return _index !== pos;
|
|
}
|
|
|
|
function getWS() {
|
|
var cc = _source.charCodeAt(_index);
|
|
// space, \n, \t, \r
|
|
while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
|
|
cc = _source.charCodeAt(++_index);
|
|
}
|
|
}
|
|
|
|
function getVariable() {
|
|
++_index;
|
|
return {
|
|
type: 'VariableExpression',
|
|
id: getIdentifier()
|
|
};
|
|
}
|
|
|
|
function getIdentifier() {
|
|
var index = _index;
|
|
var start = index;
|
|
var source = _source;
|
|
var cc = source.charCodeAt(start);
|
|
|
|
// a-zA-Z_
|
|
if ((cc < 97 || cc > 122) && (cc < 65 || cc > 90) && cc !== 95) {
|
|
throw error('Identifier has to start with [a-zA-Z_]');
|
|
}
|
|
|
|
cc = source.charCodeAt(++index);
|
|
while ((cc >= 97 && cc <= 122) || // a-z
|
|
(cc >= 65 && cc <= 90) || // A-Z
|
|
(cc >= 48 && cc <= 57) || // 0-9
|
|
cc === 95) { // _
|
|
cc = source.charCodeAt(++index);
|
|
}
|
|
_index = index;
|
|
return {
|
|
type: 'Identifier',
|
|
name: source.slice(start, index)
|
|
};
|
|
}
|
|
|
|
function getImportStatement() {
|
|
_index += 6;
|
|
if (_source.charAt(_index) !== '(') {
|
|
throw error('Expected "("');
|
|
}
|
|
++_index;
|
|
getWS();
|
|
var uri = getString(_source.charAt(_index), 1);
|
|
getWS();
|
|
if (_source.charAt(_index) !== ')') {
|
|
throw error('Expected ")"');
|
|
}
|
|
++_index;
|
|
return {
|
|
type: 'ImportStatement',
|
|
uri: uri
|
|
};
|
|
}
|
|
|
|
function getMacro(id) {
|
|
if (id.name.charAt(0) === '_') {
|
|
throw error('Macro ID cannot start with "_"');
|
|
}
|
|
++_index;
|
|
var idlist = getItemList(getVariable, ')');
|
|
getRequiredWS();
|
|
|
|
if (_source.charAt(_index) !== '{') {
|
|
throw error('Expected "{"');
|
|
}
|
|
++_index;
|
|
getWS();
|
|
var exp = getExpression();
|
|
getWS();
|
|
if (_source.charAt(_index) !== '}') {
|
|
throw error('Expected "}"');
|
|
}
|
|
++_index;
|
|
getWS();
|
|
if (_source.charCodeAt(_index) !== 62) {
|
|
throw error('Expected ">"');
|
|
}
|
|
++_index;
|
|
return {
|
|
type: 'Macro',
|
|
id: id,
|
|
args: idlist,
|
|
expression: exp
|
|
};
|
|
}
|
|
|
|
function getEntity(id, index) {
|
|
if (!getRequiredWS()) {
|
|
throw error('Expected white space');
|
|
}
|
|
|
|
var ch = _source.charAt(_index);
|
|
var value = getValue(true, ch);
|
|
var attrs = null;
|
|
if (value === null) {
|
|
if (ch === '>') {
|
|
throw error('Expected ">"');
|
|
}
|
|
attrs = getAttributes();
|
|
} else {
|
|
var ws1 = getRequiredWS();
|
|
if (_source.charAt(_index) !== '>') {
|
|
if (!ws1) {
|
|
throw error('Expected ">"');
|
|
}
|
|
attrs = getAttributes();
|
|
}
|
|
}
|
|
|
|
// skip '>'
|
|
++_index;
|
|
return {
|
|
type: 'Entity',
|
|
id: id,
|
|
value: value,
|
|
index: index,
|
|
attrs: attrs,
|
|
local: (id.name.charCodeAt(0) === 95) // _
|
|
};
|
|
}
|
|
|
|
function getEntry() {
|
|
var cc = _source.charCodeAt(_index);
|
|
|
|
// 60 == '<'
|
|
if (cc === 60) {
|
|
++_index;
|
|
var id = getIdentifier();
|
|
cc = _source.charCodeAt(_index);
|
|
// 40 == '('
|
|
if (cc === 40) {
|
|
return getMacro(id);
|
|
}
|
|
// 91 == '['
|
|
if (cc === 91) {
|
|
++_index;
|
|
return getEntity(id,
|
|
getItemList(getExpression, ']'));
|
|
}
|
|
return getEntity(id, null);
|
|
}
|
|
// 47, 42 == '/*'
|
|
if (_source.charCodeAt(_index) === 47 &&
|
|
_source.charCodeAt(_index + 1) === 42) {
|
|
return getComment();
|
|
}
|
|
if (_source.slice(_index, _index + 6) === 'import') {
|
|
return getImportStatement();
|
|
}
|
|
throw error('Invalid entry');
|
|
}
|
|
|
|
function getLOLWithRecover() {
|
|
var entries = [];
|
|
|
|
getWS();
|
|
while (_index < _length) {
|
|
try {
|
|
entries.push(getEntry());
|
|
} catch (e) {
|
|
if (e instanceof ParserError) {
|
|
_emitter.emit('error', e);
|
|
entries.push(recover());
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
if (_index < _length) {
|
|
getWS();
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: 'LOL',
|
|
body: entries
|
|
};
|
|
}
|
|
|
|
function getLOLPlain() {
|
|
var entries = [];
|
|
|
|
getWS();
|
|
while (_index < _length) {
|
|
entries.push(getEntry());
|
|
if (_index < _length) {
|
|
getWS();
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: 'LOL',
|
|
body: entries
|
|
};
|
|
}
|
|
|
|
/* Public API functions */
|
|
|
|
function parse(string) {
|
|
_source = string;
|
|
_index = 0;
|
|
_length = _source.length;
|
|
|
|
return getLOL();
|
|
}
|
|
|
|
function addEvent(type, listener) {
|
|
if (!_emitter) {
|
|
throw new Error('Emitter not available');
|
|
}
|
|
return _emitter.addEventListener(type, listener);
|
|
}
|
|
|
|
function removeEvent(type, listener) {
|
|
if (!_emitter) {
|
|
throw new Error('Emitter not available');
|
|
}
|
|
return _emitter.removeEventListener(type, listener);
|
|
}
|
|
|
|
/* Expressions */
|
|
|
|
function getExpression() {
|
|
return getConditionalExpression();
|
|
}
|
|
|
|
function getPrefixExpression(token, cl, op, nxt) {
|
|
var exp = nxt();
|
|
var t, ch;
|
|
while (true) {
|
|
t = '';
|
|
getWS();
|
|
ch = _source.charAt(_index);
|
|
if (token[0].indexOf(ch) === -1) {
|
|
break;
|
|
}
|
|
t += ch;
|
|
++_index;
|
|
if (token.length > 1) {
|
|
ch = _source.charAt(_index);
|
|
if (token[1] === ch) {
|
|
++_index;
|
|
t += ch;
|
|
} else if (token[2]) {
|
|
--_index;
|
|
return exp;
|
|
}
|
|
}
|
|
getWS();
|
|
exp = {
|
|
type: cl,
|
|
operator: {
|
|
type: op,
|
|
token: t
|
|
},
|
|
left: exp,
|
|
right: nxt()
|
|
};
|
|
}
|
|
return exp;
|
|
}
|
|
|
|
function getPostfixExpression(token, cl, op, nxt) {
|
|
var cc = _source.charCodeAt(_index);
|
|
if (token.indexOf(cc) === -1) {
|
|
return nxt();
|
|
}
|
|
++_index;
|
|
getWS();
|
|
return {
|
|
type: cl,
|
|
operator: {
|
|
type: op,
|
|
token: String.fromCharCode(cc)
|
|
},
|
|
argument: getPostfixExpression(token, cl, op, nxt)
|
|
};
|
|
}
|
|
|
|
function getConditionalExpression() {
|
|
var exp = getOrExpression();
|
|
getWS();
|
|
if (_source.charCodeAt(_index) !== 63) { // ?
|
|
return exp;
|
|
}
|
|
++_index;
|
|
getWS();
|
|
var consequent = getExpression();
|
|
getWS();
|
|
if (_source.charCodeAt(_index) !== 58) { // :
|
|
throw error('Expected ":"');
|
|
}
|
|
++_index;
|
|
getWS();
|
|
return {
|
|
type: 'ConditionalExpression',
|
|
test: exp,
|
|
consequent: consequent,
|
|
alternate: getExpression()
|
|
};
|
|
}
|
|
|
|
function getOrExpression() {
|
|
return getPrefixExpression([['|'], '|', true],
|
|
'LogicalExpression',
|
|
'LogicalOperator',
|
|
getAndExpression);
|
|
}
|
|
|
|
function getAndExpression() {
|
|
return getPrefixExpression([['&'], '&', true],
|
|
'LogicalExpression',
|
|
'Logicalperator',
|
|
getEqualityExpression);
|
|
}
|
|
|
|
function getEqualityExpression() {
|
|
return getPrefixExpression([['=', '!'], '=', true],
|
|
'BinaryExpression',
|
|
'BinaryOperator',
|
|
getRelationalExpression);
|
|
}
|
|
|
|
function getRelationalExpression() {
|
|
return getPrefixExpression([['<', '>'], '=', false],
|
|
'BinaryExpression',
|
|
'BinaryOperator',
|
|
getAdditiveExpression);
|
|
}
|
|
|
|
function getAdditiveExpression() {
|
|
return getPrefixExpression([['+', '-']],
|
|
'BinaryExpression',
|
|
'BinaryOperator',
|
|
getModuloExpression);
|
|
}
|
|
|
|
function getModuloExpression() {
|
|
return getPrefixExpression([['%']],
|
|
'BinaryExpression',
|
|
'BinaryOperator',
|
|
getMultiplicativeExpression);
|
|
}
|
|
|
|
function getMultiplicativeExpression() {
|
|
return getPrefixExpression([['*']],
|
|
'BinaryExpression',
|
|
'BinaryOperator',
|
|
getDividiveExpression);
|
|
}
|
|
|
|
function getDividiveExpression() {
|
|
return getPrefixExpression([['/']],
|
|
'BinaryExpression',
|
|
'BinaryOperator',
|
|
getUnaryExpression);
|
|
}
|
|
|
|
function getUnaryExpression() {
|
|
return getPostfixExpression([43, 45, 33], // + - !
|
|
'UnaryExpression',
|
|
'UnaryOperator',
|
|
getMemberExpression);
|
|
}
|
|
|
|
function getCallExpression(callee) {
|
|
getWS();
|
|
return {
|
|
type: 'CallExpression',
|
|
callee: callee,
|
|
arguments: getItemList(getExpression, ')')
|
|
};
|
|
}
|
|
|
|
function getAttributeExpression(idref, computed) {
|
|
if (idref.type !== 'ParenthesisExpression' &&
|
|
idref.type !== 'Identifier' &&
|
|
idref.type !== 'ThisExpression') {
|
|
throw error('AttributeExpression must have Identifier, This or ' +
|
|
'Parenthesis as left node');
|
|
}
|
|
var exp;
|
|
if (computed) {
|
|
getWS();
|
|
exp = getExpression();
|
|
getWS();
|
|
if (_source.charAt(_index) !== ']') {
|
|
throw error('Expected "]"');
|
|
}
|
|
++_index;
|
|
return {
|
|
type: 'AttributeExpression',
|
|
expression: idref,
|
|
attribute: exp,
|
|
computed: true
|
|
};
|
|
}
|
|
exp = getIdentifier();
|
|
return {
|
|
type: 'AttributeExpression',
|
|
expression: idref,
|
|
attribute: exp,
|
|
computed: false
|
|
};
|
|
}
|
|
|
|
function getPropertyExpression(idref, computed) {
|
|
var exp;
|
|
if (computed) {
|
|
getWS();
|
|
exp = getExpression();
|
|
getWS();
|
|
if (_source.charAt(_index) !== ']') {
|
|
throw error('Expected "]"');
|
|
}
|
|
++_index;
|
|
return {
|
|
type: 'PropertyExpression',
|
|
expression: idref,
|
|
property: exp,
|
|
computed: true
|
|
};
|
|
}
|
|
exp = getIdentifier();
|
|
return {
|
|
type: 'PropertyExpression',
|
|
expression: idref,
|
|
property: exp,
|
|
computed: false
|
|
};
|
|
}
|
|
|
|
function getMemberExpression() {
|
|
var exp = getParenthesisExpression();
|
|
var cc;
|
|
|
|
// 46: '.'
|
|
// 40: '('
|
|
// 58: ':'
|
|
// 91: '['
|
|
while (true) {
|
|
cc = _source.charCodeAt(_index);
|
|
if (cc === 46 || cc === 91) { // . or [
|
|
++_index;
|
|
exp = getPropertyExpression(exp, cc === 91);
|
|
} else if (cc === 58 &&
|
|
_source.charCodeAt(_index + 1) === 58) { // ::
|
|
_index += 2;
|
|
if (_source.charCodeAt(_index) === 91) { // [
|
|
++_index;
|
|
exp = getAttributeExpression(exp, true);
|
|
} else {
|
|
exp = getAttributeExpression(exp, false);
|
|
}
|
|
} else if (cc === 40) { // (
|
|
++_index;
|
|
exp = getCallExpression(exp);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return exp;
|
|
}
|
|
|
|
function getParenthesisExpression() {
|
|
// 40 == (
|
|
if (_source.charCodeAt(_index) === 40) {
|
|
++_index;
|
|
getWS();
|
|
var pexp = {
|
|
type: 'ParenthesisExpression',
|
|
expression: getExpression()
|
|
};
|
|
getWS();
|
|
if (_source.charCodeAt(_index) !== 41) {
|
|
throw error('Expected ")"');
|
|
}
|
|
++_index;
|
|
return pexp;
|
|
}
|
|
return getPrimaryExpression();
|
|
}
|
|
|
|
function getPrimaryExpression() {
|
|
var pos = _index;
|
|
var cc = _source.charCodeAt(pos);
|
|
// number
|
|
while (cc > 47 && cc < 58) {
|
|
cc = _source.charCodeAt(++pos);
|
|
}
|
|
if (pos > _index) {
|
|
var start = _index;
|
|
_index = pos;
|
|
return {
|
|
type: 'Number',
|
|
value: parseInt(_source.slice(start, pos), 10)
|
|
};
|
|
}
|
|
|
|
switch (cc) {
|
|
// value: '"{[
|
|
case 39:
|
|
case 34:
|
|
case 123:
|
|
case 91:
|
|
return getValue();
|
|
|
|
// variable: $
|
|
case 36:
|
|
return getVariable();
|
|
|
|
// globals: @
|
|
case 64:
|
|
++_index;
|
|
return {
|
|
type: 'GlobalsExpression',
|
|
id: getIdentifier()
|
|
};
|
|
|
|
// this: ~
|
|
case 126:
|
|
++_index;
|
|
return {
|
|
type: 'ThisExpression'
|
|
};
|
|
|
|
default:
|
|
return getIdentifier();
|
|
}
|
|
}
|
|
|
|
/* helper functions */
|
|
|
|
function getItemList(callback, closeChar) {
|
|
var ch;
|
|
getWS();
|
|
if (_source.charAt(_index) === closeChar) {
|
|
++_index;
|
|
return [];
|
|
}
|
|
|
|
var items = [];
|
|
|
|
while (true) {
|
|
items.push(callback());
|
|
getWS();
|
|
ch = _source.charAt(_index);
|
|
if (ch === ',') {
|
|
++_index;
|
|
getWS();
|
|
} else if (ch === closeChar) {
|
|
++_index;
|
|
break;
|
|
} else {
|
|
throw error('Expected "," or "' + closeChar + '"');
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
function error(message, pos) {
|
|
if (pos === undefined) {
|
|
pos = _index;
|
|
}
|
|
var start = _source.lastIndexOf('<', pos - 1);
|
|
var lastClose = _source.lastIndexOf('>', pos - 1);
|
|
start = lastClose > start ? lastClose + 1 : start;
|
|
var context = _source.slice(start, pos + 10);
|
|
|
|
var msg = message + ' at pos ' + pos + ': "' + context + '"';
|
|
return new ParserError(msg, pos, context);
|
|
}
|
|
|
|
// This code is being called whenever we
|
|
// hit ParserError.
|
|
//
|
|
// The strategy here is to find the closest entry opening
|
|
// and skip forward to it.
|
|
//
|
|
// It may happen that the entry opening is in fact part of expression,
|
|
// but this should just trigger another ParserError on the next char
|
|
// and we'll have to scan for entry opening again until we're successful
|
|
// or we run out of entry openings in the code.
|
|
function recover() {
|
|
var opening = _source.indexOf('<', _index);
|
|
var junk;
|
|
if (opening === -1) {
|
|
junk = {
|
|
'type': 'JunkEntry',
|
|
'content': _source.slice(_index)
|
|
};
|
|
_index = _length;
|
|
return junk;
|
|
}
|
|
junk = {
|
|
'type': 'JunkEntry',
|
|
'content': _source.slice(_index, opening)
|
|
};
|
|
_index = opening;
|
|
return junk;
|
|
}
|
|
}
|
|
|
|
/* ParserError class */
|
|
|
|
Parser.Error = ParserError;
|
|
|
|
function ParserError(message, pos, context) {
|
|
this.name = 'ParserError';
|
|
this.message = message;
|
|
this.pos = pos;
|
|
this.context = context;
|
|
}
|
|
ParserError.prototype = Object.create(Error.prototype);
|
|
ParserError.prototype.constructor = ParserError;
|
|
|
|
exports.Parser = Parser;
|
|
|
|
});
|
|
// This is L20n's on-the-fly compiler. It takes the AST produced by the parser
|
|
// and uses it to create a set of JavaScript objects and functions representing
|
|
// entities and macros and other expressions.
|
|
//
|
|
// The module defines a `Compiler` singleton with a single method: `compile`.
|
|
// The result of the compilation is stored on the `entries` object passed as
|
|
// the second argument to the `compile` function. The third argument is
|
|
// `globals`, an object whose properties provide information about the runtime
|
|
// environment, e.g., the current hour, operating system etc.
|
|
//
|
|
// Main concepts
|
|
// -------------
|
|
//
|
|
// **Entities** and **attributes** are objects which are publicly available.
|
|
// Their `toString` method is designed to be used by the L20n context to get
|
|
// a string value of the entity, given the context data passed to the method.
|
|
//
|
|
// All other symbols defined by the grammar are implemented as expression
|
|
// functions. The naming convention is:
|
|
//
|
|
// - capitalized first letters denote **expressions constructors**, e.g.
|
|
// `PropertyExpression`.
|
|
// - camel-case denotes **expression functions** returned by the
|
|
// constructors, e.g. `propertyExpression`.
|
|
//
|
|
// ### Constructors
|
|
//
|
|
// The constructor is called for every node in the AST. It stores the
|
|
// components of the expression which are constant and do not depend on the
|
|
// calling context (an example of the latter would be the data passed by the
|
|
// developer to the `toString` method).
|
|
//
|
|
// ### Expression functions
|
|
//
|
|
// The constructor, when called, returns an expression function, which, in
|
|
// turn, is called every time the expression needs to be evaluated. The
|
|
// evaluation call is context-dependend. Every expression function takes two
|
|
// mandatory arguments and one optional one:
|
|
//
|
|
// - `locals`, which stores the information about the currently evaluated
|
|
// entity (`locals.__this__`). It also stores the arguments passed to macros.
|
|
// - `ctxdata`, which is an object with data passed to the context by the
|
|
// developer. The developer can define data on the context, or pass it on
|
|
// a per-call basis.
|
|
// - `key` (optional), which is a number or a string passed to a `HashLiteral`
|
|
// expression denoting the member of the hash to return. The member will be
|
|
// another expression function which can then be evaluated further.
|
|
//
|
|
//
|
|
// Bubbling up the new _current_ entity
|
|
// ------------------------------------
|
|
//
|
|
// Every expression function returns an array [`newLocals`, `evaluatedValue`].
|
|
// The reason for this, and in particular for returning `newLocals`, is
|
|
// important for understanding how the compiler works.
|
|
//
|
|
// In most of the cases. `newLocals` will be the same as the original `locals`
|
|
// passed to the expression function during the evaluation call. In some
|
|
// cases, however, `newLocals.__this__` will reference a different entity than
|
|
// `locals.__this__` did. On runtime, as the compiler traverses the AST and
|
|
// goes deeper into individual branches, when it hits an `identifier` and
|
|
// evaluates it to an entity, it needs to **bubble up** this find back to the
|
|
// top expressions in the chain. This is so that the evaluation of the
|
|
// top-most expressions in the branch (root being at the very top of the tree)
|
|
// takes into account the new value of `__this__`.
|
|
//
|
|
// To illustrate this point, consider the following example.
|
|
//
|
|
// Two entities, `brandName` and `about` are defined as such:
|
|
//
|
|
// <brandName {
|
|
// short: "Firefox",
|
|
// long: "Mozilla {{ ~ }}"
|
|
// }>
|
|
// <about "About {{ brandName.long }}">
|
|
//
|
|
// Notice two `complexString`s: `about` references `brandName.long`, and
|
|
// `brandName.long` references its own entity via `~`. This `~` (meaning, the
|
|
// current entity) must always reference `brandName`, even when called from
|
|
// `about`.
|
|
//
|
|
// The AST for the `about` entity looks like this:
|
|
//
|
|
// [Entity]
|
|
// .id[Identifier]
|
|
// .name[unicode "about"]
|
|
// .index
|
|
// .value[ComplexString] <1>
|
|
// .content
|
|
// [String] <2>
|
|
// .content[unicode "About "]
|
|
// [PropertyExpression] <3>
|
|
// .expression[Identifier] <4>
|
|
// .name[unicode "brandName"]
|
|
// .property[Identifier]
|
|
// .name[unicode "long"]
|
|
// .computed[bool=False]
|
|
// .attrs
|
|
// .local[bool=False]
|
|
//
|
|
// During the compilation the compiler will walk the AST top-down to the
|
|
// deepest terminal leaves and will use expression constructors to create
|
|
// expression functions for the components. For instance, for `about`'s value,
|
|
// the compiler will call `ComplexString()` to create an expression function
|
|
// `complexString` <1> which will be assigned to the entity's value. The
|
|
// `ComplexString` construtor, before it returns the `complexString` <1>, will
|
|
// in turn call other expression constructors to create `content`:
|
|
// a `stringLiteral` and a `propertyExpression`. The `PropertyExpression`
|
|
// contructor will do the same, etc...
|
|
//
|
|
// When `entity.getString(ctxdata)` is called by a third-party code, we need to
|
|
// resolve the whole `complexString` <1> to return a single string value. This
|
|
// is what **resolving** means and it involves some recursion. On the other
|
|
// hand, **evaluating** means _to call the expression once and use what it
|
|
// returns_.
|
|
//
|
|
// The identifier expression sets `locals.__this__` to the current entity,
|
|
// `about`, and tells the `complexString` <1> to _resolve_ itself.
|
|
//
|
|
// In order to resolve the `complexString` <1>, we start by resolving its first
|
|
// member <2> to a string. As we resolve deeper down, we bubble down `locals`
|
|
// set by `toString`. The first member of `content` turns out to simply be
|
|
// a string that reads `About `.
|
|
//
|
|
// On to the second member, the propertyExpression <3>. We bubble down
|
|
// `locals` again and proceed to evaluate the `expression` field, which is an
|
|
// `identifier`. Note that we don't _resolve_ it to a string; we _evaluate_ it
|
|
// to something that can be further used in other expressions, in this case, an
|
|
// **entity** called `brandName`.
|
|
//
|
|
// Had we _resolved_ the `propertyExpression`, it would have resolve to
|
|
// a string, and it would have been impossible to access the `long` member.
|
|
// This leads us to an important concept: the compiler _resolves_ expressions
|
|
// when it expects a primitive value (a string, a number, a bool). On the
|
|
// other hand, it _evaluates_ expressions (calls them only once) when it needs
|
|
// to work with them further, e.g. in order to access a member of the hash.
|
|
//
|
|
// This also explains why in the above example, once the compiler hits the
|
|
// `brandName` identifier and changes the value of `locals.__this__` to the
|
|
// `brandName` entity, this value doesn't bubble up all the way up to the
|
|
// `about` entity. All components of any `complexString` are _resolved_ by the
|
|
// compiler until a primitive value is returned. This logic lives in the
|
|
// `_resolve` function.
|
|
|
|
define('l20n/compiler', function(require, exports) {
|
|
// TODO change newcap to true?
|
|
/* jshint strict: false, newcap: false */
|
|
var EventEmitter = require('./events').EventEmitter;
|
|
|
|
function Compiler() {
|
|
|
|
// Public
|
|
|
|
this.compile = compile;
|
|
this.setGlobals = setGlobals;
|
|
this.addEventListener = addEvent;
|
|
this.removeEventListener = removeEvent;
|
|
|
|
// Private
|
|
|
|
var MAX_PLACEABLE_LENGTH = 2500;
|
|
|
|
var _emitter = new EventEmitter();
|
|
var _globals = null;
|
|
var _references = {
|
|
globals: {}
|
|
};
|
|
|
|
var _entryTypes = {
|
|
Entity: Entity,
|
|
Macro: Macro
|
|
};
|
|
|
|
// Public API functions
|
|
|
|
function compile(ast, env) {
|
|
if (!env) {
|
|
env = {};
|
|
}
|
|
for (var i = 0, entry; entry = ast.body[i]; i++) {
|
|
var Constructor = _entryTypes[entry.type];
|
|
if (Constructor) {
|
|
try {
|
|
env[entry.id.name] = new Constructor(entry, env);
|
|
} catch (e) {
|
|
// rethrow non-compiler errors;
|
|
requireCompilerError(e);
|
|
// or, just ignore the error; it's been already emitted
|
|
}
|
|
}
|
|
}
|
|
return env;
|
|
}
|
|
|
|
function setGlobals(globals) {
|
|
_globals = globals;
|
|
return true;
|
|
}
|
|
|
|
function addEvent(type, listener) {
|
|
return _emitter.addEventListener(type, listener);
|
|
}
|
|
|
|
function removeEvent(type, listener) {
|
|
return _emitter.removeEventListener(type, listener);
|
|
}
|
|
|
|
// utils
|
|
|
|
function emit(Ctor, message, entry, source) {
|
|
var e = new Ctor(message, entry, source);
|
|
_emitter.emit('error', e);
|
|
return e;
|
|
}
|
|
|
|
// The Entity object.
|
|
function Entity(node, env) {
|
|
this.id = node.id.name;
|
|
this.env = env;
|
|
this.local = node.local || false;
|
|
this.index = null;
|
|
this.attributes = null;
|
|
this.publicAttributes = null;
|
|
var i;
|
|
if (node.index) {
|
|
this.index = [];
|
|
for (i = 0; i < node.index.length; i++) {
|
|
this.index.push(IndexExpression(node.index[i], this));
|
|
}
|
|
}
|
|
if (node.attrs) {
|
|
this.attributes = {};
|
|
this.publicAttributes = [];
|
|
for (i = 0; i < node.attrs.length; i++) {
|
|
var attr = node.attrs[i];
|
|
this.attributes[attr.key.name] = new Attribute(attr, this);
|
|
if (!attr.local) {
|
|
this.publicAttributes.push(attr.key.name);
|
|
}
|
|
}
|
|
}
|
|
// Bug 817610 - Optimize a fast path for String entities in the Compiler
|
|
if (node.value && node.value.type === 'String') {
|
|
this.value = node.value.content;
|
|
} else {
|
|
this.value = LazyExpression(node.value, this, this.index);
|
|
}
|
|
}
|
|
|
|
Entity.prototype.getString = function E_getString(ctxdata) {
|
|
try {
|
|
var locals = {
|
|
__this__: this,
|
|
__env__: this.env
|
|
};
|
|
return _resolve(this.value, locals, ctxdata);
|
|
} catch (e) {
|
|
requireCompilerError(e);
|
|
// `ValueErrors` are not emitted in `StringLiteral` where they are
|
|
// created, because if the string in question is being evaluated in an
|
|
// index, we'll emit an `IndexError` instead. To avoid duplication,
|
|
// `ValueErrors` are only be emitted if they actually make it to
|
|
// here. See `IndexExpression` for an example of why they wouldn't.
|
|
if (e instanceof ValueError) {
|
|
_emitter.emit('error', e);
|
|
}
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
Entity.prototype.get = function E_get(ctxdata) {
|
|
// reset `_references` to an empty state
|
|
_references.globals = {};
|
|
// evaluate the entity and its attributes; if any globals are used in
|
|
// the process, `toString` will populate `_references.globals`
|
|
// accordingly.
|
|
var entity = {
|
|
value: this.getString(ctxdata),
|
|
attributes: {}
|
|
};
|
|
if (this.publicAttributes) {
|
|
entity.attributes = {};
|
|
for (var i = 0, attr; attr = this.publicAttributes[i]; i++) {
|
|
entity.attributes[attr] = this.attributes[attr].getString(ctxdata);
|
|
}
|
|
}
|
|
entity.globals = _references.globals;
|
|
return entity;
|
|
};
|
|
|
|
|
|
function Attribute(node, entity) {
|
|
this.key = node.key.name;
|
|
this.local = node.local || false;
|
|
this.index = null;
|
|
if (node.index) {
|
|
this.index = [];
|
|
for (var i = 0; i < node.index.length; i++) {
|
|
this.index.push(IndexExpression(node.index[i], this));
|
|
}
|
|
}
|
|
if (node.value && node.value.type === 'String') {
|
|
this.value = node.value.content;
|
|
} else {
|
|
this.value = LazyExpression(node.value, entity, this.index);
|
|
}
|
|
this.entity = entity;
|
|
}
|
|
|
|
Attribute.prototype.getString = function A_getString(ctxdata) {
|
|
try {
|
|
var locals = {
|
|
__this__: this.entity,
|
|
__env__: this.entity.env
|
|
};
|
|
return _resolve(this.value, locals, ctxdata);
|
|
} catch (e) {
|
|
requireCompilerError(e);
|
|
if (e instanceof ValueError) {
|
|
_emitter.emit('error', e);
|
|
}
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
function Macro(node, env) {
|
|
this.id = node.id.name;
|
|
this.env = env;
|
|
this.local = node.local || false;
|
|
this.expression = LazyExpression(node.expression, this);
|
|
this.args = node.args;
|
|
}
|
|
Macro.prototype._call = function M_call(args, ctxdata) {
|
|
var locals = {
|
|
__this__: this,
|
|
__env__: this.env
|
|
};
|
|
// the number of arguments passed must equal the macro's arity
|
|
if (this.args.length !== args.length) {
|
|
throw new RuntimeError(this.id + '() takes exactly ' +
|
|
this.args.length + ' argument(s) (' +
|
|
args.length + ' given)');
|
|
}
|
|
for (var i = 0; i < this.args.length; i++) {
|
|
locals[this.args[i].id.name] = args[i];
|
|
}
|
|
var final = this.expression(locals, ctxdata);
|
|
locals = final[0];
|
|
final = final[1];
|
|
return [locals, _resolve(final, locals, ctxdata)];
|
|
};
|
|
|
|
|
|
var EXPRESSION_TYPES = {
|
|
// Primary expressions.
|
|
'Identifier': Identifier,
|
|
'ThisExpression': ThisExpression,
|
|
'VariableExpression': VariableExpression,
|
|
'GlobalsExpression': GlobalsExpression,
|
|
|
|
// Value expressions.
|
|
'Number': NumberLiteral,
|
|
'String': StringLiteral,
|
|
'Hash': HashLiteral,
|
|
'HashItem': Expression,
|
|
'ComplexString': ComplexString,
|
|
|
|
// Logical expressions.
|
|
'UnaryExpression': UnaryExpression,
|
|
'BinaryExpression': BinaryExpression,
|
|
'LogicalExpression': LogicalExpression,
|
|
'ConditionalExpression': ConditionalExpression,
|
|
|
|
// Member expressions.
|
|
'CallExpression': CallExpression,
|
|
'PropertyExpression': PropertyExpression,
|
|
'AttributeExpression': AttributeExpression,
|
|
'ParenthesisExpression': ParenthesisExpression
|
|
};
|
|
|
|
// The 'dispatcher' expression constructor. Other expression constructors
|
|
// call this to create expression functions for their components. For
|
|
// instance, `ConditionalExpression` calls `Expression` to create
|
|
// expression functions for its `test`, `consequent` and `alternate`
|
|
// symbols.
|
|
function Expression(node, entry, index) {
|
|
// An entity can have no value. It will be resolved to `null`.
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
if (!EXPRESSION_TYPES[node.type]) {
|
|
throw emit('CompilationError', 'Unknown expression type' + node.type);
|
|
}
|
|
if (index) {
|
|
index = index.slice();
|
|
}
|
|
return EXPRESSION_TYPES[node.type](node, entry, index);
|
|
}
|
|
|
|
function LazyExpression(node, entry, index) {
|
|
// An entity can have no value. It will be resolved to `null`.
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
var expr;
|
|
return function(locals, ctxdata, prop) {
|
|
if (!expr) {
|
|
expr = Expression(node, entry, index);
|
|
}
|
|
return expr(locals, ctxdata, prop);
|
|
};
|
|
}
|
|
|
|
function _resolve(expr, locals, ctxdata) {
|
|
// Bail out early if it's a primitive value or `null`. This is exactly
|
|
// what we want.
|
|
if (typeof expr === 'string' ||
|
|
typeof expr === 'boolean' ||
|
|
typeof expr === 'number' ||
|
|
!expr) {
|
|
return expr;
|
|
}
|
|
|
|
// Check if `expr` is an Entity or an Attribute
|
|
if (expr.value !== undefined) {
|
|
return _resolve(expr.value, locals, ctxdata);
|
|
}
|
|
|
|
// Check if `expr` is an expression
|
|
if (typeof expr === 'function') {
|
|
var current = expr(locals, ctxdata);
|
|
locals = current[0];
|
|
current = current[1];
|
|
return _resolve(current, locals, ctxdata);
|
|
}
|
|
|
|
// Throw if `expr` is a macro
|
|
if (expr.expression) {
|
|
throw new RuntimeError('Uncalled macro: ' + expr.id);
|
|
}
|
|
|
|
// Throw if `expr` is a non-primitive from ctxdata or a global
|
|
throw new RuntimeError('Cannot resolve ctxdata or global of type ' +
|
|
typeof expr);
|
|
|
|
}
|
|
|
|
function Identifier(node) {
|
|
var name = node.name;
|
|
return function identifier(locals) {
|
|
if (!locals.__env__.hasOwnProperty(name)) {
|
|
throw new RuntimeError('Reference to an unknown entry: ' + name);
|
|
}
|
|
// The only thing we care about here is the new `__this__` so we
|
|
// discard any other local variables. Note that because this is an
|
|
// assignment to a local variable, the original `locals` passed is not
|
|
// changed.
|
|
locals = {
|
|
__this__: locals.__env__[name],
|
|
__env__: locals.__env__
|
|
};
|
|
return [locals, locals.__this__];
|
|
};
|
|
}
|
|
function ThisExpression() {
|
|
return function thisExpression(locals) {
|
|
return [locals, locals.__this__];
|
|
};
|
|
}
|
|
function VariableExpression(node) {
|
|
var name = node.id.name;
|
|
return function variableExpression(locals, ctxdata) {
|
|
if (locals.hasOwnProperty(name)) {
|
|
// locals[name] is already a [locals, value] tuple on its own
|
|
return locals[name];
|
|
}
|
|
if (!ctxdata || !ctxdata.hasOwnProperty(name)) {
|
|
throw new RuntimeError('Reference to an unknown variable: ' + name);
|
|
}
|
|
return [locals, ctxdata[name]];
|
|
};
|
|
}
|
|
function GlobalsExpression(node) {
|
|
var name = node.id.name;
|
|
return function globalsExpression(locals) {
|
|
if (!_globals) {
|
|
throw new RuntimeError('No globals set (tried @' + name + ')');
|
|
}
|
|
if (!_globals.hasOwnProperty(name)) {
|
|
throw new RuntimeError('Reference to an unknown global: ' + name);
|
|
}
|
|
var value;
|
|
try {
|
|
value = _globals[name].get();
|
|
} catch (e) {
|
|
throw new RuntimeError('Cannot evaluate global ' + name);
|
|
}
|
|
_references.globals[name] = true;
|
|
return [locals, value];
|
|
};
|
|
}
|
|
function NumberLiteral(node) {
|
|
return function numberLiteral(locals) {
|
|
return [locals, node.value];
|
|
};
|
|
}
|
|
function StringLiteral(node) {
|
|
return function stringLiteral(locals, ctxdata, key) {
|
|
// if a key was passed, throw; checking arguments is more reliable
|
|
// than testing the value of key because if the key comes from context
|
|
// data it can be any type, also undefined
|
|
if (key !== undefined) {
|
|
throw new RuntimeError('Cannot get property of a string: ' + key);
|
|
}
|
|
return [locals, node.content];
|
|
};
|
|
}
|
|
|
|
function ComplexString(node, entry) {
|
|
var content = [];
|
|
for (var i = 0; i < node.content.length; i++) {
|
|
content.push(Expression(node.content[i], entry));
|
|
}
|
|
|
|
// Every complexString needs to have its own `dirty` flag whose state
|
|
// persists across multiple calls to the given complexString. On the
|
|
// other hand, `dirty` must not be shared by all complexStrings. Hence
|
|
// the need to define `dirty` as a variable available in the closure.
|
|
// Note that the anonymous function is a self-invoked one and it returns
|
|
// the closure immediately.
|
|
return (function() {
|
|
var dirty = false;
|
|
return function complexString(locals, ctxdata, key) {
|
|
if (key !== undefined) {
|
|
throw new RuntimeError('Cannot get property of a string: ' + key);
|
|
}
|
|
if (dirty) {
|
|
throw new RuntimeError('Cyclic reference detected');
|
|
}
|
|
dirty = true;
|
|
var parts = [];
|
|
try {
|
|
for (var i = 0; i < content.length; i++) {
|
|
var part = _resolve(content[i], locals, ctxdata);
|
|
if (typeof part !== 'string' && typeof part !== 'number') {
|
|
throw new RuntimeError('Placeables must be strings or ' +
|
|
'numbers');
|
|
}
|
|
if (part.length > MAX_PLACEABLE_LENGTH) {
|
|
throw new RuntimeError('Placeable has too many characters, ' +
|
|
'maximum allowed is ' +
|
|
MAX_PLACEABLE_LENGTH);
|
|
}
|
|
parts.push(part);
|
|
}
|
|
} catch (e) {
|
|
requireCompilerError(e);
|
|
// only throw, don't emit yet. If the `ValueError` makes it to
|
|
// `getString()` it will be emitted there. It might, however, be
|
|
// cought by `IndexExpression` and changed into a `IndexError`.
|
|
// See `IndexExpression` for more explanation.
|
|
throw new ValueError(e.message, entry, node.source);
|
|
} finally {
|
|
dirty = false;
|
|
}
|
|
return [locals, parts.join('')];
|
|
};
|
|
})();
|
|
}
|
|
|
|
function IndexExpression(node, entry) {
|
|
var expression = Expression(node, entry);
|
|
|
|
// This is analogous to `ComplexString` in that an individual index can
|
|
// only be visited once during the resolution of an Entity. `dirty` is
|
|
// set in a closure context of the returned function.
|
|
return (function() {
|
|
var dirty = false;
|
|
return function indexExpression(locals, ctxdata) {
|
|
if (dirty) {
|
|
throw new RuntimeError('Cyclic reference detected');
|
|
}
|
|
dirty = true;
|
|
var retval;
|
|
try {
|
|
// We need to resolve `expression` here so that we catch errors
|
|
// thrown deep within. Without `_resolve` we might end up with an
|
|
// unresolved Entity object, and no "Cyclic reference detected"
|
|
// error would be thown.
|
|
retval = _resolve(expression, locals, ctxdata);
|
|
} catch (e) {
|
|
// If it's an `IndexError` thrown deeper within `expression`, it
|
|
// has already been emitted by its `indexExpression`. We can
|
|
// safely re-throw it here.
|
|
if (e instanceof IndexError) {
|
|
throw e;
|
|
}
|
|
|
|
// Otherwise, make sure it's a `RuntimeError` or a `ValueError` and
|
|
// throw and emit an `IndexError`.
|
|
//
|
|
// If it's a `ValueError` we want to replace it by an `IndexError`
|
|
// here so that `ValueErrors` from the index don't make their way
|
|
// up to the context. The context only cares about ValueErrors
|
|
// thrown by the value of the entity it has requested, not entities
|
|
// used in the index.
|
|
//
|
|
// To illustrate this point with an example, consider the following
|
|
// two strings, where `foo` is a missing entity.
|
|
//
|
|
// <prompt1["remove"] {
|
|
// remove: "Remove {{ foo }}?",
|
|
// keep: "Keep {{ foo }}?"
|
|
// }>
|
|
//
|
|
// `prompt1` will throw a `ValueError`. The context can use it to
|
|
// display the source of the entity, i.e. `Remove {{ foo }}?`. The
|
|
// index resolved properly, so at least we know that we're showing
|
|
// the right variant of the entity.
|
|
//
|
|
// <prompt2["{{ foo }}"] {
|
|
// remove: "Remove file?",
|
|
// keep: "Keep file?"
|
|
// }>
|
|
//
|
|
// On the other hand, `prompt2` will throw an `IndexError`. This
|
|
// is a more serious scenario for the context. We should not
|
|
// assume that we know which variant to show to the user. In fact,
|
|
// in the above (much contrived, but still) example, showing the
|
|
// incorrect variant will likely lead to data loss. The context
|
|
// should be more strict in this case and should not try to recover
|
|
// from this error too hard.
|
|
requireCompilerError(e);
|
|
throw emit(IndexError, e.message, entry);
|
|
} finally {
|
|
dirty = false;
|
|
}
|
|
return [locals, retval];
|
|
};
|
|
})();
|
|
}
|
|
|
|
function HashLiteral(node, entry, index) {
|
|
var content = {};
|
|
// if absent, `defaultKey` and `defaultIndex` are undefined
|
|
var defaultKey;
|
|
var defaultIndex = index ? index.shift() : undefined;
|
|
for (var i = 0; i < node.content.length; i++) {
|
|
var elem = node.content[i];
|
|
// use `elem.value` to skip `HashItem` and create the value right away
|
|
content[elem.key.name] = Expression(elem.value, entry, index);
|
|
if (elem.default) {
|
|
defaultKey = elem.key.name;
|
|
}
|
|
}
|
|
return function hashLiteral(locals, ctxdata, prop) {
|
|
var keysToTry = [prop, defaultIndex, defaultKey];
|
|
var keysTried = [];
|
|
for (var i = 0; i < keysToTry.length; i++) {
|
|
var key = _resolve(keysToTry[i], locals, ctxdata);
|
|
if (key === undefined) {
|
|
continue;
|
|
}
|
|
if (typeof key !== 'string') {
|
|
throw emit(IndexError, 'Index must be a string', entry);
|
|
}
|
|
keysTried.push(key);
|
|
if (content.hasOwnProperty(key)) {
|
|
return [locals, content[key]];
|
|
}
|
|
}
|
|
|
|
// If no valid key was found, throw an `IndexError`
|
|
var message;
|
|
if (keysTried.length) {
|
|
message = 'Hash key lookup failed ' +
|
|
'(tried "' + keysTried.join('", "') + '").';
|
|
} else {
|
|
message = 'Hash key lookup failed.';
|
|
}
|
|
throw emit(IndexError, message, entry);
|
|
};
|
|
}
|
|
|
|
|
|
function UnaryOperator(token, entry) {
|
|
if (token === '-') return function negativeOperator(argument) {
|
|
if (typeof argument !== 'number') {
|
|
throw new RuntimeError('The unary - operator takes a number');
|
|
}
|
|
return -argument;
|
|
};
|
|
if (token === '+') return function positiveOperator(argument) {
|
|
if (typeof argument !== 'number') {
|
|
throw new RuntimeError('The unary + operator takes a number');
|
|
}
|
|
return +argument;
|
|
};
|
|
if (token === '!') return function notOperator(argument) {
|
|
if (typeof argument !== 'boolean') {
|
|
throw new RuntimeError('The ! operator takes a boolean');
|
|
}
|
|
return !argument;
|
|
};
|
|
throw emit(CompilationError, 'Unknown token: ' + token, entry);
|
|
}
|
|
function BinaryOperator(token, entry) {
|
|
if (token === '==') return function equalOperator(left, right) {
|
|
if ((typeof left !== 'number' || typeof right !== 'number') &&
|
|
(typeof left !== 'string' || typeof right !== 'string')) {
|
|
throw new RuntimeError('The == operator takes two numbers or ' +
|
|
'two strings');
|
|
}
|
|
return left === right;
|
|
};
|
|
if (token === '!=') return function notEqualOperator(left, right) {
|
|
if ((typeof left !== 'number' || typeof right !== 'number') &&
|
|
(typeof left !== 'string' || typeof right !== 'string')) {
|
|
throw new RuntimeError('The != operator takes two numbers or ' +
|
|
'two strings');
|
|
}
|
|
return left !== right;
|
|
};
|
|
if (token === '<') return function lessThanOperator(left, right) {
|
|
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
throw new RuntimeError('The < operator takes two numbers');
|
|
}
|
|
return left < right;
|
|
};
|
|
if (token === '<=') return function lessThanEqualOperator(left, right) {
|
|
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
throw new RuntimeError('The <= operator takes two numbers');
|
|
}
|
|
return left <= right;
|
|
};
|
|
if (token === '>') return function greaterThanOperator(left, right) {
|
|
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
throw new RuntimeError('The > operator takes two numbers');
|
|
}
|
|
return left > right;
|
|
};
|
|
if (token === '>=') {
|
|
return function greaterThanEqualOperator(left, right) {
|
|
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
throw new RuntimeError('The >= operator takes two numbers');
|
|
}
|
|
return left >= right;
|
|
};
|
|
}
|
|
if (token === '+') return function addOperator(left, right) {
|
|
if ((typeof left !== 'number' || typeof right !== 'number') &&
|
|
(typeof left !== 'string' || typeof right !== 'string')) {
|
|
throw new RuntimeError('The + operator takes two numbers or ' +
|
|
'two strings');
|
|
}
|
|
return left + right;
|
|
};
|
|
if (token === '-') return function substractOperator(left, right) {
|
|
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
throw new RuntimeError('The - operator takes two numbers');
|
|
}
|
|
return left - right;
|
|
};
|
|
if (token === '*') return function multiplyOperator(left, right) {
|
|
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
throw new RuntimeError('The * operator takes two numbers');
|
|
}
|
|
return left * right;
|
|
};
|
|
if (token === '/') return function devideOperator(left, right) {
|
|
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
throw new RuntimeError('The / operator takes two numbers');
|
|
}
|
|
if (right === 0) {
|
|
throw new RuntimeError('Division by zero not allowed.');
|
|
}
|
|
return left / right;
|
|
};
|
|
if (token === '%') return function moduloOperator(left, right) {
|
|
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
throw new RuntimeError('The % operator takes two numbers');
|
|
}
|
|
if (right === 0) {
|
|
throw new RuntimeError('Modulo zero not allowed.');
|
|
}
|
|
return left % right;
|
|
};
|
|
throw emit(CompilationError, 'Unknown token: ' + token, entry);
|
|
}
|
|
function LogicalOperator(token, entry) {
|
|
if (token === '&&') return function andOperator(left, right) {
|
|
if (typeof left !== 'boolean' || typeof right !== 'boolean') {
|
|
throw new RuntimeError('The && operator takes two booleans');
|
|
}
|
|
return left && right;
|
|
};
|
|
if (token === '||') return function orOperator(left, right) {
|
|
if (typeof left !== 'boolean' || typeof right !== 'boolean') {
|
|
throw new RuntimeError('The || operator takes two booleans');
|
|
}
|
|
return left || right;
|
|
};
|
|
throw emit(CompilationError, 'Unknown token: ' + token, entry);
|
|
}
|
|
function UnaryExpression(node, entry) {
|
|
var operator = UnaryOperator(node.operator.token, entry);
|
|
var argument = Expression(node.argument, entry);
|
|
return function unaryExpression(locals, ctxdata) {
|
|
return [locals, operator(_resolve(argument, locals, ctxdata))];
|
|
};
|
|
}
|
|
function BinaryExpression(node, entry) {
|
|
var left = Expression(node.left, entry);
|
|
var operator = BinaryOperator(node.operator.token, entry);
|
|
var right = Expression(node.right, entry);
|
|
return function binaryExpression(locals, ctxdata) {
|
|
return [locals, operator(
|
|
_resolve(left, locals, ctxdata),
|
|
_resolve(right, locals, ctxdata)
|
|
)];
|
|
};
|
|
}
|
|
function LogicalExpression(node, entry) {
|
|
var left = Expression(node.left, entry);
|
|
var operator = LogicalOperator(node.operator.token, entry);
|
|
var right = Expression(node.right, entry);
|
|
return function logicalExpression(locals, ctxdata) {
|
|
return [locals, operator(
|
|
_resolve(left, locals, ctxdata),
|
|
_resolve(right, locals, ctxdata)
|
|
)];
|
|
};
|
|
}
|
|
function ConditionalExpression(node, entry) {
|
|
var test = Expression(node.test, entry);
|
|
var consequent = Expression(node.consequent, entry);
|
|
var alternate = Expression(node.alternate, entry);
|
|
return function conditionalExpression(locals, ctxdata) {
|
|
var tested = _resolve(test, locals, ctxdata);
|
|
if (typeof tested !== 'boolean') {
|
|
throw new RuntimeError('Conditional expressions must test a ' +
|
|
'boolean');
|
|
}
|
|
if (tested === true) {
|
|
return consequent(locals, ctxdata);
|
|
}
|
|
return alternate(locals, ctxdata);
|
|
};
|
|
}
|
|
|
|
function CallExpression(node, entry) {
|
|
var callee = Expression(node.callee, entry);
|
|
var args = [];
|
|
for (var i = 0; i < node.arguments.length; i++) {
|
|
args.push(Expression(node.arguments[i], entry));
|
|
}
|
|
return function callExpression(locals, ctxdata) {
|
|
var evaluated_args = [];
|
|
for (var i = 0; i < args.length; i++) {
|
|
evaluated_args.push(args[i](locals, ctxdata));
|
|
}
|
|
// callee is an expression pointing to a macro, e.g. an identifier
|
|
var macro = callee(locals, ctxdata);
|
|
locals = macro[0];
|
|
macro = macro[1];
|
|
if (!macro.expression) {
|
|
throw new RuntimeError('Expected a macro, got a non-callable.');
|
|
}
|
|
// Rely entirely on the platform implementation to detect recursion.
|
|
// `Macro::_call` assigns `evaluated_args` to members of `locals`.
|
|
return macro._call(evaluated_args, ctxdata);
|
|
};
|
|
}
|
|
function PropertyExpression(node, entry) {
|
|
var expression = Expression(node.expression, entry);
|
|
var property = node.computed ?
|
|
Expression(node.property, entry) :
|
|
node.property.name;
|
|
return function propertyExpression(locals, ctxdata) {
|
|
var prop = _resolve(property, locals, ctxdata);
|
|
if (typeof prop !== 'string') {
|
|
throw new RuntimeError('Property name must evaluate to a string: ' +
|
|
prop);
|
|
}
|
|
var parent = expression(locals, ctxdata);
|
|
locals = parent[0];
|
|
parent = parent[1];
|
|
|
|
// At this point, `parent` can be anything and we need to do some
|
|
// type-checking to handle erros gracefully (bug 883664) and securely
|
|
// (bug 815962).
|
|
|
|
// If `parent` is an Entity or an Attribute, `locals` has been
|
|
// correctly set up by Identifier
|
|
if (parent && parent.value !== undefined) {
|
|
if (typeof parent.value !== 'function') {
|
|
throw new RuntimeError('Cannot get property of a ' +
|
|
typeof parent.value + ': ' + prop);
|
|
}
|
|
return parent.value(locals, ctxdata, prop);
|
|
}
|
|
|
|
// If it's a hashLiteral or stringLiteral inside a hash, just call it
|
|
if (typeof parent === 'function') {
|
|
return parent(locals, ctxdata, prop);
|
|
}
|
|
if (parent && parent.expression) {
|
|
throw new RuntimeError('Cannot get property of a macro: ' + prop);
|
|
}
|
|
|
|
// If `parent` is an object passed by the developer to the context
|
|
// (i.e., `expression` was a `VariableExpression`) or a global, return
|
|
// the member of the object corresponding to `prop`
|
|
if (typeof parent === 'object') {
|
|
if (parent === null) {
|
|
throw new RuntimeError('Cannot get property of a null: ' + prop);
|
|
}
|
|
if (Array.isArray(parent)) {
|
|
throw new RuntimeError('Cannot get property of an array: ' + prop);
|
|
}
|
|
if (!parent.hasOwnProperty(prop)) {
|
|
throw new RuntimeError(prop + ' is not defined on the object.');
|
|
}
|
|
return [locals, parent[prop]];
|
|
}
|
|
|
|
// otherwise it's a primitive
|
|
throw new RuntimeError('Cannot get property of a ' + typeof parent +
|
|
': ' + prop);
|
|
};
|
|
}
|
|
function AttributeExpression(node, entry) {
|
|
var expression = Expression(node.expression, entry);
|
|
var attribute = node.computed ?
|
|
Expression(node.attribute, entry) :
|
|
node.attribute.name;
|
|
return function attributeExpression(locals, ctxdata) {
|
|
var attr = _resolve(attribute, locals, ctxdata);
|
|
var entity = expression(locals, ctxdata);
|
|
locals = entity[0];
|
|
entity = entity[1];
|
|
if (!entity.attributes) {
|
|
throw new RuntimeError('Cannot get attribute of a non-entity: ' +
|
|
attr);
|
|
}
|
|
if (!entity.attributes.hasOwnProperty(attr)) {
|
|
throw new RuntimeError(entity.id + ' has no attribute ' + attr);
|
|
}
|
|
return [locals, entity.attributes[attr]];
|
|
};
|
|
}
|
|
function ParenthesisExpression(node, entry) {
|
|
return Expression(node.expression, entry);
|
|
}
|
|
|
|
}
|
|
|
|
Compiler.Error = CompilerError;
|
|
Compiler.CompilationError = CompilationError;
|
|
Compiler.RuntimeError = RuntimeError;
|
|
Compiler.ValueError = ValueError;
|
|
Compiler.IndexError = IndexError;
|
|
|
|
|
|
// `CompilerError` is a general class of errors emitted by the Compiler.
|
|
function CompilerError(message) {
|
|
this.name = 'CompilerError';
|
|
this.message = message;
|
|
}
|
|
CompilerError.prototype = Object.create(Error.prototype);
|
|
CompilerError.prototype.constructor = CompilerError;
|
|
|
|
// `CompilationError` extends `CompilerError`. It's a class of errors
|
|
// which happen during compilation of the AST.
|
|
function CompilationError(message, entry) {
|
|
CompilerError.call(this, message);
|
|
this.name = 'CompilationError';
|
|
this.entry = entry.id;
|
|
}
|
|
CompilationError.prototype = Object.create(CompilerError.prototype);
|
|
CompilationError.prototype.constructor = CompilationError;
|
|
|
|
// `RuntimeError` extends `CompilerError`. It's a class of errors which
|
|
// happen during the evaluation of entries, i.e. when you call
|
|
// `entity.toString()`.
|
|
function RuntimeError(message) {
|
|
CompilerError.call(this, message);
|
|
this.name = 'RuntimeError';
|
|
}
|
|
RuntimeError.prototype = Object.create(CompilerError.prototype);
|
|
RuntimeError.prototype.constructor = RuntimeError;
|
|
|
|
// `ValueError` extends `RuntimeError`. It's a class of errors which
|
|
// happen during the composition of a ComplexString value. It's easier to
|
|
// recover from than an `IndexError` because at least we know that we're
|
|
// showing the correct member of the hash.
|
|
function ValueError(message, entry, source) {
|
|
RuntimeError.call(this, message);
|
|
this.name = 'ValueError';
|
|
this.entry = entry.id;
|
|
this.source = source;
|
|
}
|
|
ValueError.prototype = Object.create(RuntimeError.prototype);
|
|
ValueError.prototype.constructor = ValueError;
|
|
|
|
// `IndexError` extends `RuntimeError`. It's a class of errors which
|
|
// happen during the lookup of a hash member. It's harder to recover
|
|
// from than `ValueError` because we en dup not knowing which variant of the
|
|
// entity value to show and in case the meanings are divergent, the
|
|
// consequences for the user can be serious.
|
|
function IndexError(message, entry) {
|
|
RuntimeError.call(this, message);
|
|
this.name = 'IndexError';
|
|
this.entry = entry.id;
|
|
}
|
|
IndexError.prototype = Object.create(RuntimeError.prototype);
|
|
IndexError.prototype.constructor = IndexError;
|
|
|
|
function requireCompilerError(e) {
|
|
if (!(e instanceof CompilerError)) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
exports.Compiler = Compiler;
|
|
|
|
});
|
|
define('l20n/retranslation', function(require, exports) {
|
|
'use strict';
|
|
|
|
function RetranslationManager() {
|
|
var _usage = [];
|
|
var _counter = {};
|
|
var _callbacks = [];
|
|
|
|
this.bindGet = bindGet;
|
|
this.unbindGet = unbindGet;
|
|
this.all = all;
|
|
this.globals = {};
|
|
|
|
RetranslationManager._constructors.forEach(function(ctor) {
|
|
initGlobal.call(this, ctor);
|
|
}, this);
|
|
|
|
function initGlobal(GlobalCtor) {
|
|
/* jshint validthis: true */
|
|
var global = new GlobalCtor();
|
|
this.globals[global.id] = global;
|
|
if (!global.activate) {
|
|
return;
|
|
}
|
|
_counter[global.id] = 0;
|
|
global.addEventListener('change', function(id) {
|
|
for (var i = 0; i < _usage.length; i++) {
|
|
if (_usage[i] && _usage[i].globals.indexOf(id) !== -1) {
|
|
// invoke the callback with the reason
|
|
_usage[i].callback({
|
|
global: global.id
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function bindGet(get, isRebind) {
|
|
/* jshint validthis: true */
|
|
var i;
|
|
// store the callback in case we want to retranslate the whole context
|
|
var inCallbacks;
|
|
for (i = 0; i < _callbacks.length; i++) {
|
|
if (_callbacks[i].id === get.id) {
|
|
inCallbacks = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!inCallbacks) {
|
|
_callbacks.push(get);
|
|
} else if (isRebind) {
|
|
_callbacks[i] = get;
|
|
}
|
|
|
|
// handle the global usage
|
|
var bound;
|
|
for (i = 0; i < _usage.length; i++) {
|
|
if (_usage[i] && _usage[i].id === get.id) {
|
|
bound = _usage[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
var added;
|
|
if (!bound) {
|
|
// it's the first time we see this get
|
|
if (get.globals.length !== 0) {
|
|
_usage.push(get);
|
|
get.globals.forEach(function(id) {
|
|
if (!this.globals[id].activate) {
|
|
return;
|
|
}
|
|
_counter[id]++;
|
|
this.globals[id].activate();
|
|
}, this);
|
|
}
|
|
} else if (isRebind) {
|
|
// if we rebinding the callback, don't remove globals
|
|
// because we're just adding new entities to the bind
|
|
bound.callback = get.callback;
|
|
added = get.globals.filter(function(id) {
|
|
return this.globals[id].activate && bound.globals.indexOf(id) === -1;
|
|
}, this);
|
|
added.forEach(function(id) {
|
|
_counter[id]++;
|
|
this.globals[id].activate();
|
|
}, this);
|
|
bound.globals = bound.globals.concat(added);
|
|
} else if (get.globals.length === 0) {
|
|
// after a retranslation, no globals were used; remove the callback
|
|
delete _usage[i];
|
|
} else {
|
|
// see which globals were added and which ones were removed
|
|
added = get.globals.filter(function(id) {
|
|
return this.globals[id].activate && bound.globals.indexOf(id) === -1;
|
|
}, this);
|
|
added.forEach(function(id) {
|
|
_counter[id]++;
|
|
this.globals[id].activate();
|
|
}, this);
|
|
var removed = bound.globals.filter(function(id) {
|
|
return this.globals[id].activate && get.globals.indexOf(id) === -1;
|
|
}, this);
|
|
removed.forEach(function(id) {
|
|
_counter[id]--;
|
|
if (_counter[id] === 0) {
|
|
this.globals[id].deactivate();
|
|
}
|
|
}, this);
|
|
bound.globals = get.globals;
|
|
}
|
|
}
|
|
|
|
function unbindGet(id) {
|
|
/* jshint validthis: true */
|
|
var bound;
|
|
var usagePos = -1;
|
|
for (var i = 0; i < _usage.length; i++) {
|
|
if (_usage[i] && _usage[i].id === id) {
|
|
bound = _usage[i];
|
|
usagePos = i;
|
|
break;
|
|
}
|
|
}
|
|
if (bound) {
|
|
bound.globals.forEach(function(id) {
|
|
if (this.globals[id].activate) {
|
|
_counter[id]--;
|
|
if (_counter[id] === 0) {
|
|
this.globals[id].deactivate();
|
|
}
|
|
}
|
|
}, this);
|
|
_usage.splice(usagePos, 1);
|
|
for (i = 0; i < _callbacks.length; i++) {
|
|
if (_callbacks[i].id === id) {
|
|
_callbacks.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function all(locales) {
|
|
for (var i = 0; i < _callbacks.length; i++) {
|
|
// invoke the callback with the reason
|
|
_callbacks[i].callback({
|
|
locales: locales
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
RetranslationManager._constructors = [];
|
|
|
|
RetranslationManager.registerGlobal = function(ctor) {
|
|
RetranslationManager._constructors.push(ctor);
|
|
};
|
|
|
|
RetranslationManager.deregisterGlobal = function(ctor) {
|
|
var pos = RetranslationManager._constructors.indexOf(ctor);
|
|
if (pos !== -1) {
|
|
RetranslationManager._constructors.splice(pos, 1);
|
|
}
|
|
};
|
|
|
|
exports.RetranslationManager = RetranslationManager;
|
|
|
|
});
|
|
define('l20n/platform/globals', function(require, exports) {
|
|
'use strict';
|
|
|
|
var EventEmitter = require('../events').EventEmitter;
|
|
var RetranslationManager = require('../retranslation').RetranslationManager;
|
|
|
|
function Global() {
|
|
this.id = null;
|
|
this._emitter = new EventEmitter();
|
|
this.value = null;
|
|
this.isActive = false;
|
|
}
|
|
|
|
Global.prototype._get = function _get() {
|
|
throw new Error('Not implemented');
|
|
};
|
|
|
|
Global.prototype.get = function get() {
|
|
// invalidate the cached value if the global is not active; active
|
|
// globals handle `value` automatically in `onchange()`
|
|
if (!this.value || !this.isActive) {
|
|
this.value = this._get();
|
|
}
|
|
return this.value;
|
|
};
|
|
|
|
Global.prototype.addEventListener = function(type, listener) {
|
|
if (type !== 'change') {
|
|
throw 'Unknown event type';
|
|
}
|
|
this._emitter.addEventListener(type, listener);
|
|
};
|
|
|
|
|
|
// XXX: https://bugzilla.mozilla.org/show_bug.cgi?id=865226
|
|
// We want to have @screen.width, but since we can't get it from compiler, we
|
|
// call it @screen and in order to keep API forward-compatible with 1.0 we
|
|
// return an object with key width to
|
|
// make it callable as @screen.width
|
|
function ScreenGlobal() {
|
|
Global.call(this);
|
|
this.id = 'screen';
|
|
this._get = _get;
|
|
|
|
this.activate = function activate() {
|
|
if (!this.isActive) {
|
|
window.addEventListener('resize', onchange);
|
|
this.isActive = true;
|
|
}
|
|
};
|
|
|
|
this.deactivate = function deactivate() {
|
|
window.removeEventListener('resize', onchange);
|
|
this.value = null;
|
|
this.isActive = false;
|
|
};
|
|
|
|
function _get() {
|
|
return {
|
|
width: {
|
|
px: document.body.clientWidth
|
|
}
|
|
};
|
|
}
|
|
|
|
var self = this;
|
|
|
|
function onchange() {
|
|
self.value = _get();
|
|
self._emitter.emit('change', self.id);
|
|
}
|
|
}
|
|
|
|
ScreenGlobal.prototype = Object.create(Global.prototype);
|
|
ScreenGlobal.prototype.constructor = ScreenGlobal;
|
|
|
|
|
|
function OSGlobal() {
|
|
Global.call(this);
|
|
this.id = 'os';
|
|
this.get = get;
|
|
|
|
function get() {
|
|
if (/^MacIntel/.test(navigator.platform)) {
|
|
return 'mac';
|
|
}
|
|
if (/^Linux/.test(navigator.platform)) {
|
|
return 'linux';
|
|
}
|
|
if (/^Win/.test(navigator.platform)) {
|
|
return 'win';
|
|
}
|
|
return 'unknown';
|
|
}
|
|
|
|
}
|
|
|
|
OSGlobal.prototype = Object.create(Global.prototype);
|
|
OSGlobal.prototype.constructor = OSGlobal;
|
|
|
|
function HourGlobal() {
|
|
Global.call(this);
|
|
this.id = 'hour';
|
|
this._get = _get;
|
|
|
|
this.activate = function activate() {
|
|
if (!this.isActive) {
|
|
var time = new Date();
|
|
I = setTimeout(function() {
|
|
onchange();
|
|
I = setInterval(onchange, interval);
|
|
}, interval - (time.getTime() % interval));
|
|
this.isActive = true;
|
|
}
|
|
};
|
|
|
|
this.deactivate = function deactivate() {
|
|
clearInterval(I);
|
|
this.value = null;
|
|
this.isActive = false;
|
|
};
|
|
|
|
function _get() {
|
|
var time = new Date();
|
|
return time.getHours();
|
|
}
|
|
|
|
var self = this;
|
|
var interval = 60 * 60 * 1000;
|
|
var I = null;
|
|
|
|
|
|
|
|
function onchange() {
|
|
var time = new Date();
|
|
if (time.getHours() !== self.value) {
|
|
self.value = time.getHours();
|
|
self._emitter.emit('change', self.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
HourGlobal.prototype = Object.create(Global.prototype);
|
|
HourGlobal.prototype.constructor = HourGlobal;
|
|
|
|
RetranslationManager.registerGlobal(ScreenGlobal);
|
|
RetranslationManager.registerGlobal(OSGlobal);
|
|
RetranslationManager.registerGlobal(HourGlobal);
|
|
|
|
exports.Global = Global;
|
|
|
|
});
|
|
define('l20n/platform/io', function(require, exports) {
|
|
'use strict';
|
|
|
|
exports.load = function load(url, callback, sync) {
|
|
var xhr = new XMLHttpRequest();
|
|
if (xhr.overrideMimeType) {
|
|
xhr.overrideMimeType('text/plain');
|
|
}
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState === 4) {
|
|
if (xhr.status === 200 || xhr.status === 0) {
|
|
callback(null, xhr.responseText);
|
|
} else {
|
|
var ex = new IOError('Not found: ' + url);
|
|
callback(ex);
|
|
}
|
|
}
|
|
};
|
|
xhr.open('GET', url, !sync);
|
|
xhr.send('');
|
|
};
|
|
|
|
function IOError(message) {
|
|
this.name = 'IOError';
|
|
this.message = message;
|
|
}
|
|
IOError.prototype = Object.create(Error.prototype);
|
|
IOError.prototype.constructor = IOError;
|
|
|
|
exports.Error = IOError;
|
|
|
|
});
|
|
define('l20n/intl', function(require, exports, module) {
|
|
'use strict';
|
|
|
|
if (!String.prototype.startsWith) {
|
|
Object.defineProperty(String.prototype, 'startsWith', {
|
|
enumerable: false,
|
|
configurable: false,
|
|
writable: false,
|
|
value: function (searchString, position) {
|
|
position = position || 0;
|
|
return this.indexOf(searchString, position) === position;
|
|
}
|
|
});
|
|
}
|
|
|
|
var unicodeLocaleExtensionSequence = "-u(-[a-z0-9]{2,8})+";
|
|
var unicodeLocaleExtensionSequenceRE = new RegExp(unicodeLocaleExtensionSequence);
|
|
var unicodeLocaleExtensionSequenceGlobalRE = new RegExp(unicodeLocaleExtensionSequence, "g");
|
|
var langTagMappings = {};
|
|
var langSubtagMappings = {};
|
|
var extlangMappings = {};
|
|
|
|
/**
|
|
* Regular expression defining BCP 47 language tags.
|
|
*
|
|
* Spec: RFC 5646 section 2.1.
|
|
*/
|
|
var languageTagRE = (function () {
|
|
// RFC 5234 section B.1
|
|
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
|
var ALPHA = "[a-zA-Z]";
|
|
// DIGIT = %x30-39
|
|
// ; 0-9
|
|
var DIGIT = "[0-9]";
|
|
|
|
// RFC 5646 section 2.1
|
|
// alphanum = (ALPHA / DIGIT) ; letters and numbers
|
|
var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")";
|
|
// regular = "art-lojban" ; these tags match the 'langtag'
|
|
// / "cel-gaulish" ; production, but their subtags
|
|
// / "no-bok" ; are not extended language
|
|
// / "no-nyn" ; or variant subtags: their meaning
|
|
// / "zh-guoyu" ; is defined by their registration
|
|
// / "zh-hakka" ; and all of these are deprecated
|
|
// / "zh-min" ; in favor of a more modern
|
|
// / "zh-min-nan" ; subtag or sequence of subtags
|
|
// / "zh-xiang"
|
|
var regular = "(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)";
|
|
// irregular = "en-GB-oed" ; irregular tags do not match
|
|
// / "i-ami" ; the 'langtag' production and
|
|
// / "i-bnn" ; would not otherwise be
|
|
// / "i-default" ; considered 'well-formed'
|
|
// / "i-enochian" ; These tags are all valid,
|
|
// / "i-hak" ; but most are deprecated
|
|
// / "i-klingon" ; in favor of more modern
|
|
// / "i-lux" ; subtags or subtag
|
|
// / "i-mingo" ; combination
|
|
// / "i-navajo"
|
|
// / "i-pwn"
|
|
// / "i-tao"
|
|
// / "i-tay"
|
|
// / "i-tsu"
|
|
// / "sgn-BE-FR"
|
|
// / "sgn-BE-NL"
|
|
// / "sgn-CH-DE"
|
|
var irregular = "(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)";
|
|
// grandfathered = irregular ; non-redundant tags registered
|
|
// / regular ; during the RFC 3066 era
|
|
var grandfathered = "(?:" + irregular + "|" + regular + ")";
|
|
// privateuse = "x" 1*("-" (1*8alphanum))
|
|
var privateuse = "(?:x(?:-[a-z0-9]{1,8})+)";
|
|
// singleton = DIGIT ; 0 - 9
|
|
// / %x41-57 ; A - W
|
|
// / %x59-5A ; Y - Z
|
|
// / %x61-77 ; a - w
|
|
// / %x79-7A ; y - z
|
|
var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])";
|
|
// extension = singleton 1*("-" (2*8alphanum))
|
|
var extension = "(?:" + singleton + "(?:-" + alphanum + "{2,8})+)";
|
|
// variant = 5*8alphanum ; registered variants
|
|
// / (DIGIT 3alphanum)
|
|
var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))";
|
|
// region = 2ALPHA ; ISO 3166-1 code
|
|
// / 3DIGIT ; UN M.49 code
|
|
var region = "(?:" + ALPHA + "{2}|" + DIGIT + "{3})";
|
|
// script = 4ALPHA ; ISO 15924 code
|
|
var script = "(?:" + ALPHA + "{4})";
|
|
// extlang = 3ALPHA ; selected ISO 639 codes
|
|
// *2("-" 3ALPHA) ; permanently reserved
|
|
var extlang = "(?:" + ALPHA + "{3}(?:-" + ALPHA + "{3}){0,2})";
|
|
// language = 2*3ALPHA ; shortest ISO 639 code
|
|
// ["-" extlang] ; sometimes followed by
|
|
// ; extended language subtags
|
|
// / 4ALPHA ; or reserved for future use
|
|
// / 5*8ALPHA ; or registered language subtag
|
|
var language = "(?:" + ALPHA + "{2,3}(?:-" + extlang + ")?|" + ALPHA + "{4}|" + ALPHA + "{5,8})";
|
|
// langtag = language
|
|
// ["-" script]
|
|
// ["-" region]
|
|
// *("-" variant)
|
|
// *("-" extension)
|
|
// ["-" privateuse]
|
|
var langtag = language + "(?:-" + script + ")?(?:-" + region + ")?(?:-" +
|
|
variant + ")*(?:-" + extension + ")*(?:-" + privateuse + ")?";
|
|
// Language-Tag = langtag ; normal language tags
|
|
// / privateuse ; private use tag
|
|
// / grandfathered ; grandfathered tags
|
|
var languageTag = "^(?:" + langtag + "|" + privateuse + "|" + grandfathered + ")$";
|
|
|
|
// Language tags are case insensitive (RFC 5646 section 2.1.1).
|
|
return new RegExp(languageTag, "i");
|
|
}());
|
|
|
|
var duplicateVariantRE = (function () {
|
|
// RFC 5234 section B.1
|
|
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
|
var ALPHA = "[a-zA-Z]";
|
|
// DIGIT = %x30-39
|
|
// ; 0-9
|
|
var DIGIT = "[0-9]";
|
|
|
|
// RFC 5646 section 2.1
|
|
// alphanum = (ALPHA / DIGIT) ; letters and numbers
|
|
var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")";
|
|
// variant = 5*8alphanum ; registered variants
|
|
// / (DIGIT 3alphanum)
|
|
var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))";
|
|
|
|
// Match a langtag that contains a duplicate variant.
|
|
var duplicateVariant =
|
|
// Match everything in a langtag prior to any variants, and maybe some
|
|
// of the variants as well (which makes this pattern inefficient but
|
|
// not wrong, for our purposes);
|
|
"(?:" + alphanum + "{2,8}-)+" +
|
|
// a variant, parenthesised so that we can refer back to it later;
|
|
"(" + variant + ")-" +
|
|
// zero or more subtags at least two characters long (thus stopping
|
|
// before extension and privateuse components);
|
|
"(?:" + alphanum + "{2,8}-)*" +
|
|
// and the same variant again
|
|
"\\1" +
|
|
// ...but not followed by any characters that would turn it into a
|
|
// different subtag.
|
|
"(?!" + alphanum + ")";
|
|
|
|
// Language tags are case insensitive (RFC 5646 section 2.1.1), but for
|
|
// this regular expression that's covered by having its character classes
|
|
// list both upper- and lower-case characters.
|
|
return new RegExp(duplicateVariant);
|
|
}());
|
|
|
|
|
|
var duplicateSingletonRE = (function () {
|
|
// RFC 5234 section B.1
|
|
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
|
var ALPHA = "[a-zA-Z]";
|
|
// DIGIT = %x30-39
|
|
// ; 0-9
|
|
var DIGIT = "[0-9]";
|
|
|
|
// RFC 5646 section 2.1
|
|
// alphanum = (ALPHA / DIGIT) ; letters and numbers
|
|
var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")";
|
|
// singleton = DIGIT ; 0 - 9
|
|
// / %x41-57 ; A - W
|
|
// / %x59-5A ; Y - Z
|
|
// / %x61-77 ; a - w
|
|
// / %x79-7A ; y - z
|
|
var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])";
|
|
|
|
// Match a langtag that contains a duplicate singleton.
|
|
var duplicateSingleton =
|
|
// Match a singleton subtag, parenthesised so that we can refer back to
|
|
// it later;
|
|
"-(" + singleton + ")-" +
|
|
// then zero or more subtags;
|
|
"(?:" + alphanum + "+-)*" +
|
|
// and the same singleton again
|
|
"\\1" +
|
|
// ...but not followed by any characters that would turn it into a
|
|
// different subtag.
|
|
"(?!" + alphanum + ")";
|
|
|
|
// Language tags are case insensitive (RFC 5646 section 2.1.1), but for
|
|
// this regular expression that's covered by having its character classes
|
|
// list both upper- and lower-case characters.
|
|
return new RegExp(duplicateSingleton);
|
|
}());
|
|
|
|
/**
|
|
* Verifies that the given string is a well-formed BCP 47 language tag
|
|
* with no duplicate variant or singleton subtags.
|
|
*
|
|
* Spec: ECMAScript Internationalization API Specification, 6.2.2.
|
|
*/
|
|
function IsStructurallyValidLanguageTag(locale) {
|
|
if (!languageTagRE.test(locale))
|
|
return false;
|
|
|
|
// Before checking for duplicate variant or singleton subtags with
|
|
// regular expressions, we have to get private use subtag sequences
|
|
// out of the picture.
|
|
if (locale.startsWith("x-"))
|
|
return true;
|
|
var pos = locale.indexOf("-x-");
|
|
if (pos !== -1)
|
|
locale = locale.substring(0, pos);
|
|
|
|
// Check for duplicate variant or singleton subtags.
|
|
return !duplicateVariantRE.test(locale) &&
|
|
!duplicateSingletonRE.test(locale);
|
|
}
|
|
|
|
/**
|
|
* Canonicalizes the given structurally valid BCP 47 language tag, including
|
|
* regularized case of subtags. For example, the language tag
|
|
* Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE, where
|
|
*
|
|
* Zh ; 2*3ALPHA
|
|
* -NAN ; ["-" extlang]
|
|
* -haNS ; ["-" script]
|
|
* -bu ; ["-" region]
|
|
* -variant2 ; *("-" variant)
|
|
* -Variant1
|
|
* -u-ca-chinese ; *("-" extension)
|
|
* -t-Zh-laTN
|
|
* -x-PRIVATE ; ["-" privateuse]
|
|
*
|
|
* becomes nan-Hans-mm-variant2-variant1-t-zh-latn-u-ca-chinese-x-private
|
|
*
|
|
* Spec: ECMAScript Internationalization API Specification, 6.2.3.
|
|
* Spec: RFC 5646, section 4.5.
|
|
*/
|
|
function CanonicalizeLanguageTag(locale) {
|
|
// The input
|
|
// "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE"
|
|
// will be used throughout this method to illustrate how it works.
|
|
|
|
// Language tags are compared and processed case-insensitively, so
|
|
// technically it's not necessary to adjust case. But for easier processing,
|
|
// and because the canonical form for most subtags is lower case, we start
|
|
// with lower case for all.
|
|
// "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE" ->
|
|
// "zh-nan-hans-bu-variant2-variant1-u-ca-chinese-t-zh-latn-x-private"
|
|
locale = locale.toLowerCase();
|
|
|
|
// Handle mappings for complete tags.
|
|
if (langTagMappings && langTagMappings.hasOwnProperty(locale))
|
|
return langTagMappings[locale];
|
|
|
|
var subtags = locale.split("-");
|
|
var i = 0;
|
|
|
|
// Handle the standard part: All subtags before the first singleton or "x".
|
|
// "zh-nan-hans-bu-variant2-variant1"
|
|
while (i < subtags.length) {
|
|
var subtag = subtags[i];
|
|
|
|
// If we reach the start of an extension sequence or private use part,
|
|
// we're done with this loop. We have to check for i > 0 because for
|
|
// irregular language tags, such as i-klingon, the single-character
|
|
// subtag "i" is not the start of an extension sequence.
|
|
// In the example, we break at "u".
|
|
if (subtag.length === 1 && (i > 0 || subtag === "x"))
|
|
break;
|
|
|
|
if (subtag.length === 4) {
|
|
// 4-character subtags are script codes; their first character
|
|
// needs to be capitalized. "hans" -> "Hans"
|
|
subtag = subtag[0].toUpperCase() +
|
|
subtag.substring(1);
|
|
} else if (i !== 0 && subtag.length === 2) {
|
|
// 2-character subtags that are not in initial position are region
|
|
// codes; they need to be upper case. "bu" -> "BU"
|
|
subtag = subtag.toUpperCase();
|
|
}
|
|
if (langSubtagMappings.hasOwnProperty(subtag)) {
|
|
// Replace deprecated subtags with their preferred values.
|
|
// "BU" -> "MM"
|
|
// This has to come after we capitalize region codes because
|
|
// otherwise some language and region codes could be confused.
|
|
// For example, "in" is an obsolete language code for Indonesian,
|
|
// but "IN" is the country code for India.
|
|
// Note that the script generating langSubtagMappings makes sure
|
|
// that no regular subtag mapping will replace an extlang code.
|
|
subtag = langSubtagMappings[subtag];
|
|
} else if (extlangMappings.hasOwnProperty(subtag)) {
|
|
// Replace deprecated extlang subtags with their preferred values,
|
|
// and remove the preceding subtag if it's a redundant prefix.
|
|
// "zh-nan" -> "nan"
|
|
// Note that the script generating extlangMappings makes sure that
|
|
// no extlang mapping will replace a normal language code.
|
|
subtag = extlangMappings[subtag].preferred;
|
|
if (i === 1 && extlangMappings[subtag].prefix === subtags[0]) {
|
|
subtags.shift();
|
|
i--;
|
|
}
|
|
}
|
|
subtags[i] = subtag;
|
|
i++;
|
|
}
|
|
var normal = subtags.slice(0, i).join("-");
|
|
|
|
// Extension sequences are sorted by their singleton characters.
|
|
// "u-ca-chinese-t-zh-latn" -> "t-zh-latn-u-ca-chinese"
|
|
var extensions = [];
|
|
while (i < subtags.length && subtags[i] !== "x") {
|
|
var extensionStart = i;
|
|
i++;
|
|
while (i < subtags.length && subtags[i].length > 1)
|
|
i++;
|
|
var extension = sybtags.slice(extensionStart, i).join("-");
|
|
extensions.push(extension);
|
|
}
|
|
extensions.sort();
|
|
|
|
// Private use sequences are left as is. "x-private"
|
|
var privateUse = "";
|
|
if (i < subtags.length)
|
|
privateUse = subtags.slice(i).join("-");
|
|
|
|
// Put everything back together.
|
|
var canonical = normal;
|
|
if (extensions.length > 0)
|
|
canonical += "-" + extensions.join("-");
|
|
if (privateUse.length > 0) {
|
|
// Be careful of a Language-Tag that is entirely privateuse.
|
|
if (canonical.length > 0)
|
|
canonical += "-" + privateUse;
|
|
else
|
|
canonical = privateUse;
|
|
}
|
|
|
|
return canonical;
|
|
}
|
|
|
|
/**
|
|
* Canonicalizes a locale list.
|
|
*
|
|
* Spec: ECMAScript Internationalization API Specification, 9.2.1.
|
|
*/
|
|
function CanonicalizeLocaleList(locales) {
|
|
if (locales === undefined)
|
|
return [];
|
|
var seen = [];
|
|
if (typeof locales === "string")
|
|
locales = [locales];
|
|
var O = locales;
|
|
var len = O.length;
|
|
var k = 0;
|
|
while (k < len) {
|
|
// Don't call ToString(k) - SpiderMonkey is faster with integers.
|
|
var kPresent = k in O;
|
|
if (kPresent) {
|
|
var kValue = O[k];
|
|
if (!(typeof kValue === "string" || typeof kValue === "object"))
|
|
ThrowError(JSMSG_INVALID_LOCALES_ELEMENT);
|
|
var tag = kValue;
|
|
if (!IsStructurallyValidLanguageTag(tag))
|
|
ThrowError(JSMSG_INVALID_LANGUAGE_TAG, tag);
|
|
tag = CanonicalizeLanguageTag(tag);
|
|
if (seen.indexOf(tag) === -1)
|
|
seen.push(tag);
|
|
}
|
|
k++;
|
|
}
|
|
return seen;
|
|
}
|
|
|
|
/**
|
|
* Compares a BCP 47 language tag against the locales in availableLocales
|
|
* and returns the best available match. Uses the fallback
|
|
* mechanism of RFC 4647, section 3.4.
|
|
*
|
|
* Spec: ECMAScript Internationalization API Specification, 9.2.2.
|
|
* Spec: RFC 4647, section 3.4.
|
|
*/
|
|
function BestAvailableLocale(availableLocales, locale) {
|
|
var candidate = locale;
|
|
while (true) {
|
|
if (availableLocales.indexOf(candidate) !== -1)
|
|
return candidate;
|
|
var pos = candidate.lastIndexOf('-');
|
|
if (pos === -1)
|
|
return undefined;
|
|
if (pos >= 2 && candidate[pos - 2] === "-")
|
|
pos -= 2;
|
|
candidate = candidate.substring(0, pos);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the subset of availableLocales for which requestedLocales has a
|
|
* matching (possibly fallback) locale. Locales appear in the same order in the
|
|
* returned list as in the input list.
|
|
*
|
|
* This function is a slight modification of the LookupSupprtedLocales algorithm
|
|
* The difference is in step 4d where instead of adding requested locale,
|
|
* we're adding availableLocale to the subset.
|
|
*
|
|
* This allows us to directly use returned subset to pool resources.
|
|
*
|
|
* Spec: ECMAScript Internationalization API Specification, 9.2.6.
|
|
*/
|
|
function LookupAvailableLocales(availableLocales, requestedLocales) {
|
|
// Steps 1-2.
|
|
var len = requestedLocales.length;
|
|
var subset = [];
|
|
|
|
// Steps 3-4.
|
|
var k = 0;
|
|
while (k < len) {
|
|
// Steps 4.a-b.
|
|
var locale = requestedLocales[k];
|
|
var noExtensionsLocale = locale.replace(unicodeLocaleExtensionSequenceGlobalRE, "");
|
|
|
|
// Step 4.c-d.
|
|
var availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale);
|
|
if (availableLocale !== undefined)
|
|
// in LookupSupportedLocales it pushes locale here
|
|
subset.push(availableLocale);
|
|
|
|
// Step 4.e.
|
|
k++;
|
|
}
|
|
|
|
// Steps 5-6.
|
|
return subset.slice(0);
|
|
}
|
|
|
|
function PrioritizeLocales(availableLocales,
|
|
requestedLocales,
|
|
defaultLocale) {
|
|
availableLocales = CanonicalizeLocaleList(availableLocales);
|
|
requestedLocales = CanonicalizeLocaleList(requestedLocales);
|
|
|
|
var result = LookupAvailableLocales(availableLocales, requestedLocales);
|
|
if (!defaultLocale) {
|
|
return result;
|
|
}
|
|
|
|
// if default locale is not present in result,
|
|
// add it to the end of fallback chain
|
|
defaultLocale = CanonicalizeLanguageTag(defaultLocale);
|
|
if (result.indexOf(defaultLocale) === -1) {
|
|
result.push(defaultLocale);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
exports.Intl = {
|
|
prioritizeLocales: PrioritizeLocales
|
|
};
|
|
|
|
});
|
|
// attach the L20n singleton to the global object
|
|
window.L20n = require('l20n');
|
|
|
|
// close the function defined in build/prefix/microrequire.js
|
|
})(window);
|