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">
|
<p className="countdown">
|
||||||
A new verse is appointed in <strong>{countdown}</strong>
|
A new verse is appointed in <strong>{countdown}</strong>
|
||||||
</p>
|
</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>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Link, Navigate, useLocation, useParams } from 'react-router-dom'
|
import { Link, Navigate, useLocation, useParams } from 'react-router-dom'
|
||||||
import { findBook, getBooks, loadBible, type KJVBookData } from './bible'
|
import { findBook, getBooks, loadBible, type KJVBookData } from './bible'
|
||||||
|
import { loadRedLetter, redLetterFor } from './redletter'
|
||||||
|
import RedText from './redtext'
|
||||||
import TopBar from './TopBar'
|
import TopBar from './TopBar'
|
||||||
|
|
||||||
const FONT_KEY = 'wbf-font-size'
|
const FONT_KEY = 'wbf-font-size'
|
||||||
@@ -30,7 +32,7 @@ export default function Reader() {
|
|||||||
const chapterData = valid && bookData ? bookData.chapters[chapterNum - 1] : undefined
|
const chapterData = valid && bookData ? bookData.chapters[chapterNum - 1] : undefined
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadBible().then(() => setLoaded(true))
|
Promise.all([loadBible(), loadRedLetter()]).then(() => setLoaded(true))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -160,7 +162,7 @@ export default function Reader() {
|
|||||||
{chapterData?.verses.map((v) => (
|
{chapterData?.verses.map((v) => (
|
||||||
<p className="verse-line" id={`v${v.verse}`} key={v.verse}>
|
<p className="verse-line" id={`v${v.verse}`} key={v.verse}>
|
||||||
<sup>{v.verse}</sup>
|
<sup>{v.verse}</sup>
|
||||||
{v.text}
|
<RedText text={v.text} entry={redLetterFor(bookData.book, chapterNum, v.verse)} />
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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);
|
--ink-faint: rgba(160, 138, 104, 0.55);
|
||||||
--accent: #f0a94f;
|
--accent: #f0a94f;
|
||||||
--accent-soft: #ffd9a0;
|
--accent-soft: #ffd9a0;
|
||||||
|
--red: #d05f5a;
|
||||||
--line: rgba(240, 169, 79, 0.28);
|
--line: rgba(240, 169, 79, 0.28);
|
||||||
--serif: 'EB Garamond', Georgia, 'Times New Roman', serif;
|
--serif: 'EB Garamond', Georgia, 'Times New Roman', serif;
|
||||||
--display: 'Cinzel', 'EB Garamond', Georgia, serif;
|
--display: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||||
@@ -612,6 +613,10 @@ a {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jesus {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
.verse-line.flash {
|
.verse-line.flash {
|
||||||
animation: verse-flash 2s ease-out;
|
animation: verse-flash 2s ease-out;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-6
@@ -1,4 +1,5 @@
|
|||||||
import { getBooks, loadBible, type Verse } from './bible'
|
import { getBooks, loadBible, type Verse } from './bible'
|
||||||
|
import { loadRedLetter, redLetterFor } from './redletter'
|
||||||
|
|
||||||
const ROLLOVER_LOCAL_HOUR = 2
|
const ROLLOVER_LOCAL_HOUR = 2
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ export interface DailyVerse extends Verse {
|
|||||||
verse: number
|
verse: number
|
||||||
}
|
}
|
||||||
|
|
||||||
let flatNT: DailyVerse[] | null = null
|
let flatRed: DailyVerse[] | null = null
|
||||||
|
|
||||||
export function daySeed(now: Date = new Date()): number {
|
export function daySeed(now: Date = new Date()): number {
|
||||||
const shifted = new Date(now.getTime() - ROLLOVER_LOCAL_HOUR * 3600_000)
|
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[]> {
|
export async function loadKJV(): Promise<DailyVerse[]> {
|
||||||
if (flatNT) return flatNT
|
if (flatRed) return flatRed
|
||||||
await loadBible()
|
await Promise.all([loadBible(), loadRedLetter()])
|
||||||
flatNT = []
|
flatRed = []
|
||||||
let inNewTestament = false
|
let inNewTestament = false
|
||||||
for (const book of getBooks()) {
|
for (const book of getBooks()) {
|
||||||
if (book.book === 'Matthew') inNewTestament = true
|
if (book.book === 'Matthew') inNewTestament = true
|
||||||
@@ -50,9 +51,17 @@ export async function loadKJV(): Promise<DailyVerse[]> {
|
|||||||
for (const chapter of book.chapters) {
|
for (const chapter of book.chapters) {
|
||||||
const chapterNumber = Number(chapter.chapter)
|
const chapterNumber = Number(chapter.chapter)
|
||||||
for (const v of chapter.verses) {
|
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