// The renderer: the templates in templates/, and the two things sw.js does // with them -- compose a fragment into the shell, and stand a notice in for a // page that could not be delivered. // // A file of its own rather than more of sw.js because it is the half with // nothing to do with the network: no routes, no caches, no requests. It was // loaded by boot.html as well once, which composed pages in the browser for a // device that could not install a worker; nothing but the worker builds a page // now. // // Loaded with importScripts, so it defines a global rather than exporting. // Nunjucks must already be loaded. (function (scope) { 'use strict'; // A fragment is a 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', 'verse.njk', 'version.njk', 'toast.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. reference is null on the book list, which is the // picker's hidden state; the other two are always there, because the // worker composing this is the thing that knows them. 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.mark, reference: chrome.reference, version: chrome.version }); }, // 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);