diff --git a/App.js b/App.js
index d2bcbbd..f447214 100644
--- a/App.js
+++ b/App.js
@@ -30,6 +30,8 @@ import NewGroup from './Views/NewGroup.js';
import Slideshow from './Views/Slideshow.js';
import SongPlayer from './Views/SongPlayer.js';
import GlobalChat from './Views/GlobalChat.js';
+import BiblePicker from './Views/BiblePicker.js';
+import BibleChapterView from './Views/BibleChapterView.js';
import { Platform } from 'react-native';
import { PostHogProvider } from 'posthog-react-native'
import * as Updates from 'expo-updates';
@@ -426,6 +428,15 @@ export default function App() {
name="GlobalChat"
component={GlobalChat}
/>
+
+
diff --git a/Views/BibleChapterView.js b/Views/BibleChapterView.js
new file mode 100644
index 0000000..1013a58
--- /dev/null
+++ b/Views/BibleChapterView.js
@@ -0,0 +1,155 @@
+import React from "react";
+import { FlatList, Pressable, View } from "react-native";
+import { ActivityIndicator, Text } from "react-native-paper";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useNavigation } from "@react-navigation/native";
+import { parseBibleReference } from "../utils/bibleReferences.js";
+import GlobalState from "../contexts/GlobalState.js";
+
+const BibleChapterView = ({ route }) => {
+ const navigation = useNavigation();
+ const reference = route?.params?.reference || "";
+ const selectable = route?.params?.selectable === true;
+ const { chapterReference, verse: selectedVerse } = parseBibleReference(reference);
+ const [loading, setLoading] = React.useState(true);
+ const [error, setError] = React.useState("");
+ const [chapterData, setChapterData] = React.useState(null);
+ const listRef = React.useRef(null);
+ const autoScrolledRef = React.useRef(false);
+
+ React.useEffect(() => {
+ let mounted = true;
+ const loadChapter = async () => {
+ setLoading(true);
+ setError("");
+ try {
+ const response = await fetch(`https://bible-api.com/${encodeURIComponent(chapterReference)}`);
+ if (!response.ok) throw new Error("Failed chapter request");
+ const payload = await response.json();
+ if (!mounted) return;
+ setChapterData(payload);
+ } catch (_error) {
+ if (!mounted) return;
+ setError("Unable to load chapter.");
+ setChapterData(null);
+ } finally {
+ if (mounted) setLoading(false);
+ }
+ };
+ loadChapter();
+ return () => {
+ mounted = false;
+ };
+ }, [chapterReference]);
+
+ const verses = Array.isArray(chapterData?.verses) ? chapterData.verses : [];
+ const selectedVerseNumber = Number(selectedVerse || 1);
+ const selectedIndex = verses.findIndex((item) => Number(item?.verse || 0) === selectedVerseNumber);
+
+ React.useEffect(() => {
+ autoScrolledRef.current = false;
+ }, [chapterReference, selectedVerseNumber]);
+
+ const scrollToSelectedVerse = React.useCallback((animated = false) => {
+ if (autoScrolledRef.current) return;
+ if (!listRef.current || selectedIndex < 0 || !verses.length) return;
+ try {
+ listRef.current.scrollToIndex({
+ index: selectedIndex,
+ animated,
+ viewPosition: 0.35,
+ });
+ autoScrolledRef.current = true;
+ } catch (_error) {
+ // FlatList can throw before enough measurements are available.
+ }
+ }, [selectedIndex, verses.length]);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+
+ const handleVersePress = (verseNumber) => {
+ if (!selectable) return;
+ GlobalState.bibleChapterSelection = {
+ ...parseBibleReference(`${chapterReference}:${verseNumber}`),
+ ts: Date.now(),
+ };
+ navigation.goBack();
+ };
+
+ return (
+
+
+
+ {chapterData?.reference || chapterReference}
+
+
+ {chapterData?.translation_name || chapterData?.translation_id || "KJV"}
+
+ {selectable ? (
+ Tap a verse to select it.
+ ) : null}
+ `${item?.verse || idx}`}
+ initialNumToRender={24}
+ onLayout={() => {
+ setTimeout(() => scrollToSelectedVerse(false), 40);
+ }}
+ onContentSizeChange={() => {
+ setTimeout(() => scrollToSelectedVerse(false), 40);
+ }}
+ onScrollToIndexFailed={({ index, averageItemLength }) => {
+ if (!listRef.current) return;
+ listRef.current.scrollToOffset({
+ offset: Math.max(0, (averageItemLength || 36) * index),
+ animated: false,
+ });
+ setTimeout(() => {
+ scrollToSelectedVerse(false);
+ }, 120);
+ }}
+ renderItem={({ item }) => {
+ const verseNumber = Number(item?.verse || 0);
+ const isSelected = verseNumber === selectedVerseNumber;
+ return (
+ handleVersePress(verseNumber)}
+ style={{
+ paddingVertical: 8,
+ paddingHorizontal: 10,
+ borderRadius: 8,
+ marginBottom: 6,
+ backgroundColor: isSelected ? "#fff3cd" : "transparent",
+ borderWidth: isSelected ? 1 : 0,
+ borderColor: isSelected ? "#f59e0b" : "transparent",
+ }}
+ >
+
+ {verseNumber}
+
+ {item?.text || ""}
+
+ );
+ }}
+ />
+
+
+ );
+};
+
+export default BibleChapterView;
diff --git a/Views/BiblePicker.js b/Views/BiblePicker.js
new file mode 100644
index 0000000..6ae86e3
--- /dev/null
+++ b/Views/BiblePicker.js
@@ -0,0 +1,252 @@
+import React from "react";
+import { FlatList, Pressable, View } from "react-native";
+import { ActivityIndicator, Button, Chip, Divider, Text, TextInput } from "react-native-paper";
+import { useNavigation } from "@react-navigation/native";
+import { useSnapshot } from "valtio";
+import GlobalState from "../contexts/GlobalState.js";
+import { BIBLE_BOOKS, createBibleToken, fetchBiblePassage, getBookChapterCount } from "../utils/bibleReferences.js";
+
+const BiblePicker = ({ route }) => {
+ const navigation = useNavigation();
+ const gState = useSnapshot(GlobalState);
+ const target = route?.params?.target || "post";
+ const [query, setQuery] = React.useState(route?.params?.initialReference || "");
+ const [chapter, setChapter] = React.useState("1");
+ const [verse, setVerse] = React.useState("1");
+ const [selectedBook, setSelectedBook] = React.useState("");
+ const [activeStep, setActiveStep] = React.useState("book");
+ const [preview, setPreview] = React.useState(null);
+ const [loadingPreview, setLoadingPreview] = React.useState(false);
+ const [loadingVerses, setLoadingVerses] = React.useState(false);
+ const [error, setError] = React.useState("");
+ const [verseOptions, setVerseOptions] = React.useState(["1"]);
+ const chapterSelectionTs = gState?.bibleChapterSelection?.ts;
+
+ const computedReference = React.useMemo(() => {
+ if (query.trim()) return query.trim();
+ if (!selectedBook) return "";
+ return `${selectedBook} ${chapter || "1"}:${verse || "1"}`;
+ }, [chapter, query, selectedBook, verse]);
+
+ const filteredBooks = React.useMemo(() => {
+ const q = query.toLowerCase().trim();
+ if (!q) return BIBLE_BOOKS;
+ return BIBLE_BOOKS.filter((book) => book.toLowerCase().includes(q));
+ }, [query]);
+
+ const selectedBookChapterCount = React.useMemo(() => getBookChapterCount(selectedBook), [selectedBook]);
+ const chapterOptions = React.useMemo(
+ () => Array.from({ length: selectedBookChapterCount }, (_v, i) => String(i + 1)),
+ [selectedBookChapterCount]
+ );
+
+ const addReferenceToCaller = (reference) => {
+ const cleanReference = String(reference || "").trim();
+ if (!cleanReference) return;
+ GlobalState.biblePickerSelection = {
+ token: createBibleToken(cleanReference),
+ reference: cleanReference,
+ target,
+ ts: Date.now(),
+ };
+ navigation.goBack();
+ };
+
+ const loadPreviewForReference = async (reference) => {
+ if (!reference) return;
+ setError("");
+ setLoadingPreview(true);
+ try {
+ const passage = await fetchBiblePassage(reference);
+ setPreview(passage);
+ } catch (_err) {
+ setPreview(null);
+ setError("Unable to load this Bible passage.");
+ } finally {
+ setLoadingPreview(false);
+ }
+ };
+
+ const loadVerseOptions = async (book, chapterNumber) => {
+ if (!book || !chapterNumber) {
+ setVerseOptions(["1"]);
+ return;
+ }
+ setLoadingVerses(true);
+ try {
+ const response = await fetch(`https://bible-api.com/${encodeURIComponent(`${book} ${chapterNumber}`)}`);
+ if (!response.ok) throw new Error("Failed chapter fetch");
+ const payload = await response.json();
+ const options = Array.isArray(payload?.verses)
+ ? payload.verses.map((v) => String(v?.verse || "")).filter(Boolean)
+ : [];
+ setVerseOptions(options.length ? options : ["1"]);
+ } catch (_error) {
+ setVerseOptions(["1"]);
+ } finally {
+ setLoadingVerses(false);
+ }
+ };
+
+ React.useEffect(() => {
+ const picked = gState?.bibleChapterSelection;
+ if (!picked || !picked.book) return;
+ setSelectedBook(picked.book);
+ setChapter(String(picked.chapter || 1));
+ setVerse(String(picked.verse || 1));
+ setQuery("");
+ setActiveStep("verse");
+ loadVerseOptions(picked.book, String(picked.chapter || 1));
+ loadPreviewForReference(picked.reference || `${picked.book} ${picked.chapter || 1}:${picked.verse || 1}`);
+ GlobalState.bibleChapterSelection = null;
+ }, [chapterSelectionTs]);
+
+ return (
+
+ Bible Reference
+
+ Pick a reference to insert into your {target === "chat" ? "chat message" : "post"}.
+
+
+
+
+
+
+
+
+ {computedReference ? Selected: {computedReference} : null}
+ {preview?.text ? (
+ navigation.navigate("BibleChapter", { reference: computedReference, selectable: true })}
+ style={{ marginTop: 10, padding: 10, backgroundColor: "#f8fafc", borderRadius: 10 }}
+ >
+ {preview.text.slice(0, 420)}
+
+ {preview.reference} ({preview.translation})
+
+ Tap preview to pick verse from chapter
+
+ ) : null}
+ {error ? {error} : null}
+
+
+
+
+
+
+
+
+
+ {activeStep === "book" ? (
+ <>
+ Books
+ item}
+ renderItem={({ item }) => (
+ {
+ setSelectedBook(item);
+ setQuery("");
+ setChapter("1");
+ setVerse("1");
+ setActiveStep("chapter");
+ await loadVerseOptions(item, "1");
+ await loadPreviewForReference(`${item} 1:1`);
+ }}
+ >
+ {item}
+
+ )}
+ />
+ >
+ ) : null}
+
+ {activeStep === "chapter" ? (
+ <>
+
+ Chapters {selectedBook ? `(${selectedBook})` : ""}
+
+ item}
+ renderItem={({ item }) => (
+ {
+ setChapter(item);
+ setVerse("1");
+ setActiveStep("verse");
+ await loadVerseOptions(selectedBook, item);
+ await loadPreviewForReference(`${selectedBook} ${item}:1`);
+ }}
+ >
+ {item}
+
+ )}
+ />
+ >
+ ) : null}
+
+ {activeStep === "verse" ? (
+ <>
+
+ Verses {selectedBook && chapter ? `(${selectedBook} ${chapter})` : ""}
+
+ {loadingVerses ? (
+
+ ) : (
+ item}
+ renderItem={({ item }) => (
+ {
+ setVerse(item);
+ await loadPreviewForReference(`${selectedBook} ${chapter || "1"}:${item}`);
+ }}
+ >
+ {item}
+
+ )}
+ />
+ )}
+ >
+ ) : null}
+
+ );
+};
+
+export default BiblePicker;
diff --git a/Views/GlobalChat.js b/Views/GlobalChat.js
index de3bd6c..2f46640 100644
--- a/Views/GlobalChat.js
+++ b/Views/GlobalChat.js
@@ -8,6 +8,8 @@ import { useSnapshot } from "valtio";
import API from "../API.js";
import GlobalState from "../contexts/GlobalState.js";
import i18n from "../i18nMessages.js";
+import BibleEmbeddedView from "../components/BibleEmbeddedView.js";
+import { stripBibleTokens } from "../utils/bibleReferences.js";
const PANEL_WIDTH = 260;
@@ -59,7 +61,8 @@ const CircleIconAction = ({ icon, onPress, color = "#6b7280", disabled = false }
const ChatMessage = ({ item, myProfileId, showOriginal, onPressIn, onPressOut }) => {
const isMine = item?.senderProfileId === myProfileId;
const isTranslated = !!(item?.textOriginal && item?.text && item.textOriginal !== item.text);
- const messageText = isTranslated && showOriginal ? item?.textOriginal : item?.text;
+ const messageTextRaw = isTranslated && showOriginal ? item?.textOriginal : item?.text;
+ const messageText = stripBibleTokens(messageTextRaw || "");
const youtubeVideoId = getYouTubeVideoIdFromText(item?.textOriginal || "") || getYouTubeVideoIdFromText(item?.text || "");
const createdAt = item?.createdAt ? new Date(item.createdAt) : null;
return (
@@ -90,6 +93,9 @@ const ChatMessage = ({ item, myProfileId, showOriginal, onPressIn, onPressOut })
{createdAt && !Number.isNaN(createdAt.getTime()) ? createdAt.toLocaleTimeString() : ""}
+
+
+
{youtubeVideoId ? (
{
+let GlobalChat = ({ navigation }) => {
const gState = useSnapshot(GlobalState);
const myProfileId = gState?.me?._id || "";
const [messages, setMessages] = React.useState([]);
@@ -133,6 +139,7 @@ let GlobalChat = () => {
const panelOpacity = React.useRef(new Animated.Value(0)).current;
const listRef = React.useRef(null);
const hasDoneInitialScrollRef = React.useRef(false);
+ const biblePickerSelectionTs = gState?.biblePickerSelection?.ts;
const scrollToBottom = React.useCallback((animated = true) => {
if (!listRef.current) return;
@@ -188,6 +195,16 @@ let GlobalChat = () => {
};
}, [loadMessages, refreshPresence]);
+ React.useEffect(() => {
+ const selection = gState?.biblePickerSelection;
+ if (!selection || selection.target !== "chat" || !selection.token) return;
+ setText((prev) => {
+ if ((prev || "").includes(selection.token)) return prev;
+ return `${(prev || "").trim()} ${selection.token}`.trim();
+ });
+ GlobalState.biblePickerSelection = null;
+ }, [biblePickerSelectionTs, gState?.biblePickerSelection]);
+
const sendMessage = async () => {
const trimmedText = (text || "").trim();
if (!trimmedText || sending) return;
@@ -343,7 +360,7 @@ let GlobalChat = () => {
elevation: 2,
}}
>
- { }} />
+ navigation.navigate("BiblePicker", { target: "chat" })} />
{
+ const gState = useSnapshot(GlobalState);
let [postContent, setPostContent] = useState('');
- let initialContent = props.route.params.intialContent;
- let [extraContent, setExtraContent] = useState([initialContent]);
+ let initialContent = props.route?.params?.intialContent;
+ let [extraContent, setExtraContent] = useState(initialContent ? [initialContent] : []);
let [toProfile, setToProfile] = useState([]);
const [photo, setPhoto] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false); // Variable to prevent posting during upload
const [cancelUpload, setCancelUpload] = useState(false); // Variable to handle upload cancellation
+ const [bibleReferences, setBibleReferences] = useState([]);
const navigation = useNavigation();
+ const biblePickerSelectionTs = gState?.biblePickerSelection?.ts;
useEffect(() => {
let subscribed = true;
const getProfileData = async () => {
- if (!props.route.params?.toProfile) return;
+ if (!props.route?.params?.toProfile) return;
try {
// Fetch profile data and update state if component is still subscribed
const profileObj = await API.getUserProfile(props.route.params.toProfile);
@@ -44,7 +49,31 @@ let NewPostView = (props) => {
// Cleanup subscription flag
subscribed = false;
}
- }, [props.route.params?.sendNow]);
+ }, [props.route?.params?.sendNow]);
+
+ useEffect(() => {
+ const selection = gState?.biblePickerSelection;
+ if (!selection || selection.target !== "post" || !selection.token) return;
+ if (selection.reference) {
+ setBibleReferences((prev) => {
+ if (prev.includes(selection.reference)) return prev;
+ return prev.concat(selection.reference);
+ });
+ }
+ GlobalState.biblePickerSelection = null;
+ }, [biblePickerSelectionTs]);
+
+ const handlePostContentChange = (nextValue = "") => {
+ const detectedReferences = extractBibleReferences(nextValue);
+ if (detectedReferences.length) {
+ setBibleReferences((prev) => {
+ const seen = new Set(prev);
+ detectedReferences.forEach((reference) => seen.add(reference));
+ return Array.from(seen);
+ });
+ }
+ setPostContent(stripBibleTokens(nextValue));
+ };
const pickImage = async () => {
try {
@@ -142,10 +171,15 @@ let NewPostView = (props) => {
return;
}
try {
+ const bibleTokens = bibleReferences.map((reference) => createBibleToken(reference)).filter(Boolean);
// Create a new post with the combined content
- await API.newPost(postContent + " " + extraContent.join(" "), props.route.params?.toProfile);
+ await API.newPost(
+ [postContent, extraContent.join(" "), bibleTokens.join(" ")].join(" ").trim(),
+ props.route?.params?.toProfile
+ );
setPostContent(''); // Clear post content after submission
setExtraContent([]); // Clear extra content after submission
+ setBibleReferences([]);
navigation.navigate('Feed', { reRender: Math.random() }); // Navigate back to the Feed and trigger re-render
} catch (error) {
console.error("Error creating new post", error);
@@ -177,7 +211,7 @@ let NewPostView = (props) => {
{/* Text input for post content */}
{
}}
autoFocus={true}
/>
+ {bibleReferences.length ? (
+
+ {bibleReferences.map((reference) => (
+ navigation.navigate("BibleChapter", { reference })}
+ onClose={() => {
+ setBibleReferences((prev) => prev.filter((item) => item !== reference));
+ }}
+ >
+ {reference}
+
+ ))}
+
+ ) : null}
{/* Button to pick images from the gallery */}
+
diff --git a/components/BibleEmbeddedView.js b/components/BibleEmbeddedView.js
new file mode 100644
index 0000000..8af02f8
--- /dev/null
+++ b/components/BibleEmbeddedView.js
@@ -0,0 +1,118 @@
+import React, { useMemo, useState } from "react";
+import { StyleSheet, Text, View } from "react-native";
+import { ActivityIndicator, Chip } from "react-native-paper";
+import { useNavigation } from "@react-navigation/native";
+import { extractBibleReferences, fetchBiblePassage } from "../utils/bibleReferences.js";
+
+const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress = false }) => {
+ const navigation = useNavigation();
+ const references = useMemo(() => extractBibleReferences(content), [content]);
+ const [selectedRef, setSelectedRef] = useState("");
+ const [byReference, setByReference] = useState({});
+
+ if (!references.length) return null;
+
+ const handleSelectReference = async (reference) => {
+ if (openChapterOnPress) {
+ navigation.navigate("BibleChapter", { reference });
+ return;
+ }
+ setSelectedRef(reference);
+ const current = byReference[reference];
+ if (current?.loading || current?.text || current?.error) return;
+
+ setByReference((prev) => ({ ...prev, [reference]: { loading: true } }));
+ try {
+ const data = await fetchBiblePassage(reference);
+ setByReference((prev) => ({ ...prev, [reference]: { loading: false, ...data } }));
+ } catch (_error) {
+ setByReference((prev) => ({ ...prev, [reference]: { loading: false, error: true } }));
+ }
+ };
+
+ const selectedData = selectedRef ? byReference[selectedRef] : null;
+
+ return (
+
+ Bible
+
+ {references.map((reference) => (
+ handleSelectReference(reference)}
+ >
+ {reference}
+
+ ))}
+
+ {selectedData?.loading ? : null}
+ {selectedData?.text ? (
+
+ {selectedData.text.slice(0, compact ? 160 : 280)}
+
+ {selectedData.reference} ({selectedData.translation})
+
+
+ ) : null}
+ {selectedData?.error ? Unable to load this passage. : null}
+
+ );
+};
+
+export default BibleEmbeddedView;
+
+const styles = StyleSheet.create({
+ container: {
+ paddingTop: 6,
+ paddingHorizontal: 8,
+ },
+ compactContainer: {
+ paddingTop: 4,
+ paddingHorizontal: 0,
+ },
+ label: {
+ fontSize: 12,
+ color: "#4b5563",
+ fontWeight: "700",
+ marginBottom: 4,
+ },
+ chipsWrap: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ },
+ chip: {
+ marginRight: 6,
+ marginBottom: 6,
+ backgroundColor: "#f8fafc",
+ },
+ loader: {
+ alignSelf: "flex-start",
+ },
+ previewBox: {
+ marginTop: 2,
+ backgroundColor: "#f8fafc",
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ borderRadius: 10,
+ padding: 8,
+ },
+ previewText: {
+ fontSize: 13,
+ color: "#111827",
+ },
+ previewMeta: {
+ marginTop: 4,
+ fontSize: 11,
+ color: "#6b7280",
+ fontWeight: "600",
+ },
+ errorText: {
+ marginTop: 2,
+ fontSize: 12,
+ color: "#b91c1c",
+ },
+});
diff --git a/components/Post.js b/components/Post.js
index fc00b08..38f2db3 100644
--- a/components/Post.js
+++ b/components/Post.js
@@ -14,6 +14,8 @@ import ProfilePhotoCircle from './ProfilePhotoCircle.js';
import { posthog } from './../PostHog.js';
import { useNavigation } from '@react-navigation/native';
import ParsedText from 'react-native-parsed-text';
+import BibleEmbeddedView from './BibleEmbeddedView.js';
+import { stripBibleTokens } from '../utils/bibleReferences.js';
let Post = (props) => {
@@ -30,7 +32,7 @@ let Post = (props) => {
const SWIPE_WIDTH = 86;
let toProfileText = post.toProfile && post.toProfile !== post.profileid ?
: undefined;
- let cleanContent = post.content.replace(/@[A-z]+:.+\w/g, '').trim();
+ let cleanContent = stripInlineTags(stripBibleTokens(post.content));
const navigation = useNavigation();
//cleanContent = convertLinks(cleanContent);
const newComentAdded = (commentData) => {
@@ -172,6 +174,9 @@ let Post = (props) => {
{cleanContent}
+
+
+
{
@@ -356,3 +361,10 @@ const styles = StyleSheet.create({
textDecorationLine: 'underline',
},
});
+ const stripInlineTags = (content = "") => {
+ return String(content || "")
+ .replace(/@[A-Za-z]+:[^\s]+/g, "")
+ .replace(/[ \t]{2,}/g, " ")
+ .replace(/[ \t]+\n/g, "\n")
+ .trim();
+ };
diff --git a/contexts/GlobalState.js b/contexts/GlobalState.js
index 3cf6e4f..169e207 100644
--- a/contexts/GlobalState.js
+++ b/contexts/GlobalState.js
@@ -6,6 +6,8 @@ const GlobalState = proxy({
profiles: {},
currentMedia: '',
mediaPost: {},
+ biblePickerSelection: null,
+ bibleChapterSelection: null,
});
export default GlobalState;
diff --git a/utils/bibleReferences.js b/utils/bibleReferences.js
new file mode 100644
index 0000000..27fb4f1
--- /dev/null
+++ b/utils/bibleReferences.js
@@ -0,0 +1,229 @@
+const BIBLE_TOKEN_REGEX = /@bible:([^\s]+)/gi;
+
+const normalizeReference = (value = "") => {
+ try {
+ return decodeURIComponent(String(value || ""))
+ .replace(/_/g, " ")
+ .replace(/\s+/g, " ")
+ .trim();
+ } catch (_error) {
+ return String(value || "").replace(/_/g, " ").replace(/\s+/g, " ").trim();
+ }
+};
+
+export const encodeBibleReference = (reference = "") => {
+ return String(reference || "").replace(/\s+/g, "_").trim();
+};
+
+export const createBibleToken = (reference = "") => {
+ const encoded = encodeBibleReference(reference);
+ if (!encoded) return "";
+ return `@bible:${encoded}`;
+};
+
+export const extractBibleReferences = (content = "") => {
+ const seen = new Set();
+ const refs = [];
+ if (!content || typeof content !== "string") return refs;
+
+ let match;
+ while ((match = BIBLE_TOKEN_REGEX.exec(content)) !== null) {
+ const normalized = normalizeReference(match[1]);
+ if (!normalized || seen.has(normalized)) continue;
+ seen.add(normalized);
+ refs.push(normalized);
+ }
+ return refs;
+};
+
+export const stripBibleTokens = (content = "") => {
+ if (!content || typeof content !== "string") return "";
+ return content
+ .replace(BIBLE_TOKEN_REGEX, "")
+ .replace(/[ \t]{2,}/g, " ")
+ .replace(/[ \t]+\n/g, "\n")
+ .trim();
+};
+
+export const fetchBiblePassage = async (reference = "") => {
+ const safeReference = normalizeReference(reference);
+ if (!safeReference) {
+ throw new Error("Missing Bible reference");
+ }
+ const response = await fetch(`https://bible-api.com/${encodeURIComponent(safeReference)}`);
+ if (!response.ok) {
+ throw new Error("Failed to load Bible passage");
+ }
+ const payload = await response.json();
+ return {
+ reference: payload?.reference || safeReference,
+ text: (payload?.text || "").trim(),
+ translation: payload?.translation_name || payload?.translation_id || "KJV",
+ };
+};
+
+export const parseBibleReference = (reference = "") => {
+ const normalized = normalizeReference(reference);
+ const match = normalized.match(/^(.*)\s+(\d+)(?::(\d+)(?:-\d+)?)?$/);
+ if (!match) {
+ return {
+ reference: normalized,
+ book: normalized,
+ chapter: 1,
+ verse: 1,
+ chapterReference: normalized,
+ };
+ }
+ const book = (match[1] || "").trim();
+ const chapter = Number(match[2] || "1");
+ const verse = Number(match[3] || "1");
+ return {
+ reference: normalized,
+ book,
+ chapter,
+ verse,
+ chapterReference: `${book} ${chapter}`,
+ };
+};
+
+export const BIBLE_BOOKS = [
+ "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",
+];
+
+export const BIBLE_BOOK_CHAPTERS = {
+ Genesis: 50,
+ Exodus: 40,
+ Leviticus: 27,
+ Numbers: 36,
+ Deuteronomy: 34,
+ Joshua: 24,
+ Judges: 21,
+ Ruth: 4,
+ "1 Samuel": 31,
+ "2 Samuel": 24,
+ "1 Kings": 22,
+ "2 Kings": 25,
+ "1 Chronicles": 29,
+ "2 Chronicles": 36,
+ Ezra: 10,
+ Nehemiah: 13,
+ Esther: 10,
+ Job: 42,
+ Psalms: 150,
+ Proverbs: 31,
+ Ecclesiastes: 12,
+ "Song of Solomon": 8,
+ Isaiah: 66,
+ Jeremiah: 52,
+ Lamentations: 5,
+ Ezekiel: 48,
+ Daniel: 12,
+ Hosea: 14,
+ Joel: 3,
+ Amos: 9,
+ Obadiah: 1,
+ Jonah: 4,
+ Micah: 7,
+ Nahum: 3,
+ Habakkuk: 3,
+ Zephaniah: 3,
+ Haggai: 2,
+ Zechariah: 14,
+ Malachi: 4,
+ Matthew: 28,
+ Mark: 16,
+ Luke: 24,
+ John: 21,
+ Acts: 28,
+ Romans: 16,
+ "1 Corinthians": 16,
+ "2 Corinthians": 13,
+ Galatians: 6,
+ Ephesians: 6,
+ Philippians: 4,
+ Colossians: 4,
+ "1 Thessalonians": 5,
+ "2 Thessalonians": 3,
+ "1 Timothy": 6,
+ "2 Timothy": 4,
+ Titus: 3,
+ Philemon: 1,
+ Hebrews: 13,
+ James: 5,
+ "1 Peter": 5,
+ "2 Peter": 3,
+ "1 John": 5,
+ "2 John": 1,
+ "3 John": 1,
+ Jude: 1,
+ Revelation: 22,
+};
+
+export const getBookChapterCount = (book = "") => {
+ return BIBLE_BOOK_CHAPTERS[book] || 1;
+};