Menu: Listen and Read entries, a bottom bar you choose, an Apps sheet with search and recents. Bible tab with the Berean Standard Bible (search, highlights, notes, synced place)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj
This commit is contained in:
15 files changed
+393
-27
No files matched your search
@@ -0,0 +1,25 @@
|
||||
// Bible: where you are reading, and your highlights and notes, are kept per person; the newest edit of each wins and two devices can add without losing each other's.
|
||||
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
|
||||
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-bible-'));
|
||||
const bible = require('../bible'), users = require('../users');
|
||||
const people = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', tabs: ['bible'], handle: 'Yg' }) };
|
||||
const hub = http.createServer((req, res) => users.runAs(req.headers['x-user'] === 'b' ? people.b : people.a, () => bible.handle(req, res))).listen(0, async () => {
|
||||
const base = 'http://127.0.0.1:' + hub.address().port + '/api/bible', j = r => r.json(), post = (p, o, h = {}) => fetch(base + p, { method: 'POST', body: JSON.stringify(o), headers: h });
|
||||
try {
|
||||
assert.deepEqual(await j(await fetch(base + '/state')), { pos: null, marks: [] });
|
||||
await post('/pos', { b: 'JHN', c: 3, v: 16, t: 100 }); let r = await j(await post('/pos', { b: 'GEN', c: 1, t: 50 })); assert.equal(r.b, 'JHN', 'the newest place wins');
|
||||
assert.deepEqual((await j(await post('/pos', { b: '../x', c: 1, t: 999 }))).b, 'JHN', 'a bad book is ignored'); assert.equal((await j(await fetch(base + '/state'))).pos.c, 3);
|
||||
r = await j(await post('/marks', [{ id: 'm1', t: 10, type: 'hl', b: 'JHN', c: 3, v: 16, color: 'green', text: 'For God so loved the world' }])); assert.equal(r.length, 1);
|
||||
r = await j(await post('/marks', [{ id: 'm2', t: 11, type: 'note', b: 'PSA', c: 23, v: 1, ve: 3, note: 'my shepherd' }, { id: 'm1', t: 5, type: 'hl', b: 'JHN', c: 3, v: 16, text: 'stale edit' }, { id: '../bad', t: 1, b: 'JHN' }, { id: 'm3', t: 1, b: 'nope' }]));
|
||||
assert.deepEqual(r.map(m => m.id).sort(), ['m1', 'm2'], 'two devices add without losing each other'); assert.equal(r.find(m => m.id === 'm1').text, 'For God so loved the world', 'an older edit does not win'); assert.equal(r.find(m => m.id === 'm2').ve, 3);
|
||||
r = await j(await post('/marks', [{ id: 'm1', t: 20, b: 'JHN', c: 3, v: 16, del: true }])); assert.deepEqual(r.map(m => m.id), ['m2'], 'deleted marks stay deleted');
|
||||
assert.deepEqual(await j(await fetch(base + '/state', { headers: { 'x-user': 'b' } })), { pos: null, marks: [] }, "each person's place and marks are their own");
|
||||
assert.equal((await fetch(base + '/nope')).status, 404);
|
||||
// the bundled text
|
||||
const d = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'public', 'vendor', 'bible', 'bsb.json'), 'utf8')); assert.equal(d.books.length, 66); assert.equal(d.books.reduce((n, b) => n + b.chapters.length, 0), 1189);
|
||||
const verses = d.books.flatMap(b => b.chapters.flat()).filter(x => x[0] === 'v').length; assert.equal(verses, 31086, 'every verse is there');
|
||||
const jn = d.books.find(b => b.id === 'JHN').chapters[2].find(x => x[0] === 'v' && x[1] === 16); assert.match(jn[2].join(''), /For God so loved the world/);
|
||||
console.log('bible test ok');
|
||||
} catch (err) { console.error(err); process.exitCode = 1; }
|
||||
hub.close();
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
# Builds public/vendor/bible/bsb.json, the Berean Standard Bible (public domain: https://berean.bible/), from the free Hello AO Bible API
|
||||
# (https://bible.helloao.org). Run once (needs a network): python3 tools/make-bsb.py
|
||||
# Compact format: {books:[{id,name,chapters:[[block,...]]}]}; a block is ["h",heading] | ["s",subtitle] | ["p"] (new paragraph) |
|
||||
# ["v",number,[part,...],[note,...]] where a part is prose text, [level,text] for a poetry line (level 1 or 2), or "\n" for a line break inside a verse.
|
||||
import json, urllib.request, concurrent.futures as cf, os, sys
|
||||
API = 'https://bible.helloao.org/api/BSB'
|
||||
def get(p):
|
||||
for t in range(4):
|
||||
try: return json.load(urllib.request.urlopen(urllib.request.Request(API + p, headers={'User-Agent': 'NE-Hub/1.0'}), timeout=40))
|
||||
except Exception as e: err = e
|
||||
raise err
|
||||
books = get('/books.json')['books']
|
||||
jobs = [(b['id'], c) for b in books for c in range(1, b['numberOfChapters'] + 1)]
|
||||
with cf.ThreadPoolExecutor(8) as ex: raw = dict(zip(jobs, ex.map(lambda j: get(f'/{j[0]}/{j[1]}.json'), jobs)))
|
||||
def txt(c): return ' '.join(p if isinstance(p, str) else p.get('text', '') for p in c).replace(' ', ' ').strip()
|
||||
def chapter(d):
|
||||
notes = {n['noteId']: n for n in d['chapter'].get('footnotes', [])}; out = []
|
||||
for x in d['chapter']['content']:
|
||||
t = x['type']
|
||||
if t == 'heading': out.append(['h', txt(x['content'])])
|
||||
elif t == 'hebrew_subtitle': out.append(['s', txt(x['content'])])
|
||||
elif t == 'line_break': out.append(['p'])
|
||||
elif t == 'verse':
|
||||
parts, ns = [], []
|
||||
for p in x['content']:
|
||||
if isinstance(p, str): parts.append(p)
|
||||
elif 'noteId' in p and p['noteId'] in notes: ns.append(notes[p['noteId']]['text']); parts.append(len(ns))
|
||||
elif 'lineBreak' in p: parts.append('\n')
|
||||
elif 'poem' in p: parts.append([p['poem'], p['text']])
|
||||
elif 'text' in p: parts.append(p['text'])
|
||||
out.append(['v', x['number'], parts] + ([ns] if ns else []))
|
||||
return out
|
||||
bible = {'id': 'BSB', 'name': 'Berean Standard Bible', 'license': 'Public domain (dedicated by Bible Hub / Berean Bible, April 30, 2023)', 'source': 'https://berean.bible/', 'books': [
|
||||
{'id': b['id'], 'name': b['commonName'], 'chapters': [chapter(raw[(b['id'], c)]) for c in range(1, b['numberOfChapters'] + 1)]} for b in books]}
|
||||
dest = os.path.join(os.path.dirname(__file__), '..', 'public', 'vendor', 'bible', 'bsb.json'); os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
json.dump(bible, open(dest, 'w'), ensure_ascii=False, separators=(',', ':')); print('wrote', dest, os.path.getsize(dest), 'bytes;', sum(len(b['chapters']) for b in bible['books']), 'chapters')
|
||||
Reference in new issue
Block a user