Add the Browser tab: a live web page with no browser chrome

A new tab (globe, after Maps) shows one web page full-bleed in the content area —
no address bar, bookmarks or scrollbars — built for It Is Written but usable with
any page. It loads at startup and keeps running in the background.

Stage (browser-tab.js, browser-guest-preload.cjs):
- An Electron <webview> per Browser tab, in its own sandboxed process and persistent
  session. The pages live in one persistent layer inside .tab-content: every tab
  switch rebuilds .tab-content and moving a webview reloads it, so app.js now clears
  around that layer (clearTabContentKeepingBrowser).
- Tapping a text field brings up the Prayer tab's keyboard (createPrayerKeyboard with
  a sink) and types into the page with trusted input; Enter submits; the keyboard puts
  itself away when the page moves focus on (e.g. after a search). The OS touch
  keyboard is suppressed with inputmode="none" while a field is focused.
- Tapping a scripture reference opens it in a new Bible tab, in the translation
  printed beside it when that one is switched on. References are found with the app's
  own scanner: scripture-scan.js gains scanAll() (every match with its offsets),
  translationAfter() and isTranslation(); scanText/parseSlide are unchanged.
- Input inside a webview never reaches the app, so the page reports activity (idle
  splash) and five-finger touches (blank the screen) itself.
- main.cjs: webviewTag on; every webview is forced sandboxed, context-isolated, no Node,
  http(s) only, with the guest preload. No new windows (opened in place), no permission
  prompts, no downloads, no pinch-zoom, no alert/confirm/prompt boxes, and "Leave
  site?" can never block Reload or Start page. The session reports plain Chrome.
- server.js CSP: frame-src allows http(s) (a webview is a frame to CSP); style-src and
  font-src allow https (the follower's srcdoc copy inherits this policy).

Everywhere a tab kind is listed:
- app.js: create at startup and on Reset, icon, visibility defaults, + menu, fixed
  mode, no ink layer, tool handling, OSC kind + information line (the page's search
  term), phone remote scroll / tap / page buttons, OSC show / reload / home actions,
  live setting changes, translation for tapped references.
- quick-dock, tool-wheel, presentation-tools: no drawing tools on the tab.
- Phone remote: tab label, one finger scrolls the page (taps still tap), hint text;
  mirror-stream.js reports the page's scroll for the phone's local preview.
- Follower screen: the stage publishes a script-free copy of the page's DOM (only while
  a follower is linked), its scroll and form fields; the follower shows it in
  sandboxed, double-buffered frames; the relay keeps the page for late joiners.
- OSC: /bibleapp/browser, /bibleapp/browser/reload, /bibleapp/browser/home;
  tab_kind/browser feedback. Admin and phone allow-lists updated.
- Settings: Tab Visibility, a Browser Tab section (start page, page size, scripture
  taps, return to start page), Service Profiles carry its settings, OSC references.
- Admin console: show the tab, reload / start page, start page and page size.
- BROWSER_TAB.md documents it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:25:15 -04:00
co-authored by Claude Opus 5.5
parent a0c3921edd
commit e7e527197f
20 changed files with 1780 additions and 60 deletions
+48 -1
View File
@@ -9,6 +9,9 @@
* scanText(text) — the FIRST "Book chapter:verse(-verse)" inside free prose ("…as it says in
* Rev. 21:4, God will…"). Needs the colon (or "Heb 11.4"), so a chapter-only
* mention in a sentence never fires — too ambiguous on stage.
* scanAll(text) — EVERY such reference, each with the [start, end) it occupies in the text as
* given. Same rules as scanText; used by the Browser tab to find the reference
* under a finger on a web page.
*
* Book names go through window.BibleBooks (book-aliases.js), which refuses to guess between two books.
* One extra rule here: a one/two-letter alias found in prose ("is", "am", "re") only counts when it is
@@ -100,6 +103,49 @@
return null;
}
/*
* scanText's rules, applied to the whole text, keeping WHERE each reference sits. The normalisation is
* character-for-character (a dash stays one dash, every odd space stays one space — nothing collapses,
* nothing is trimmed), so start/end index the caller's own string and map straight back onto the text
* nodes of a web page. `start` skips punctuation glued to the book ("(John 3:16" starts at "John").
*/
function scanAll(text) {
var B = books();
var s = String(text || '').replace(/[‐-―−]/g, '-').replace(/[  -​\s]/g, ' ');
var out = [];
if (!B || !s.trim()) return out;
var re = new RegExp(CHAPTER_VERSE.source, 'g'); // never share scanText's lastIndex
var m;
while ((m = re.exec(s))) {
var before = s.slice(0, m.index).replace(/[\s"'“”‘’(\[]+$/, '');
var found = null;
// Longest run first, exactly as scanText: "1 John" before "John", "Song of Songs" before "Songs".
for (var n = 4; n >= 1 && !found; n--) {
var tail = before.match(new RegExp('(\\S+(?:\\s+\\S+){' + (n - 1) + '})$'));
if (!tail) continue;
var run = tail[1].split(/\s+/);
var head = run[0].replace(/^[^A-Za-z0-9]+/, '');
if (!(/^[A-Za-z]/.test(head) || BOOK_NUMBER.test(head.replace(/[^A-Za-z0-9]/g, '')))) continue;
var book = resolveCandidate(run);
if (book) found = { book: book, start: tail.index + (run[0].length - head.length) };
}
if (!found) continue;
var ref = makeRef(found.book, m[1], m[2], m[3] ? null : m[4]);
if (ref) out.push({ ref: ref, start: found.start, end: m.index + m[0].length });
}
return out;
}
// The translation printed right after a reference — "John 3:16 (NIV)", "Isaiah 41:10NIV" — or null.
var TRANSLATION_AFTER = new RegExp('^\\s*[-,:;]?\\s*[\\(\\[]?\\s*(' + TRANSLATIONS + ')(?![A-Za-z0-9])', 'i');
function translationAfter(text, index) {
var m = String(text || '').slice(index || 0).match(TRANSLATION_AFTER);
return m ? m[1].toUpperCase() : null;
}
function isTranslation(code) {
return new RegExp('^(?:' + TRANSLATIONS + ')$', 'i').test(String(code || '').trim());
}
function parseField(text) {
var B = books();
var t = stripTranslation(text);
@@ -147,7 +193,8 @@
return null;
}
var API = { parseField: parseField, scanText: scanText, parseSlide: parseSlide, stripTranslation: stripTranslation };
var API = { parseField: parseField, scanText: scanText, scanAll: scanAll, parseSlide: parseSlide, stripTranslation: stripTranslation,
translationAfter: translationAfter, isTranslation: isTranslation };
if (typeof module === 'object' && module.exports) module.exports = API;
root.ScriptureScan = API;
})(typeof globalThis !== 'undefined' ? globalThis : this);