Bible App 1.4.34 — full source
First commit on the local Gitea server, made after moving the tree off the Windows machine. Build outputs, node_modules and the AI models stay out (see .gitignore); everything the installer build needs is tracked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* book-aliases.js — turn anything the operator types into an exact Bible reference.
|
||||
*
|
||||
* Shared by the phone remote (remote.html) and the app (app.js falls back to it in
|
||||
* __normalizeGroundingBook). Canonical names match the keys inside Translations/*.json exactly, so a
|
||||
* resolved reference can be handed straight to the loader.
|
||||
*
|
||||
* Two rules, because this gets used live on stage:
|
||||
* 1. NEVER guess between two books. "Jud" could be Judges or Jude, so it resolves to nothing and the
|
||||
* remote says "not a book" rather than opening the wrong passage in front of the room.
|
||||
* 2. Aliases are checked before prefixes, so conventional short forms win: "Phil" is Philippians (the
|
||||
* convention) even though it also prefixes Philemon.
|
||||
*
|
||||
* Collisions are impossible to introduce silently: buildIndex() throws if two books claim one alias.
|
||||
*
|
||||
* Deliberately a CLASSIC script (window.BibleBooks), not an ES module: index.html and remote.html load
|
||||
* it with a plain <script src>, and `export` syntax would be a parse error there. This package is
|
||||
* "type": "module", so Node sees a .js file as ESM and the module.exports line below is inert under
|
||||
* require() — load it with an eval of the file text if you ever need it in Node.
|
||||
*/
|
||||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
var CANON = [
|
||||
'Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth',
|
||||
'1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles', 'Ezra', 'Nehemiah',
|
||||
'Esther', 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', 'Song of Solomon', 'Isaiah', 'Jeremiah',
|
||||
'Lamentations', 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos', 'Obadiah', 'Jonah', 'Micah', 'Nahum',
|
||||
'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi', 'Matthew', 'Mark', 'Luke', 'John', 'Acts',
|
||||
'Romans', '1 Corinthians', '2 Corinthians', 'Galatians', 'Ephesians', 'Philippians', 'Colossians',
|
||||
'1 Thessalonians', '2 Thessalonians', '1 Timothy', '2 Timothy', 'Titus', 'Philemon', 'Hebrews',
|
||||
'James', '1 Peter', '2 Peter', '1 John', '2 John', '3 John', 'Jude', 'Revelation'
|
||||
];
|
||||
|
||||
/*
|
||||
* Deliberately absent, because each is ambiguous and a wrong book on stage is worse than a retype:
|
||||
* ez (Ezra / Ezekiel) jud (Judges / Jude) jo (Job/Joel/John/Jonah/Joshua)
|
||||
* ma (Matthew/Mark/Malachi) 1t (1 Thess / 1 Tim) ju (Judges / Jude) hb (Hebrews / Habakkuk)
|
||||
*/
|
||||
var ALIASES = {
|
||||
'Genesis': ['gen', 'ge', 'gn'],
|
||||
'Exodus': ['exod', 'exo', 'ex'],
|
||||
'Leviticus': ['lev', 'le', 'lv'],
|
||||
'Numbers': ['num', 'nu', 'nm', 'nb'],
|
||||
'Deuteronomy': ['deut', 'deu', 'dt', 'de'],
|
||||
'Joshua': ['josh', 'jos', 'jsh'],
|
||||
'Judges': ['judg', 'jdg', 'jdgs', 'jg'],
|
||||
'Ruth': ['ru', 'rth'],
|
||||
'1 Samuel': ['1sam', '1sa', '1sm', '1s'],
|
||||
'2 Samuel': ['2sam', '2sa', '2sm', '2s'],
|
||||
'1 Kings': ['1kings', '1kgs', '1kin', '1ki', '1k'],
|
||||
'2 Kings': ['2kings', '2kgs', '2kin', '2ki', '2k'],
|
||||
'1 Chronicles': ['1chron', '1chr', '1ch', '1cr'],
|
||||
'2 Chronicles': ['2chron', '2chr', '2ch', '2cr'],
|
||||
'Ezra': ['ezr'],
|
||||
'Nehemiah': ['neh', 'ne'],
|
||||
'Esther': ['esth', 'est', 'es'],
|
||||
'Job': ['jb'],
|
||||
'Psalms': ['psalm', 'psa', 'ps', 'pss', 'pslm', 'psm', 'pslms'],
|
||||
'Proverbs': ['prov', 'pro', 'prv', 'pr'],
|
||||
'Ecclesiastes': ['eccles', 'eccl', 'ecc', 'ec', 'qoh'],
|
||||
'Song of Solomon': ['songofsongs', 'songofsol', 'song', 'sos', 'sng', 'canticles', 'cant'],
|
||||
'Isaiah': ['isa', 'is', 'isah'],
|
||||
'Jeremiah': ['jer', 'je', 'jr'],
|
||||
'Lamentations': ['lam', 'la'],
|
||||
'Ezekiel': ['ezek', 'eze', 'ezk'],
|
||||
'Daniel': ['dan', 'da', 'dn'],
|
||||
'Hosea': ['hos', 'ho'],
|
||||
'Joel': ['joe', 'jl'],
|
||||
'Amos': ['amo', 'am'],
|
||||
'Obadiah': ['obad', 'oba', 'ob'],
|
||||
'Jonah': ['jon', 'jnh'],
|
||||
'Micah': ['mic', 'mch', 'mi'],
|
||||
'Nahum': ['nah', 'nam', 'na'],
|
||||
'Habakkuk': ['hab', 'hbk'],
|
||||
'Zephaniah': ['zeph', 'zep', 'zp'],
|
||||
'Haggai': ['hag', 'hg'],
|
||||
'Zechariah': ['zech', 'zec', 'zc'],
|
||||
'Malachi': ['mal', 'ml'],
|
||||
'Matthew': ['matt', 'mat', 'mt'],
|
||||
'Mark': ['mrk', 'mk', 'mr'],
|
||||
'Luke': ['luk', 'lk', 'lu'],
|
||||
'John': ['jhn', 'joh', 'jn'],
|
||||
'Acts': ['act', 'ac'],
|
||||
'Romans': ['rom', 'ro', 'rm'],
|
||||
'1 Corinthians': ['1cor', '1co'],
|
||||
'2 Corinthians': ['2cor', '2co'],
|
||||
'Galatians': ['gal', 'ga'],
|
||||
'Ephesians': ['ephes', 'eph', 'ep'],
|
||||
'Philippians': ['philip', 'phil', 'php', 'phi', 'pp'],
|
||||
'Colossians': ['col', 'cl'],
|
||||
'1 Thessalonians': ['1thess', '1thes', '1th'],
|
||||
'2 Thessalonians': ['2thess', '2thes', '2th'],
|
||||
'1 Timothy': ['1tim', '1ti', '1tm'],
|
||||
'2 Timothy': ['2tim', '2ti', '2tm'],
|
||||
'Titus': ['tit', 'ti'],
|
||||
'Philemon': ['philem', 'phlm', 'phm', 'pm'],
|
||||
'Hebrews': ['hebr', 'heb', 'hbr'],
|
||||
'James': ['jas', 'jam', 'jm'],
|
||||
'1 Peter': ['1pet', '1pe', '1pt', '1p'],
|
||||
'2 Peter': ['2pet', '2pe', '2pt', '2p'],
|
||||
'1 John': ['1john', '1jhn', '1jn', '1jo', '1j'],
|
||||
'2 John': ['2john', '2jhn', '2jn', '2jo', '2j'],
|
||||
'3 John': ['3john', '3jhn', '3jn', '3jo', '3j'],
|
||||
'Jude': ['jde'],
|
||||
'Revelation': ['revelations', 'rev', 'rv', 're', 'apocalypse', 'apoc']
|
||||
};
|
||||
|
||||
// Strip everything that is not a letter or digit, so "1 Cor.", "1cor" and "I Corinthians" converge.
|
||||
function key(s) { return String(s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); }
|
||||
|
||||
// Leading book number in any form the operator might type.
|
||||
function normalizeLeadingNumber(s) {
|
||||
return String(s || '')
|
||||
.replace(/^\s*(?:1st|first|i)\s+/i, '1 ')
|
||||
.replace(/^\s*(?:2nd|second|ii)\s+/i, '2 ')
|
||||
.replace(/^\s*(?:3rd|third|iii)\s+/i, '3 ')
|
||||
// roman numerals with no space: "iijohn", and "icor"
|
||||
.replace(/^\s*iii(?=[a-z])/i, '3 ')
|
||||
.replace(/^\s*ii(?=[a-z])/i, '2 ')
|
||||
.replace(/^\s*i(?=[a-z]{2})/i, '1 ');
|
||||
}
|
||||
|
||||
var INDEX = null;
|
||||
function buildIndex() {
|
||||
if (INDEX) return INDEX;
|
||||
var map = Object.create(null);
|
||||
var claim = function (alias, book) {
|
||||
var k = key(alias);
|
||||
if (!k) return;
|
||||
if (map[k] && map[k] !== book) {
|
||||
throw new Error('book alias collision: "' + k + '" claimed by both ' + map[k] + ' and ' + book);
|
||||
}
|
||||
map[k] = book;
|
||||
};
|
||||
CANON.forEach(function (book) {
|
||||
claim(book, book); // the full canonical name
|
||||
(ALIASES[book] || []).forEach(function (a) { claim(a, book); });
|
||||
});
|
||||
INDEX = map;
|
||||
return map;
|
||||
}
|
||||
|
||||
// A prefix must be at least this long to count. Every legitimate 1-2 character short form is an
|
||||
// explicit alias above ("ge", "jn", "ps", "1p"...), so this only throws away guesses: a bare "s"
|
||||
// technically prefixes nothing but "Song of Solomon", and opening that because someone typed one
|
||||
// letter is not the behaviour we want in front of a room.
|
||||
var MIN_PREFIX = 3;
|
||||
|
||||
/*
|
||||
* exact alias -> unique prefix of exactly one book -> nothing.
|
||||
* The prefix step is what makes half-typed names work ("zecha", "philipp"); "thessal" is rejected
|
||||
* because it matches two books. It never resolves when more than one book could be meant.
|
||||
*/
|
||||
function lookup(raw) {
|
||||
var map = buildIndex();
|
||||
var k = key(raw);
|
||||
if (!k) return null;
|
||||
if (map[k]) return map[k];
|
||||
if (k.length < MIN_PREFIX) return null;
|
||||
|
||||
var hits = [];
|
||||
for (var i = 0; i < CANON.length; i++) {
|
||||
if (key(CANON[i]).indexOf(k) === 0 && hits.indexOf(CANON[i]) === -1) hits.push(CANON[i]);
|
||||
}
|
||||
// One match wins. Several means a numbered book typed without its number ("corinthians") or a
|
||||
// genuinely shared start ("jud") — both stay unresolved on purpose.
|
||||
return hits.length === 1 ? hits[0] : null;
|
||||
}
|
||||
|
||||
function resolveBook(input) {
|
||||
var raw = String(input || '').trim();
|
||||
var direct = lookup(raw);
|
||||
if (direct) return direct;
|
||||
/*
|
||||
* ONLY now consider a leading roman numeral. Doing it first was a real bug: "Isaiah" begins with
|
||||
* "I", so it became "1 saiah" and resolved to 1 Samuel — the wrong book, silently. Trying the plain
|
||||
* name first means any actual book beginning with I/II/III wins before the numeral rule is reached,
|
||||
* and "I Cor" / "IITim" still work as a fallback.
|
||||
*/
|
||||
var roman = normalizeLeadingNumber(raw);
|
||||
return (roman !== raw) ? lookup(roman) : null;
|
||||
}
|
||||
|
||||
// Why a string failed, so the remote can say something useful instead of just refusing.
|
||||
function explain(input) {
|
||||
var raw = String(input || '').trim();
|
||||
var k = key(raw);
|
||||
if (!k) return 'empty';
|
||||
var hits = [];
|
||||
for (var i = 0; i < CANON.length; i++) if (key(CANON[i]).indexOf(k) === 0) hits.push(CANON[i]);
|
||||
if (hits.length > 1) return 'did you mean ' + hits.slice(0, 3).join(', ') + '?';
|
||||
return 'not a book';
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a whole reference. Accepts, among others:
|
||||
* "Heb 11.4" "Heb11:4" "1 Cor 13:4-7" "1cor13" "John 3 16" "Ps 23" "Song of Songs 2:1"
|
||||
* "II Timothy 2:15" "3jn 1" "Revelation 21:1-4"
|
||||
* Returns { book, chapter, verse, verseEnd, canonical } or null.
|
||||
*/
|
||||
function parseReference(text) {
|
||||
var s = String(text || '').trim().replace(/[‐-―]/g, '-').replace(/\s+/g, ' ');
|
||||
if (!s) return null;
|
||||
|
||||
/*
|
||||
* Take the numbers off the END rather than hunting for where the book stops. Splitting at the first
|
||||
* digit needed a leading-numeral step first, and that step mangled real books beginning with "I":
|
||||
* "Isaiah 53:5" became "1 saiah 53:5" and resolved to nothing. Anchoring at the end leaves the book
|
||||
* text untouched, so resolveBook sees "Isaiah", "1cor", "I Cor" or "ps" and decides for itself.
|
||||
*
|
||||
* The lazy (.*?) backtracks until the trailing numbers fit, which handles every shape:
|
||||
* "Isaiah 53:5" -> Isaiah / 53 / 5 "1 John 3:16" -> 1 John / 3 / 16
|
||||
* "ps23" -> ps / 23 "1cor13:4" -> 1cor / 13 / 4
|
||||
* "John 3 16" -> John / 3 / 16 "1 John" -> no match here; book-only below
|
||||
*/
|
||||
var nm = s.match(/^(.*?)\s*(\d{1,3})(?:\s*[:.\s]\s*(\d{1,3}))?(?:\s*-\s*(\d{1,3}))?\s*$/);
|
||||
|
||||
if (!nm) {
|
||||
var bookOnly = resolveBook(s);
|
||||
return bookOnly ? { book: bookOnly, chapter: null, verse: null, verseEnd: null, canonical: bookOnly } : null;
|
||||
}
|
||||
|
||||
var book = resolveBook(nm[1]);
|
||||
if (!book) return null;
|
||||
|
||||
var chapter = parseInt(nm[2], 10);
|
||||
var verse = nm[3] ? parseInt(nm[3], 10) : null;
|
||||
var verseEnd = nm[4] ? parseInt(nm[4], 10) : null;
|
||||
if (!chapter || chapter < 1) return null;
|
||||
// "Heb 11-13" without a verse is a chapter range we do not support; treat the second number as a
|
||||
// verse range only when a verse was actually given.
|
||||
if (verseEnd && !verse) verseEnd = null;
|
||||
if (verseEnd && verse && verseEnd < verse) verseEnd = null;
|
||||
|
||||
var canonical = book + ' ' + chapter + (verse ? ':' + verse + (verseEnd ? '-' + verseEnd : '') : '');
|
||||
return { book: book, chapter: chapter, verse: verse, verseEnd: verseEnd, canonical: canonical };
|
||||
}
|
||||
|
||||
var API = {
|
||||
books: CANON.slice(),
|
||||
resolveBook: resolveBook,
|
||||
parseReference: parseReference,
|
||||
explain: explain,
|
||||
_buildIndex: buildIndex
|
||||
};
|
||||
|
||||
if (typeof module === 'object' && module.exports) module.exports = API;
|
||||
root.BibleBooks = API;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this);
|
||||
Reference in New Issue
Block a user