Remove comments

This commit is contained in:
mark committed 2026-09-09 12:09:44 -04:00
1 parent 872175153a
commit 057b4be7d7
18 files changed
+66 -1350

No files matched your search

-118
View File
@@ -1,52 +1,14 @@
{# The shell every page is wrapped in. Rendered by sw.js, not by the build,
so editing this file changes every page without regenerating any of them.
Context: title, content (safe HTML fragment), mark (bookmark or null),
reference (the navbar picker's model, or null).
Bootstrap is the whole stylesheet. The fragments carry semantic USFM
classes and no Bootstrap ones, so the body text is styled by Reboot; the
chrome here is what uses the framework. #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
{# No viewport-fit=cover, deliberately. It extends the page into the notch
and the home indicator, which only makes sense paired with safe-area-inset
padding in the stylesheet -- and without that padding on iOS the first line
of scripture sits under the clock. Paired with the status bar style below:
change one and the other has to change too. #}
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ title | safe }}{% endblock %}</title>
{# Vendored rather than linked from a CDN: sw.js only caches same-origin
responses, so a CDN stylesheet would leave the app unstyled offline. #}
<link rel="stylesheet" href="css/bootstrap.min.css">
{# app.css comes second so its variable overrides win. It styles the
scripture; Bootstrap styles the chrome. #}
<link rel="stylesheet" href="css/app.css">
{# History entries are whole books, and htmx keeps its history cache in
localStorage -- a couple of them would blow the quota. Turned off, so every
back or forward is a cache miss, and refreshOnHistoryMiss decides what a
miss does.
False on purpose. True makes htmx call location.reload(true) -- a forced
reload, which is exactly the kind of navigation a browser takes past the
service worker, so it reaches server.py and comes back as boot.html: the
spinner flashes by and then reloads into the real page. False makes htmx
fetch the page over XHR instead, which the worker answers itself, from its
own cache when offline. No navigation, no boot page, nothing to flash.
That request carries HX-History-Restore-Request, and the worker answers it
with a whole composed document; htmx lifts the element marked
hx-history-elt out of the response and puts it back in the page. #}
<meta name="htmx-config"
content='{"historyCacheSize":0,"refreshOnHistoryMiss":false}'>
{# The bundle carries Popper, which the navbar collapse and the modal do not
need -- but a dropdown or tooltip added later would, and this is one file
in the precache either way. Deferred scripts run in order, so `bootstrap`
is defined by the time app.js runs. #}
<script src="js/bootstrap.bundle.min.js" defer></script>
{# htmx is not deferred: hx-boost has to be in place before the first click. #}
<script src="js/htmx.min.js"></script>
<script src="js/app.js" defer></script>
<link rel="manifest" href="manifest.webmanifest">
@@ -54,22 +16,9 @@
<link rel="apple-touch-icon" href="icons/apple-touch-icon.png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
{# black, not black-translucent. Translucent makes iOS draw the status bar
over the web view and reserve no space for it, so the navbar starts at the
very top of the screen with the clock and battery on top of it. black keeps
the status bar opaque and the view below it, which is what this app wants:
there is no full-bleed artwork here to show through. #}
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Bible">
</head>
{# Boosted links swap only the fragment, so the navbar, the modal and the
scripts above survive navigation and never reload.
No show: modifier, deliberately. htmx adds show:top itself for a boosted
link, but skips it when the href carries a fragment identifier and scrolls
to that anchor after the swap instead. Spelling show:top out here overrides
that decision for every link, which leaves a cross-reference like ISA#v61_6
sitting at the top of Isaiah. #}
<body class="{% block body_class %}{% endblock %} bg-body-tertiary"
hx-boost="true" hx-target="#content" hx-swap="innerHTML">
@@ -94,31 +43,8 @@
</div>
</nav>
{# col-xl-8 keeps a line of verse near a readable measure instead of running
the full width of a desktop window. #}
<main class="container-xl my-4">
<div class="row justify-content-center">
{# The swap target. hx-target on <body> names this id, so every boosted
link lands here -- remove it and a click does nothing at all while a
full navigation still works.
It is also where a verse click becomes a request. htmx's bootstrap-modal
example puts hx-get on the element that opens the dialog; the element
here is a verse number in a generated book, and there are 38,000 of
them -- an attribute apiece would cost more than the whole shell, and a
listener apiece more than the reading. So one handler sits on the
container they all swap into, and names the verse it found. Delegated,
so it survives every swap: this element is never replaced, only filled.
hx-vals cannot do this. htmx evaluates it as Function("return (...)")()
with nothing passed in, so an expression there can read the document but
not the click -- and which verse was clicked is the one thing that has
to travel.
Enter and space go through the same door rather than a second one:
compile_bible.py writes role=button and tabindex on every verse, and a
button that answers the keyboard by being clicked is a button with one
behaviour instead of two. #}
<div id="content" class="col-12 col-xl-8" hx-history-elt
hx-on:click="var verse = event.target.closest('.v[id]');
if (verse) htmx.ajax('GET', 'api/verse?at=' + verse.id,
@@ -131,37 +57,8 @@
{% block content %}{% endblock %}
</div>
</div>
{% include "version.njk" %}
</main>
{# The verse dialog: empty in the page, filled per request. Bootstrap owns
showing it, dismissing it, the backdrop and Escape; sw.js owns everything
inside, and templates/verse.njk is what lands in the modal-content below.
Opened by the arrival of the content rather than by data-bs-toggle on
whatever was clicked. The example puts that attribute on its trigger, which
is not available here for the same reason hx-get is not -- see #content
above -- and the swap is the honest signal anyway: there is nothing worth
showing until it is here. A verse the corpus does not have is answered 204,
which swaps nothing, which opens nothing.
after-swap and not after-settle: settling is the 20ms attribute pass and
this dialog has nothing to settle. Only the modal-content swap can reach
here -- the bookmark write targets the navbar slot, and the toast it sends
back rides in out of band, which fires its own event and not this one.
verse-done is the dialog saying it has finished its job, and it arrives
from both of the things in it: sw.js sends it as a trigger header on a
bookmark write that committed, and the copy button dispatches it when the
clipboard has actually taken the link. Neither sends it when the action
failed -- the field in the dialog is the fallback for a copy that was
refused, and there is nothing to take down the dialog for when there is
nothing to celebrate.
Closed at once rather than after a pause. The pause was there to let the
status line inside be read before the dialog took it away; the line is a
toast now, outside and above all of this, so it outlives the dialog and
there is nothing left to wait for. #}
<div class="modal fade" id="verse-dialog" tabindex="-1"
aria-labelledby="verse-dialog-ref" aria-hidden="true"
hx-on::after-swap="bootstrap.Modal.getOrCreateInstance(this).show();"
@@ -171,21 +68,6 @@
</div>
</div>
{# Where confirmations land. Empty in the page and filled a toast at a time:
api/toast appends one when the copy button asks for it, and a bookmark
write sends one out of band on its own answer. templates/toast.njk is what
lands here, and each one shows and then removes itself, so this is empty
again a few seconds later.
Fixed, and outside #content, for the same reason the dialog is: it has to
survive the swap under it. It also has to survive the dialog -- a copy
confirmed by a toast inside the modal would go when the modal did.
Bottom right, and last in the document, so it covers the text rather than
moving it, and so nothing in the reading order comes after it. It sits
above the modal without being asked to: Bootstrap gives .toast a z-index of
1090 against the modal's 1055, and this container takes none of its own,
which is what leaves that comparison to the toast. #}
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toasts"></div>
</body>
-16
View File
@@ -1,21 +1,5 @@
{# The navbar's bookmark slot. Rendered into the shell with whatever mark the
worker holds, and returned on its own by api/bookmark after a write. Both
paths must produce the same element for the swap to be seamless.
Always filled: getBookmark() answers with the default when nothing has been
set and when storage cannot be read at all, so there is no empty state to
render. There was one -- a hidden slot that fetched itself once a worker
turned up -- for pages boot.html composed before there was a worker to ask.
Nothing composes a page but the worker now, and the worker always knows. #}
<span class="bookmark navbar-text d-flex align-items-center gap-1" id="bookmark">
{# A word and a colon, not a badge: this is one line of text that happens to
end in a link, and a pill in front of it read as a control. Outside the
anchor so that only the reference is underlined -- Reboot underlines an
<a>, and the label is not the part you can go to. It takes the muted
navbar colour from .navbar-text on the parent without asking. #}
<span class="bookmark-label">Bookmark:</span>
{# Not boosted: a real navigation lets the browser scroll to the verse
anchor, which a swap into #content would not do. #}
<a class="bookmark-link link-light text-truncate" href="{{ mark.href }}"
hx-boost="false">{{ mark.ref }}</a>
</span>
-2
View File
@@ -1,7 +1,5 @@
{% extends "base.njk" %}
{% block body_class %}browsing{% endblock %}
{# Already on the index, so the nav entry marks the current page instead of
offering a link to it. #}
{% block navbar_nav %}
{% endblock %}
{% block content %}{{ content | safe }}{% endblock %}
-6
View File
@@ -1,9 +1,3 @@
{# A page that could not be delivered, written in the same shape as a real
one: a title and one root element naming the template that wraps it.
Sent with 200 on purpose. htmx only swaps a 2xx body, and an error status
would leave the reader looking at the page they came from with nothing to
say what happened. #}
<title>{{ heading }}</title>
<div class="notice" data-template="page">
<h1>{{ heading }}</h1>
-1
View File
@@ -1,3 +1,2 @@
{# Fallback for a fragment that names no template of its own. #}
{% extends "base.njk" %}
{% block content %}{{ content | safe }}{% endblock %}
+2 -67
View File
@@ -1,77 +1,15 @@
{# The navbar's reference picker: which book you are in, and where in it to go.
Context: reference (or null), oob.
reference is built by sw.js from bible/verses.json, which compile_bible.py
writes from the same tree it renders the books from -- so an option can
never name an anchor that is not in the page it lands on.
It used to be built in the browser, by app.js, reading the chapters and
verses off the book already on screen. That was free, and it only worked
where there was a document to read: this shell is composed in a service
worker, which has no DOM, before any document exists. What the picker needs
is a fact on disk now, and everything below is markup and htmx.
Outside the collapse deliberately -- it is the main way of moving around a
book now that the chapter list is gone from the page, and a chapter picker
behind a hamburger is no use on a phone.
d-none for the empty state rather than the hidden attribute: Bootstrap
declares .d-flex after Reboot's [hidden] rule, both !important and at the
same specificity, so the utility would win.
Three things bring it back into step with the page, because the navbar is
never the swap target -- every boosted link lands in #content:
a navigation sw.js appends this template to the fragment with oob set,
the same way a bookmark write carries the modal's status
line, so one response moves both.
a restore htmx swaps the history element straight in and processes
no out-of-band elements at all, so the picker asks for
itself instead.
an anchor a link to GEN#v3_16 should open with the picker reading
3:16, and the fragment never reaches the worker: a browser
asks for /GEN and keeps the rest. So a picker that arrived
with the page asks once, on load, if there is an anchor to
ask about.
None of them carries the book or the anchor. The request does, in
HX-Current-URL, which htmx sets from location.href -- the address the page
is actually showing, fragment and all. Baking either into these attributes
is the one thing here that can go stale, because the picker outlives the
page it was rendered for.
settled is what stops the load trigger looping: the answer to it is a
picker already standing in the right place, and it says so by leaving the
trigger out of the copy it sends back.
!!location.hash, and not location.hash, because htmx keeps an event only
when the filter returns boolean true exactly -- it tests the result with
!== true -- and a hash is a string. Written without the coercion the
trigger is dead: every link carrying an anchor lands with the picker still
reading 1:1, which is where the worker rendered it, because a browser
keeps the fragment to itself and the request that would correct it is
never sent. #}
<div class="reference navbar-text {% if reference %}d-flex{% else %}d-none{% endif %} align-items-center gap-1 ms-3"
id="reference"
{%- if oob %} hx-swap-oob="true"{% endif %}
hx-get="api/reference"
{# htmx keeps an event only when the filter returns true exactly, so a
string is discarded. #}
hx-trigger="{% if reference and not settled %}load[!!location.hash], {% endif %}htmx:historyRestore from:body"
hx-target="this" hx-swap="outerHTML">
{%- if reference %}
<span class="reference-book text-light fw-semibold"
id="reference-book">{{ reference.book }}</span>
<label class="visually-hidden" for="reference-chapter">Chapter</label>
{# The value is the id of the element to land on, so nothing downstream has
to know how a verse anchor is spelled -- including the worker, which
reads the chapter back out of the id it sent.
Which is why both selects post under the same name. ch3 and v3_16 are
told apart by their shape, so a second name saying which select it came
from would only be repeating what the value already says.
hx-target is spelled out on both selects because the one on the div above
is "this", and an inherited "this" would resolve to the select. #}
<select class="reference-chapter form-select form-select-sm"
id="reference-chapter" name="at"
hx-get="api/reference" hx-target="#reference-verse"
@@ -83,9 +21,6 @@
</select>
<span class="reference-sep text-light">:</span>
<label class="visually-hidden" for="reference-verse">Verse</label>
{# Answers with its own options again rather than nothing, so the response
has something to hang the scroll on -- and so the option the reader chose
comes back marked selected instead of being reset by the swap. #}
<select class="reference-verse form-select form-select-sm"
id="reference-verse" name="at"
hx-get="api/reference" hx-target="this" hx-swap="innerHTML">
-29
View File
@@ -1,29 +1,3 @@
{# One confirmation: something the reader just did, said once and then gone.
Bootstrap's custom-content shape -- a flex row of body and close button,
with no toast-header above it. The header carries a title, an icon and a
timestamp, which is what a notification arriving from elsewhere needs; this
is the answer to a button pressed a second ago, and the sentence is the
whole of it.
Context: text, bad (it did not happen), oob.
Two ways in, the same element either way. api/toast renders it alone and
htmx appends it to the container; the bookmark write renders it with oob
set and it rides along on the answer that updates the navbar slot -- one
response, both halves of the page, the way the modal's status line used to.
The wrapper is how an out-of-band element with no id of its own says where
it goes: htmx appends the wrapper's children to the named container and
drops the wrapper itself.
It shows itself on arrival and removes itself once it has gone. hx-on::load
is htmx's own event, fired on content it has just inserted and processed,
so it covers both ways in -- and it is the same move base.njk makes to open
the dialog: the arrival of the content is the signal to show it. Bootstrap
owns the timer, the fade, the close button and Escape from there. Removing
the element on hidden.bs.toast is what keeps the container from filling up
with dead markup: a toast that has been read is not history, and nothing
here ever looks at it again. #}
{% if oob %}<div hx-swap-oob="beforeend:#toasts">{% endif %}
<div class="toast align-items-center{% if bad %} text-bg-danger{% endif %}"
role="alert" aria-live="assertive" aria-atomic="true"
@@ -31,9 +5,6 @@
hx-on:hidden.bs.toast="this.remove();">
<div class="d-flex">
<div class="toast-body">{{ text }}</div>
{# me-2 m-auto, not ms-auto: the row is body then button, and the body is
what grows -- the button only has to sit centred against however many
lines it turns out to be. #}
<button type="button"
class="btn-close{% if bad %} btn-close-white{% endif %} me-2 m-auto"
data-bs-dismiss="toast" aria-label="Close"></button>
-38
View File
@@ -1,24 +1,3 @@
{# The verse dialog's contents: what this verse is called, a link to it, and
the one thing you can do with it.
Rendered by sw.js for the verse that was clicked and swapped into the empty
modal-content in base.njk -- the shape htmx's bootstrap-modal example uses.
Bootstrap owns showing, dismissing, the backdrop and Escape; everything
here is what is inside.
Context: verse (ref, href, url).
All three are built in the worker, from the book in the address bar and the
anchor that was clicked. app.js used to read them off the page -- the book
out of article[data-name], the chapter out of the enclosing section -- which
put the reference in two places and left the spelling of an anchor known to
both the file that generates them and the file that scrapes them back.
The link is a field rather than a line of text because it is the fallback
for the button beside it: navigator.clipboard exists only in a secure
context, so over plain http:// from a LAN address there is nothing to write
with and a selected field is the whole of what is left. Readonly, so it
reads as something to take rather than something to fill in. #}
<div class="modal-header">
<h2 class="modal-title fs-5 verse-dialog-ref" id="verse-dialog-ref">{{ verse.ref }}</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal"
@@ -30,27 +9,10 @@
type="text" readonly value="{{ verse.url }}">
</div>
<div class="modal-footer">
{# The form is the bookmark write and nothing else. ref and href are hidden
because they are this dialog's whole subject: the reader chose them by
clicking a verse, and there is nothing here to edit. #}
<form class="verse-dialog-actions d-flex flex-wrap gap-2 m-0"
hx-post="api/bookmark" hx-target="#bookmark" hx-swap="outerHTML">
<input type="hidden" name="ref" value="{{ verse.ref }}">
<input type="hidden" name="href" value="{{ verse.href }}">
{# Selected first, written second: the selection is what makes the field
useful when the write is refused, and it costs nothing when it is not.
The toast is only raised once the clipboard has actually taken the link,
which is why the promise is answered rather than assumed.
Both outcomes are a toast, and neither is written here: api/toast is
asked for one by name, so the wording lives in the worker with the rest
of the app's words rather than in the middle of an attribute. It is the
same request the dialog itself arrives on -- htmx, the worker, a
fragment -- with the container in base.njk as the target.
Only a copy that happened ends the dialog. A refused one leaves it
standing, because the field above it, selected, is the whole of what is
left to copy with. #}
<button class="btn btn-outline-secondary" type="button"
hx-on:click="var button = this;
var field = document.getElementById('verse-dialog-url');
-3
View File
@@ -1,6 +1,3 @@
{# The verse select's options. Rendered into the picker by reference.njk, and
returned on their own by api/reference after a chapter or verse change.
Both paths must produce the same options for the swap to be seamless. #}
{% for verse in reference.verses -%}
<option value="{{ verse.id }}"
{%- if verse.id == reference.verse %} selected{% endif %}>{{ verse.label }}</option>
+1 -20
View File
@@ -1,23 +1,4 @@
{# The footer under the book list: what this device is running.
Both digests are stamped into sw.js by server.py from the files on disk:
shell covers assets/, the hand-written chrome, and library covers bible/,
the generated pages. They are the same two values the worker names its
caches after, so a device showing a digest is a device serving that cache.
It says what the worker that composed this document was, not what is
controlling right now. An update installs, claims and starts answering
without this document being rebuilt, so the value only becomes current on
the next full load -- which is the reload you would do to check it anyway.
Rendered on every page and shown under the book list alone. Populating it
only for the index would look like the smaller change and would not work:
the shell is composed once per full navigation, and a boosted link to the
book list swaps #content without rebuilding anything around it. So the page
decides, not the composer -- app.js looks for the index in #content after
every swap. The body class this was rendered with cannot be asked, for the
same reason: it goes stale the moment a link is clicked. #}
<div class="app-version d-none text-center mt-5 mb-2" id="app-version">
<div class="app-version text-center mt-5 mb-2">
<p class="app-version-digests form-text m-0">
shell {{ version.shell }} &middot; library {{ version.library }}
</p>