150 lines
6.5 KiB
JavaScript
150 lines
6.5 KiB
JavaScript
// The renderer, shared by the two things that ever build a page: the service
|
|
// worker, which composes every navigation once it is installed, and boot.html,
|
|
// which composes the one navigation that arrives before there is a worker to
|
|
// do it.
|
|
//
|
|
// Sharing it is the whole point. A shell only the worker could build is what
|
|
// made a first visit show a spinner and reload; with the same templates and
|
|
// the same code reachable from the page, the first visit is already the app
|
|
// and the worker is what makes the second one fast and the hundredth one
|
|
// offline.
|
|
//
|
|
// Loaded as a plain script by both -- importScripts in the worker, a <script>
|
|
// tag in the page -- so it defines a global rather than exporting. Nunjucks
|
|
// must already be loaded.
|
|
(function (scope) {
|
|
'use strict';
|
|
|
|
// A fragment is a <title> and one root element. The root's data-template
|
|
// names the template that wraps it; compile_bible.py writes both.
|
|
var TITLE_RE = /<title>([\s\S]*?)<\/title>/i;
|
|
var TEMPLATE_RE = /data-template="([a-z0-9_-]+)"/i;
|
|
|
|
// Templates live in one directory and are named by file within it, so a
|
|
// template that extends or includes another names only the file.
|
|
var TEMPLATES = './templates/';
|
|
|
|
// All of them. Named here rather than in either caller because this file is
|
|
// the one that knows what a template is: sw.js precaches them from this list
|
|
// and both renderers load them from it, so there is one place to add the
|
|
// next one. A template missing from here is a template nunjucks would have
|
|
// to fetch mid-render, which is the one thing loadAll() exists to prevent.
|
|
var NAMES = [
|
|
'base.njk',
|
|
'page.njk',
|
|
'book.njk',
|
|
'index.njk',
|
|
'bookmark.njk',
|
|
'reference.njk',
|
|
'verses.njk',
|
|
'version.njk',
|
|
'status.njk',
|
|
'notice.njk'
|
|
];
|
|
|
|
scope.TEMPLATE_URLS = NAMES.map(function (name) { return TEMPLATES + name; });
|
|
|
|
// readText(path) -> Promise<string> is the only thing the two callers do
|
|
// differently: the worker reads through its cache, the page over the network.
|
|
scope.createRenderer = function (readText) {
|
|
// Nunjucks ships a WebLoader built on XMLHttpRequest, which does not exist
|
|
// in a service worker. This one is readText and nothing else.
|
|
function Loader() {}
|
|
Loader.prototype.async = true;
|
|
Loader.prototype.getSource = function (name, callback) {
|
|
readText(TEMPLATES + name).then(
|
|
function (src) { callback(null, { src: src, path: name, noCache: false }); },
|
|
function (err) { callback(err); });
|
|
};
|
|
|
|
// Compiled templates are held for the lifetime of whatever loaded this. In
|
|
// the worker an edit to any of them changes VERSION, which starts a new
|
|
// worker, so they never go stale.
|
|
var env = new nunjucks.Environment(new Loader(), { autoescape: true });
|
|
|
|
// Every template, in nunjucks' hands before the first render, because an
|
|
// include that has to wait for one does not come out whole.
|
|
//
|
|
// getTemplate answers from the loader's cache by calling straight back,
|
|
// and only reaches this loader -- and this loader's promise -- for a
|
|
// template it has never seen. Nunjucks compiles {% include %} into a
|
|
// waterfall, and the rest of the block it sits in is the waterfall's
|
|
// callback while the code after that block runs on regardless. Answered
|
|
// in the same tick the two happen in order and the output is whole;
|
|
// answered a tick later the render is finished and handed back first,
|
|
// and what comes back is missing the include and everything between it
|
|
// and the end of its block. The verse list, and then the </select> after
|
|
// it -- a picker with a chapter and no verses, on whichever navigation
|
|
// happened to be the first to need the template.
|
|
//
|
|
// Which is why it is all of them and not just the ones that get included:
|
|
// whether a template has been reached yet depends on which pages have
|
|
// been rendered, and in the worker on whether it has been restarted since.
|
|
// That is the whole shape of the bug -- intermittent, and never twice in a
|
|
// row. One pass over a few kilobytes already in the cache buys the
|
|
// question being settled.
|
|
//
|
|
// Failures resolve rather than reject: a template that cannot be read now
|
|
// will fail again in the render that actually needs it, where the error
|
|
// belongs to a request and can be answered with the bare fragment.
|
|
var loaded = null;
|
|
|
|
function loadAll() {
|
|
if (!loaded) {
|
|
loaded = Promise.all(NAMES.map(function (name) {
|
|
return new Promise(function (resolve) {
|
|
env.getTemplate(name, true, null, false, function () { resolve(); });
|
|
});
|
|
}));
|
|
}
|
|
return loaded;
|
|
}
|
|
|
|
function render(name, context) {
|
|
return loadAll().then(function () {
|
|
return new Promise(function (resolve, reject) {
|
|
env.render(name, context, function (err, out) {
|
|
return err ? reject(err) : resolve(out);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
return {
|
|
render: render,
|
|
|
|
// A fragment becomes a whole document. page.njk is the fallback for one
|
|
// that names no template of its own.
|
|
//
|
|
// content is already escaped -- compile_bible.py escapes as it writes and
|
|
// notice.njk comes out of nunjucks -- which is what the |safe on it in
|
|
// base.njk is for.
|
|
//
|
|
// chrome is what the shell needs and the fragment cannot carry: the
|
|
// bookmark, the navbar picker's model, and the two versions the worker
|
|
// was built with. All three are null from boot.html, which has no worker
|
|
// to ask for any of them; the first two render as a hidden slot that
|
|
// fills itself if one ever appears, and the third renders as nothing,
|
|
// because no worker is the whole of what there is to report.
|
|
compose: function (fragment, chrome) {
|
|
var title = (TITLE_RE.exec(fragment) || ['', ''])[1].trim();
|
|
var content = fragment.replace(TITLE_RE, '');
|
|
var name = (TEMPLATE_RE.exec(content) || ['', 'page'])[1] + '.njk';
|
|
return render(name, {
|
|
title: title,
|
|
content: content,
|
|
mark: (chrome && chrome.mark) || null,
|
|
reference: (chrome && chrome.reference) || null,
|
|
version: (chrome && chrome.version) || null
|
|
});
|
|
},
|
|
|
|
// A page that could not be delivered, in the shape of a real fragment so
|
|
// that it composes into a document and swaps into #content like one.
|
|
notice: function (heading, text) {
|
|
return render('notice.njk', { heading: heading, text: text });
|
|
}
|
|
};
|
|
};
|
|
})(self);
|