This commit is contained in:
mark committed 2026-09-07 08:22:43 -04:00
1 parent ba3b588ac5
commit d74385ca3c
2 files changed
+80 -24

No files matched your search

+63 -17
View File
@@ -311,9 +311,19 @@ function verseLabels(chapters, number) {
// no verse anchors to offer.
//
// anchor says which chapter to show the verses of, and which verse to mark
// selected. Null starts at 1:1, which is where every book starts: this says
// where you are going, not where you are, and the address bar is the only
// record of the latter.
// selected. Anything this cannot read -- nothing at all, an empty select, a
// footnote id, a chapter past the end of the book -- is chapter 1, and chapter
// 1 is filled in as if it had been picked. There is no state where a chapter
// is only sort of chosen: a select showing an option is a select that chose
// it, and the verses beside it have to be that chapter's.
//
// Every book that gets a picker has verses in its first chapter -- the two
// that do not, the front matter and the glossary, are turned away above -- so
// falling back to 1 always fills the verse list rather than emptying it.
//
// The verse is left unselected when the anchor names only a chapter. That is
// not the same as unfilled: the options are chapter 1's, and the select shows
// the first of them. It says where you are going, not where you are.
function referenceFor(map, code, anchor) {
const chapters = map && Object.prototype.hasOwnProperty.call(map, code)
? map[code] : null;
@@ -328,12 +338,15 @@ function referenceFor(map, code, anchor) {
const at = parseAnchor(anchor) || { chapter: 1, verse: null };
const number = at.chapter >= 1 && at.chapter <= chapters.length ? at.chapter : 1;
// Only when the anchor got as far as naming this chapter. A fallback to 1
// has not chosen a verse, it has just run out of anchor.
const verse = at.chapter === number ? at.verse : null;
return {
book: code,
chapter: 'ch' + number,
chapters: chapters.map((_, i) => ({ id: 'ch' + (i + 1), label: String(i + 1) })),
verse: at.verse,
verse: verse,
verses: verseLabels(chapters, number)
.map((label) => ({ id: 'v' + number + '_' + label, label: label }))
};
@@ -415,19 +428,30 @@ router.get('/api/bookmark', () => bookmarkFragment(''));
// The picker, and the two moves it makes.
//
// no parameters the whole picker, for whichever book is on screen. Asked
// for by the picker itself, after a history restore -- which
// swaps the history element straight in and processes no
// out-of-band elements -- and when a worker first appears
// under a page boot.html composed.
// no parameters the whole picker, standing where the anchor in the address
// bar puts it. Asked for by the picker itself: on load,
// after a history restore -- which swaps the history element
// straight in and processes no out-of-band elements -- and
// when a worker first appears under a page boot.html
// composed.
// chapter=ch3 the verses of that chapter, and land on the chapter.
// verse=v3_16 the same verses, that one selected, and land on the verse.
//
// The book is not in the URL, deliberately: it comes from HX-Current-URL,
// which is the address the page is showing now rather than the address it was
// showing when the picker was last rendered. The picker sits in the navbar and
// outlives the page under it, so anything baked into its attributes is the one
// thing here that can go stale.
// Neither the book nor the anchor is in the URL, deliberately. Both come from
// HX-Current-URL, which htmx sets from location.href on every request it
// makes: the whole address, fragment included.
//
// For the book, because the picker sits in the navbar and outlives the page
// under it -- an address baked into its attributes is the one thing here that
// can go stale. For the anchor, because this is the only way a fragment
// reaches this file at all. A browser opening /GEN#v3_16 asks for /GEN and
// keeps the rest to itself, so the shell is composed without it and the picker
// is rendered at 1:1 like any other. The page then asks this route for itself,
// and that request is the one carrying the anchor.
//
// Which also puts the picker where it belongs after a back or forward, and
// under a page boot.html composed once a worker turns up, for nothing extra:
// those requests carry the address bar too.
//
// What the response headers are doing is the whole of what app.js used to do
// on a change event, and htmx has a name for each part of it:
@@ -442,11 +466,33 @@ router.get('/api/bookmark', () => bookmarkFragment(''));
// history entry per option spun past.
router.get('/api/reference', async (call) => {
const params = call.url.searchParams;
const anchor = params.get('verse') || params.get('chapter');
const page = call.currentUrl();
const reference = await referenceOf(bookOf(page), anchor);
// Which select the reader moved, if either did -- by whether the parameter
// is there, not by whether it says anything. A select posts an empty value
// when it has no options, and reading that as "no select moved" answers it
// with a whole picker: htmx swaps a <div> into a <select>, which renders as
// nothing, and the select is then empty for good -- every change after that
// posts the same empty value and gets the same answer.
//
// An empty value is still an answerable question. It names no chapter, so it
// gets chapter 1, which is what everything here does with an anchor it
// cannot read.
const moved = params.has('verse') ? params.get('verse')
: params.has('chapter') ? params.get('chapter')
: null;
if (!anchor) return view.render('reference.njk', { reference: reference });
// The address bar is the fallback, and only that: once a select has been
// used it is the one saying where to go, and HX-Replace-Url has not caught
// up yet.
const reference = await referenceOf(
bookOf(page), moved === null ? (page && page.hash.slice(1)) : moved);
// settled, because this picker is already standing where the anchor put it.
// The one that came with the page is not, and carries a load trigger to ask
// -- which this answer must not carry as well, or it would ask forever.
if (moved === null) {
return view.render('reference.njk', { reference: reference, settled: true });
}
// The options were rendered from this map, so failing to parse them back
// means the map moved under an open page: leave the picker alone rather
// than swapping in options for a book that is no longer on screen.