red letter
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -105,7 +105,7 @@ export default function Home() {
|
||||
<p className="countdown">
|
||||
A new verse is appointed in <strong>{countdown}</strong>
|
||||
</p>
|
||||
<p className="foot-note">King James Version · Public Domain · The New Testament</p>
|
||||
<p className="foot-note">King James Version · Public Domain · Red Letter Edition</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Link, Navigate, useLocation, useParams } from 'react-router-dom'
|
||||
import { findBook, getBooks, loadBible, type KJVBookData } from './bible'
|
||||
import { loadRedLetter, redLetterFor } from './redletter'
|
||||
import RedText from './redtext'
|
||||
import TopBar from './TopBar'
|
||||
|
||||
const FONT_KEY = 'wbf-font-size'
|
||||
@@ -30,7 +32,7 @@ export default function Reader() {
|
||||
const chapterData = valid && bookData ? bookData.chapters[chapterNum - 1] : undefined
|
||||
|
||||
useEffect(() => {
|
||||
loadBible().then(() => setLoaded(true))
|
||||
Promise.all([loadBible(), loadRedLetter()]).then(() => setLoaded(true))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -160,7 +162,7 @@ export default function Reader() {
|
||||
{chapterData?.verses.map((v) => (
|
||||
<p className="verse-line" id={`v${v.verse}`} key={v.verse}>
|
||||
<sup>{v.verse}</sup>
|
||||
{v.text}
|
||||
<RedText text={v.text} entry={redLetterFor(bookData.book, chapterNum, v.verse)} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
export type RedLetterEntry = string | null
|
||||
|
||||
let loaded = false
|
||||
let promise: Promise<void> | null = null
|
||||
let map: Map<string, string | null> = new Map()
|
||||
|
||||
export function loadRedLetter(): Promise<void> {
|
||||
if (loaded) return Promise.resolve()
|
||||
if (!promise) {
|
||||
promise = fetch('/red_letter.json')
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`Failed to load red letter data (${r.status})`)
|
||||
return r.json() as Promise<{ verses: Record<string, string> }>
|
||||
})
|
||||
.then((data) => {
|
||||
map = new Map()
|
||||
for (const [ref, value] of Object.entries(data.verses)) {
|
||||
map.set(ref.toLowerCase(), value === 'full' ? null : value)
|
||||
}
|
||||
loaded = true
|
||||
})
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
export function redLetterFor(book: string, chapter: number, verse: number): RedLetterEntry | undefined {
|
||||
const name = book === 'Psalms' ? 'Psalm' : book
|
||||
return map.get(`${name} ${chapter}:${verse}`.toLowerCase())
|
||||
}
|
||||
|
||||
function normalize(s: string): string {
|
||||
let out = ''
|
||||
for (const c of s) {
|
||||
if (/\s/.test(c)) out += ' '
|
||||
else if (/[a-z0-9]/i.test(c)) out += c.toLowerCase()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function spansOf(text: string): Array<{ ch: string; i: number }> {
|
||||
const arr: Array<{ ch: string; i: number }> = []
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i]
|
||||
if (/\s/.test(c)) arr.push({ ch: ' ', i })
|
||||
else if (/[a-z0-9]/i.test(c)) arr.push({ ch: c.toLowerCase(), i })
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
export function redLetterSpan(text: string, quote: string): [number, number] | null {
|
||||
const raw = text.indexOf(quote)
|
||||
if (raw >= 0) return [raw, raw + quote.length]
|
||||
|
||||
const arr = spansOf(text)
|
||||
const normText = arr.map((o) => o.ch).join('')
|
||||
const normQuote = normalize(quote)
|
||||
const qlen = normQuote.length
|
||||
if (!qlen || qlen > normText.length) return null
|
||||
|
||||
const idx = normText.indexOf(normQuote)
|
||||
if (idx >= 0) return [arr[idx].i, arr[idx + qlen - 1].i + 1]
|
||||
|
||||
let best = -1
|
||||
let bestScore = Infinity
|
||||
for (let i = 0; i + qlen <= normText.length; i++) {
|
||||
let diff = 0
|
||||
for (let j = 0; j < qlen; j++) {
|
||||
if (normText[i + j] !== normQuote[j]) diff++
|
||||
}
|
||||
if (diff < bestScore) {
|
||||
bestScore = diff
|
||||
best = i
|
||||
if (diff === 0) break
|
||||
}
|
||||
}
|
||||
if (bestScore <= 2) return [arr[best].i, arr[best + qlen - 1].i + 1]
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { redLetterSpan, type RedLetterEntry } from './redletter'
|
||||
|
||||
export default function RedText({ text, entry }: { text: string; entry: RedLetterEntry | undefined }) {
|
||||
if (entry === null) return <span className="jesus">{text}</span>
|
||||
if (typeof entry === 'string') {
|
||||
const span = redLetterSpan(text, entry)
|
||||
if (span) {
|
||||
const [s, e] = span
|
||||
return (
|
||||
<>
|
||||
{text.slice(0, s)}
|
||||
<span className="jesus">{text.slice(s, e)}</span>
|
||||
{text.slice(e)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
return <>{text}</>
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
--ink-faint: rgba(160, 138, 104, 0.55);
|
||||
--accent: #f0a94f;
|
||||
--accent-soft: #ffd9a0;
|
||||
--red: #d05f5a;
|
||||
--line: rgba(240, 169, 79, 0.28);
|
||||
--serif: 'EB Garamond', Georgia, 'Times New Roman', serif;
|
||||
--display: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||
@@ -612,6 +613,10 @@ a {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.jesus {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.verse-line.flash {
|
||||
animation: verse-flash 2s ease-out;
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,4 +1,5 @@
|
||||
import { getBooks, loadBible, type Verse } from './bible'
|
||||
import { loadRedLetter, redLetterFor } from './redletter'
|
||||
|
||||
const ROLLOVER_LOCAL_HOUR = 2
|
||||
|
||||
@@ -8,7 +9,7 @@ export interface DailyVerse extends Verse {
|
||||
verse: number
|
||||
}
|
||||
|
||||
let flatNT: DailyVerse[] | null = null
|
||||
let flatRed: DailyVerse[] | null = null
|
||||
|
||||
export function daySeed(now: Date = new Date()): number {
|
||||
const shifted = new Date(now.getTime() - ROLLOVER_LOCAL_HOUR * 3600_000)
|
||||
@@ -39,9 +40,9 @@ export function pickForDay(verses: DailyVerse[], seed: number): DailyVerse {
|
||||
}
|
||||
|
||||
export async function loadKJV(): Promise<DailyVerse[]> {
|
||||
if (flatNT) return flatNT
|
||||
await loadBible()
|
||||
flatNT = []
|
||||
if (flatRed) return flatRed
|
||||
await Promise.all([loadBible(), loadRedLetter()])
|
||||
flatRed = []
|
||||
let inNewTestament = false
|
||||
for (const book of getBooks()) {
|
||||
if (book.book === 'Matthew') inNewTestament = true
|
||||
@@ -50,9 +51,17 @@ export async function loadKJV(): Promise<DailyVerse[]> {
|
||||
for (const chapter of book.chapters) {
|
||||
const chapterNumber = Number(chapter.chapter)
|
||||
for (const v of chapter.verses) {
|
||||
flatNT.push({ text: v.text, reference: `${name} ${chapterNumber}:${v.verse}`, book: book.book, chapter: chapterNumber, verse: v.verse })
|
||||
const entry = redLetterFor(book.book, chapterNumber, v.verse)
|
||||
if (entry === undefined) continue
|
||||
flatRed.push({
|
||||
text: entry === null ? v.text : entry,
|
||||
reference: `${name} ${chapterNumber}:${v.verse}`,
|
||||
book: book.book,
|
||||
chapter: chapterNumber,
|
||||
verse: v.verse,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return flatNT
|
||||
return flatRed
|
||||
}
|
||||
Reference in New Issue
Block a user