Open Bible hover cards powered by your own WordPress database. No remote lookup is needed during page views.
Installed Texts
Install any combination of local Bible texts. Installed translations are automatically available in frontend tooltip tabs; the default translation only controls which one opens first.
Settings
Open Text Policy
This plugin stores only open/public texts in your local WordPress database. WEB, KJV, ASV, and YLT are available from eBible VPL packages. NIV, NASB, and most modern commercial translations should remain external/licensed integrations unless you explicitly secure rights to store and redistribute them.
Bible Reference Tooltips . Local open Bible text lookup for WordPress.
get_results("SELECT translation, COUNT(*) AS verses, MAX(source) AS source FROM " . self::table() . " GROUP BY translation ORDER BY translation ASC");
}
private function available_translations() {
return [
'WEB' => [
'name' => 'World English Bible',
'url' => 'https://ebible.org/Scriptures/eng-web_vpl.zip',
'file' => 'eng-web_vpl.txt',
'source' => 'eBible WEB VPL',
'license' => 'World English Bible public domain',
],
'KJV' => [
'name' => 'King James Version',
'url' => 'https://ebible.org/Scriptures/eng-kjv_vpl.zip',
'file' => 'eng-kjv_vpl.txt',
'source' => 'eBible KJV VPL',
'license' => 'King James Version public domain in the United States',
],
'ASV' => [
'name' => 'American Standard Version',
'url' => 'https://ebible.org/Scriptures/eng-asv_vpl.zip',
'file' => 'eng-asv_vpl.txt',
'source' => 'eBible ASV VPL',
'license' => 'American Standard Version 1901 public domain',
],
'YLT' => [
'name' => "Young's Literal Translation",
'url' => 'https://ebible.org/Scriptures/engylt_vpl.zip',
'file' => 'engylt_vpl.txt',
'source' => 'eBible YLT VPL',
'license' => "Young's Literal Translation public domain",
],
];
}
public function handle_save_settings() {
$this->require_admin();
$translation = strtoupper(sanitize_text_field($_POST['translation'] ?? $this->settings()['translation'] ?? 'WEB'));
$known = array_keys($this->available_translations());
$installed = array_map(function($row) { return $row['code']; }, $this->installed_translations());
if (!in_array($translation, array_unique(array_merge($known, $installed)), true)) $translation = 'WEB';
$theme = sanitize_key($_POST['theme'] ?? 'auto');
if (!in_array($theme, ['auto', 'dark', 'light'], true)) $theme = 'auto';
$link_target = sanitize_key($_POST['link_target'] ?? 'same');
if (!in_array($link_target, ['same', 'new'], true)) $link_target = 'same';
$logos_icon = sanitize_key($_POST['logos_icon'] ?? 'dark');
if (!in_array($logos_icon, ['dark', 'light'], true)) $logos_icon = 'dark';
$link_versions = array_keys($this->bible_link_versions());
$bible_version = strtoupper(sanitize_text_field($_POST['bible_version'] ?? $translation));
if (!in_array($bible_version, $link_versions, true)) $bible_version = $translation;
$logos_translation = strtoupper(sanitize_text_field($_POST['logos_translation'] ?? $translation));
if (!in_array($logos_translation, $link_versions, true)) $logos_translation = $translation;
$settings = [
'enabled' => !empty($_POST['enabled']) ? '1' : '0',
'translation' => $translation,
'scope' => sanitize_textarea_field($_POST['scope'] ?? self::defaults()['scope']),
'theme' => $theme,
'underline' => !empty($_POST['underline']) ? '1' : '0',
'show_translation' => !empty($_POST['show_translation']) ? '1' : '0',
'show_tooltips' => !empty($_POST['show_tooltips']) ? '1' : '0',
'link_references' => !empty($_POST['link_references']) ? '1' : '0',
'link_target' => $link_target,
'bible_version' => $bible_version,
'logos_insert' => !empty($_POST['logos_insert']) ? '1' : '0',
'logos_existing' => !empty($_POST['logos_existing']) ? '1' : '0',
'logos_translation' => $logos_translation,
'logos_icon' => $logos_icon,
'add_tooltips_to_links' => !empty($_POST['add_tooltips_to_links']) ? '1' : '0',
'case_sensitive' => !empty($_POST['case_sensitive']) ? '1' : '0',
'tag_chapters' => !empty($_POST['tag_chapters']) ? '1' : '0',
'scan_comments' => !empty($_POST['scan_comments']) ? '1' : '0',
'skip_tags' => $this->normalize_skip_tags($_POST['skip_tags'] ?? []),
'max_range' => (string)max(1, min(50, absint($_POST['max_range'] ?? 12))),
];
update_option(self::OPTION_KEY, $settings, false);
$this->redirect('Settings saved.');
}
public function handle_clear() {
$this->require_admin();
global $wpdb;
$wpdb->query("TRUNCATE TABLE " . self::table());
self::log_event('texts_cleared', [], 'Bible verse table cleared.');
$this->redirect('Verse table cleared.');
}
public function handle_import_web() {
$this->require_admin();
$available = $this->available_translations();
$bulk_action = sanitize_key($_POST['bulk_action'] ?? 'install_selected');
$selected = array_map(function($code) {
return strtoupper(preg_replace('/[^A-Z0-9_-]/i', '', sanitize_text_field($code)));
}, (array)($_POST['translations'] ?? []));
if (!$selected && !empty($_POST['translation'])) {
$selected[] = strtoupper(preg_replace('/[^A-Z0-9_-]/i', '', sanitize_text_field($_POST['translation'])));
}
$selected = array_values(array_filter(array_unique($selected), function($code) use ($available) {
return isset($available[$code]);
}));
if ($bulk_action === 'install_all') $selected = array_keys($available);
if ($bulk_action === 'clear_all') {
global $wpdb;
$wpdb->query("TRUNCATE TABLE " . self::table());
self::log_event('texts_removed_all', [], 'All installed Bible texts removed.');
$this->redirect('All installed Bible texts removed.');
}
if (!$selected) $this->redirect('No Bible translations selected.');
if ($bulk_action === 'remove_selected') {
$removed = $this->remove_translations($selected);
$remaining = $this->installed_translations();
$this->set_active_translation($this->default_translation($this->settings()['translation'], $remaining));
$this->redirect('Removed ' . number_format_i18n($removed) . ' verse rows from selected translation(s).');
}
if ($bulk_action === 'set_default') {
$this->set_active_translation($selected[0]);
$this->redirect($selected[0] . ' is now the default tooltip translation.');
}
$messages = [];
foreach ($selected as $translation) $messages[] = $this->import_vpl_translation($translation, false);
$this->set_active_translation($selected[0]);
$this->redirect(implode(' ', $messages));
}
private function remove_translations($translations) {
global $wpdb;
$count = 0;
foreach ((array)$translations as $translation) {
$count += (int)$wpdb->delete(self::table(), ['translation' => strtoupper((string)$translation)], ['%s']);
}
self::log_event('texts_removed', ['translations' => array_values((array)$translations), 'rows' => $count], 'Removed Bible text translations.');
return $count;
}
private function require_admin() {
if (!current_user_can('manage_options')) wp_die('Insufficient permissions.');
check_admin_referer(self::NONCE);
}
private function redirect($msg) {
wp_safe_redirect(add_query_arg('msg', rawurlencode($msg), admin_url('admin.php?page=' . self::SLUG)));
exit;
}
private function set_active_translation($translation) {
$settings = $this->settings();
$settings['translation'] = $translation;
update_option(self::OPTION_KEY, $settings, false);
}
private function import_vpl_translation($translation, $make_default = true) {
$translations = $this->available_translations();
if (empty($translations[$translation])) return 'Unknown translation requested.';
$info = $translations[$translation];
if (!class_exists('ZipArchive')) return 'ZipArchive is not available on this server.';
require_once ABSPATH . 'wp-admin/includes/file.php';
$tmp = download_url($info['url'], 120);
if (is_wp_error($tmp)) return 'Download failed: ' . $tmp->get_error_message();
$dir = wp_tempnam('brt-vpl');
if (file_exists($dir)) @unlink($dir);
wp_mkdir_p($dir);
$zip = new ZipArchive();
if ($zip->open($tmp) !== true) {
@unlink($tmp);
return 'Could not open downloaded ' . $translation . ' archive.';
}
$zip->extractTo($dir);
$zip->close();
@unlink($tmp);
$file = trailingslashit($dir) . $info['file'];
if (!file_exists($file)) return $translation . ' VPL file was not found in archive.';
$handle = fopen($file, 'r');
if (!$handle) return 'Could not read ' . $translation . ' VPL file.';
global $wpdb;
$wpdb->delete(self::table(), ['translation' => $translation], ['%s']);
$batch = [];
$count = 0;
while (($line = fgets($handle)) !== false) {
$parsed = $this->parse_vpl_line($line);
if (!$parsed) continue;
$batch[] = [$translation, $parsed['book']['abbr'], $parsed['book']['name'], $parsed['chapter'], $parsed['verse'], $parsed['text']];
if (count($batch) >= 400) {
$count += $this->insert_rows($batch, $info['source'], $info['license']);
$batch = [];
}
}
fclose($handle);
if ($batch) $count += $this->insert_rows($batch, $info['source'], $info['license']);
if ($make_default) $this->set_active_translation($translation);
self::log_event('text_imported', ['translation' => $translation, 'verses' => $count], 'Imported Bible text.');
return $translation . ' import complete: ' . number_format_i18n($count) . ' verses installed.';
}
private function import_vpl_book($translation, $target_abbr) {
$translations = $this->available_translations();
if (empty($translations[$translation])) return 0;
$info = $translations[$translation];
if (!class_exists('ZipArchive')) return 0;
require_once ABSPATH . 'wp-admin/includes/file.php';
$tmp = download_url($info['url'], 120);
if (is_wp_error($tmp)) return 0;
$dir = wp_tempnam('brt-vpl-book');
if (file_exists($dir)) @unlink($dir);
wp_mkdir_p($dir);
$zip = new ZipArchive();
if ($zip->open($tmp) !== true) {
@unlink($tmp);
return 0;
}
$zip->extractTo($dir);
$zip->close();
@unlink($tmp);
$file = trailingslashit($dir) . $info['file'];
if (!file_exists($file)) {
$this->delete_temp_dir($dir);
return 0;
}
$handle = fopen($file, 'r');
if (!$handle) {
$this->delete_temp_dir($dir);
return 0;
}
$batch = [];
$count = 0;
while (($line = fgets($handle)) !== false) {
$parsed = $this->parse_vpl_line($line);
if (!$parsed || strtoupper($parsed['book']['abbr']) !== strtoupper($target_abbr)) continue;
$batch[] = [$translation, $parsed['book']['abbr'], $parsed['book']['name'], $parsed['chapter'], $parsed['verse'], $parsed['text']];
if (count($batch) >= 200) {
$count += $this->insert_rows($batch, $info['source'], $info['license']);
$batch = [];
}
}
fclose($handle);
if ($batch) $count += $this->insert_rows($batch, $info['source'], $info['license']);
$this->delete_temp_dir($dir);
return $count;
}
private function delete_temp_dir($dir) {
if (!is_dir($dir)) return;
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
foreach ($it as $file) {
$file->isDir() ? @rmdir($file->getRealPath()) : @unlink($file->getRealPath());
}
@rmdir($dir);
}
private function parse_vpl_line($line) {
$line = trim((string)$line);
if ($line === '') return null;
if (!preg_match('/^((?:[1-3]\s*)?[A-Za-z][A-Za-z0-9 .\'-]{1,40}?)\s+(\d{1,3}):(\d{1,3})\s+(.+)$/u', $line, $m)) return null;
$book = $this->normalize_book($m[1]) ?: ['abbr' => strtoupper(preg_replace('/[^A-Z0-9]/i', '', $m[1])), 'name' => $this->book_name_from_abbr($m[1])];
$text = trim(wp_specialchars_decode($m[4], ENT_QUOTES));
if ($text === '') return null;
return [
'book' => $book,
'chapter' => (int)$m[2],
'verse' => (int)$m[3],
'text' => $text,
];
}
private function insert_rows($rows, $source, $license) {
global $wpdb;
$count = 0;
foreach ($rows as $row) {
$result = $wpdb->replace(self::table(), [
'translation' => $row[0],
'book' => $row[1],
'book_name' => $row[2],
'chapter' => (int)$row[3],
'verse' => (int)$row[4],
'verse_text' => $row[5],
'source' => $source,
'license' => $license,
'created_at' => current_time('mysql'),
], ['%s','%s','%s','%d','%d','%s','%s','%s','%s']);
if ($result !== false) $count++;
}
return $count;
}
private function repair_book_aliases() {
if (get_option('brt_alias_repair_version') === self::VERSION) return;
global $wpdb;
$repairs = [
'JAM' => ['JAS', 'James'],
'JMS' => ['JAS', 'James'],
'JM' => ['JAS', 'James'],
'JA' => ['JAS', 'James'],
'JAC' => ['JAS', 'James'],
'JACOB' => ['JAS', 'James'],
'EPISTLEOFJAMES' => ['JAS', 'James'],
'THEEPISTLEOFJAMES' => ['JAS', 'James'],
'JOH' => ['JHN', 'John'],
'JOE' => ['JOL', 'Joel'],
'NAH' => ['NAM', 'Nahum'],
'PHI' => ['PHP', 'Philippians'],
'PHL' => ['PHM', 'Philemon'],
];
foreach ($repairs as $old => $new) {
$wpdb->query($wpdb->prepare(
"UPDATE IGNORE " . self::table() . " SET book = %s, book_name = %s WHERE book = %s",
$new[0],
$new[1],
$old
));
$wpdb->query($wpdb->prepare(
"UPDATE " . self::table() . " SET book_name = %s WHERE book = %s",
$new[1],
$new[0]
));
}
$name_repairs = [
'jam' => ['JAS', 'James'],
'jms' => ['JAS', 'James'],
'jm' => ['JAS', 'James'],
'ja' => ['JAS', 'James'],
'jas' => ['JAS', 'James'],
'jac' => ['JAS', 'James'],
'jacob' => ['JAS', 'James'],
'epistleofjames' => ['JAS', 'James'],
'theepistleofjames' => ['JAS', 'James'],
'joh' => ['JHN', 'John'],
'joe' => ['JOL', 'Joel'],
'nah' => ['NAM', 'Nahum'],
'phi' => ['PHP', 'Philippians'],
'phl' => ['PHM', 'Philemon'],
];
foreach ($name_repairs as $old_name => $new) {
$wpdb->query($wpdb->prepare(
"UPDATE IGNORE " . self::table() . " SET book = %s, book_name = %s WHERE LOWER(REPLACE(REPLACE(REPLACE(book_name, ' ', ''), '.', ''), '-', '')) = %s",
$new[0],
$new[1],
$old_name
));
}
update_option('brt_alias_repair_version', self::VERSION, false);
}
private function repair_missing_james_verses() {
if (get_option('brt_james_backfill_version') === self::VERSION) return;
global $wpdb;
$installed = $wpdb->get_col("SELECT DISTINCT translation FROM " . self::table() . " ORDER BY translation ASC");
$available = $this->available_translations();
$repaired = [];
foreach ((array)$installed as $translation) {
$translation = strtoupper((string)$translation);
if (empty($available[$translation])) continue;
$count = (int)$wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM " . self::table() . " WHERE translation = %s AND (book IN ('JAS','JAM','JMS','JM','JA','JAC') OR book_name IN ('James','JAS','JAM','Jms','JMS','Jacob'))",
$translation
));
if ($count > 0) continue;
$inserted = $this->import_vpl_book($translation, 'JAS');
if ($inserted > 0) $repaired[$translation] = $inserted;
}
update_option('brt_james_backfill_version', self::VERSION, false);
if ($repaired) {
self::log_event('james_backfilled', ['translations' => $repaired], 'Backfilled missing James verses for installed Bible translations.');
}
}
private function book_name_from_abbr($abbr) {
$abbr = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string)$abbr));
foreach ($this->book_map() as $book) {
if ($book['abbr'] === $abbr) return $book['name'];
}
return $abbr;
}
private function admin_css() {
return '.brt-admin{max-width:1500px}.brt-lede{font-size:15px;color:#56657a}.brt-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.brt-card{background:#f8fafc;border:1px solid #cbd5e1;border-radius:8px;padding:20px;box-shadow:0 12px 28px rgba(15,23,42,.08)}.brt-card h2{margin-top:0}.brt-wide{grid-column:1/-1}.brt-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}.brt-card label{display:block;margin:0 0 12px;font-weight:700}.brt-card input:not([type=checkbox]),.brt-card textarea,.brt-card select{width:100%;max-width:100%;border-radius:6px;min-height:38px}.brt-card label span,.brt-table span{display:block;margin-top:5px;color:#64748b;font-size:12px;font-weight:500}.brt-current{background:#e8eef8;border:1px solid #cbd5e1;border-radius:6px;margin:0 0 14px;padding:10px 12px}.brt-check{display:flex!important;gap:8px;align-items:center}.brt-row{display:grid;grid-template-columns:1fr 220px;gap:12px}.brt-skip-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px 14px;margin:8px 0 16px}.brt-bulkbar{display:flex;gap:10px;align-items:center;margin:14px 0}.brt-bulkbar select{max-width:250px}.brt-table td,.brt-table th{vertical-align:middle}.brt-muted{color:#94a3b8!important}.brt-pill{display:inline-flex!important;margin:0!important;padding:4px 8px;border-radius:999px;background:#dbeafe;color:#1d4ed8!important;font-weight:800!important;font-size:11px!important}.brt-library .description{margin-top:10px}.brt-footer{color:#64748b;font-size:12px;margin:18px 0 0}@media(max-width:900px){.brt-grid,.brt-row,.brt-skip-grid{grid-template-columns:1fr}.brt-bulkbar{align-items:stretch;flex-direction:column}.brt-bulkbar select{max-width:none}}';
}
private function frontend_css() {
return '.brt-ref{cursor:help;text-decoration:underline;text-decoration-style:dotted;text-underline-offset:3px;color:inherit;font-weight:700}.brt-ref:focus{outline:2px solid rgba(47,128,237,.45);outline-offset:2px}.brt-ref.no-underline{text-decoration:none}.brt-logos-link{display:inline-flex;align-items:center;justify-content:center;width:1.15em;height:1.15em;margin-left:.2em;border:1px solid currentColor;border-radius:.25em;font:800 .68em/1 system-ui,-apple-system,Segoe UI,sans-serif;text-decoration:none;vertical-align:.08em;opacity:.78}.brt-logos-link:hover,.brt-logos-link:focus{opacity:1;text-decoration:none}.brt-logos-dark{color:#162033;background:#fff}.brt-logos-light{color:#fff;background:#162033}.brt-pop{position:fixed;z-index:999999;max-width:min(520px,calc(100vw - 28px));padding:16px;border-radius:12px;border:1px solid rgba(148,163,184,.45);background:#fff;color:#0f172a;box-shadow:0 20px 50px rgba(15,23,42,.18);font:14px/1.55 system-ui,-apple-system,Segoe UI,sans-serif;pointer-events:auto;opacity:0;transform:translateY(5px);transition:opacity .22s ease,transform .22s ease}.brt-pop.ready{opacity:1;transform:translateY(0)}.brt-pop.dark{background:#080b10;color:#eef4ff;box-shadow:0 20px 60px rgba(0,0,0,.42)}.brt-pop.light{background:#fff;color:#0f172a;box-shadow:0 20px 50px rgba(15,23,42,.18)}.brt-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 10px}.brt-title{font-weight:800;font-size:15px;color:inherit;white-space:nowrap}.brt-tabs{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}.brt-tab{border:1px solid rgba(148,163,184,.45);border-radius:999px;background:rgba(148,163,184,.12);color:inherit;cursor:pointer;font:700 11px/1 system-ui,-apple-system,Segoe UI,sans-serif;padding:5px 8px}.brt-tab.active{background:#2f80ed;border-color:#2f80ed;color:#fff}.brt-pop.dark .brt-tab.active{background:#60a5fa;border-color:#60a5fa;color:#06101f}.brt-verse{margin:0 0 9px}.brt-verse:last-child{margin-bottom:0}.brt-verse-num{font-weight:900;margin-right:5px}.brt-foot{display:block;margin-top:11px;color:#64748b;font-size:12px}.brt-pop.dark .brt-foot{color:#93a4bb}.brt-pop .err{color:#b91c1c}.brt-pop.dark .err{color:#fca5a5}.brt-loading{display:flex;align-items:center;gap:10px;margin:3px 0 1px;color:#64748b}.brt-pop.dark .brt-loading{color:#93a4bb}.brt-spinner{width:14px;height:14px;border:2px solid rgba(148,163,184,.35);border-top-color:#2f80ed;border-radius:999px;animation:brtSpin .8s linear infinite}@keyframes brtSpin{to{transform:rotate(360deg)}}@media(max-width:520px){.brt-head{align-items:flex-start;flex-direction:column}.brt-tabs{justify-content:flex-start}}';
}
private function frontend_js() {
return <<<'JS'
(function(){
if (!window.BibleReferenceTooltips || !BibleReferenceTooltips.rest) return;
const bookAliases = [
'Genesis','Gen','Ge','Gn','Exodus','Exod','Exo','Ex','Leviticus','Lev','Le','Lv','Numbers','Num','Nu','Nm','Nb','Deuteronomy','Deut','Deu','Dt',
'Joshua','Josh','Jos','Judges','Judg','Jdg','Jg','Ruth','Rut','Ru','1\\s*Samuel','1\\s*Sam','1\\s*Sa','2\\s*Samuel','2\\s*Sam','2\\s*Sa',
'1\\s*Kings','1\\s*Kgs','1\\s*Ki','2\\s*Kings','2\\s*Kgs','2\\s*Ki','1\\s*Chronicles','1\\s*Chron','1\\s*Chr','2\\s*Chronicles','2\\s*Chron','2\\s*Chr',
'Ezra','Ezr','Nehemiah','Neh','Ne','Esther','Est','Job','Psalms','Psalm','Psa','Ps','Proverbs','Prov','Pro','Pr','Ecclesiastes','Eccl','Ecc',
'Song\\s*of\\s*Solomon','Song\\s*of\\s*Songs','Song','Sos','Sng','Isaiah','Isa','Jeremiah','Jer','Lamentations','Lam','Ezekiel','Ezek','Ezk',
'Daniel','Dan','Dn','Hosea','Hos','Joel','Jol','Joe','Jl','Amos','Amo','Obadiah','Obad','Oba','Jonah','Jon','Micah','Mic','Nahum','Nah','Nam','Habakkuk','Hab','Zephaniah','Zeph','Zep',
'Haggai','Hag','Zechariah','Zech','Zec','Malachi','Mal','Matthew','Matt','Mat','Mt','Mark','Mrk','Mk','Luke','Luk','Lk','John','Jhn','Jn',
'Acts','Act','Romans','Rom','Ro','1\\s*Corinthians','1\\s*Cor','1\\s*Co','2\\s*Corinthians','2\\s*Cor','2\\s*Co','Galatians','Gal','Ephesians','Eph',
'Philippians','Phil','Php','Colossians','Col','1\\s*Thessalonians','1\\s*Thess','1\\s*Th','2\\s*Thessalonians','2\\s*Thess','2\\s*Th',
'1\\s*Timothy','1\\s*Tim','1\\s*Ti','2\\s*Timothy','2\\s*Tim','2\\s*Ti','Titus','Tit','Philemon','Phlm','Phm','Hebrews','Heb',
'James\\.?','Jas\\.?','Ja\\.?','Jam\\.?','Jms\\.?','Jm\\.?','Jac\\.?','Jacob','Epistle\\s*of\\s*James','1\\s*Peter','1\\s*Pet','1\\s*Pe','2\\s*Peter','2\\s*Pet','2\\s*Pe','1\\s*John','1\\s*Jn','1\\s*Jhn',
'2\\s*John','2\\s*Jn','2\\s*Jhn','3\\s*John','3\\s*Jn','3\\s*Jhn','Jude','Jud','Revelation','Rev','Re'
];
const book = '(?:' + bookAliases.sort((a,b) => b.length - a.length).join('|') + ')';
const chapterPattern = BibleReferenceTooltips.tagChapters ? '(?:\\s*:\\s*\\d{1,3}(?:\\s*[-\\u2013\\u2014]\\s*\\d{1,3})?)?' : '\\s*:\\s*\\d{1,3}(?:\\s*[-\\u2013\\u2014]\\s*\\d{1,3})?';
const refPattern = '\\b(' + book + ')\\s+\\d{1,3}' + chapterPattern + '\\b';
const refFlags = BibleReferenceTooltips.caseSensitive ? 'g' : 'gi';
const refRe = new RegExp(refPattern, refFlags);
const refTestRe = new RegExp(refPattern, BibleReferenceTooltips.caseSensitive ? '' : 'i');
const configuredSkip = Array.isArray(BibleReferenceTooltips.skipTags) ? BibleReferenceTooltips.skipTags : [];
const skip = new Set(['A','SCRIPT','STYLE','TEXTAREA','INPUT','SELECT','BUTTON','CODE','PRE','NOSCRIPT','IFRAME','OBJECT','EMBED','SVG','MATH'].concat(configuredSkip.map(tag => String(tag || '').toUpperCase())));
const preferenceKey = 'brtTranslation';
const cache = new Map();
let pop = null;
let activeRef = '';
let activeTrigger = null;
let hideTimer = null;
let loadingTimer = null;
let requestToken = 0;
function getStoredPreference(){
try { return window.localStorage ? (localStorage.getItem(preferenceKey) || '') : ''; }
catch(e) { return ''; }
}
function setStoredPreference(code){
try { if (window.localStorage) localStorage.setItem(preferenceKey, code); }
catch(e) {}
}
function preferredTranslation(){
const available = Array.isArray(BibleReferenceTooltips.translations) ? BibleReferenceTooltips.translations.map(row => String(row.code || '').toUpperCase()).filter(Boolean) : [];
const stored = String(getStoredPreference()).toUpperCase();
if (stored && (!available.length || available.includes(stored))) return stored;
return String(BibleReferenceTooltips.translation || 'WEB').toUpperCase();
}
function rememberTranslation(code){
code = String(code || '').toUpperCase();
if (!code) return;
setStoredPreference(code);
}
function scan(root){
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node){
if (!node.nodeValue || !refTestRe.test(node.nodeValue)) return NodeFilter.FILTER_REJECT;
refRe.lastIndex = 0;
let el = node.parentElement;
while (el) {
if (skip.has(el.tagName) || el.classList.contains('brt-ref') || el.classList.contains('brt-pop') || el.classList.contains('brt-logos-link')) return NodeFilter.FILTER_REJECT;
el = el.parentElement;
}
return NodeFilter.FILTER_ACCEPT;
}
});
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
nodes.forEach(wrapNode);
}
function wrapNode(node){
const text = node.nodeValue;
const frag = document.createDocumentFragment();
let last = 0;
refRe.lastIndex = 0;
text.replace(refRe, function(match){
const offset = arguments[arguments.length - 2];
if (offset > last) frag.appendChild(document.createTextNode(text.slice(last, offset)));
const ref = makeReferenceNode(match);
frag.appendChild(ref);
if (BibleReferenceTooltips.logosInsert) frag.appendChild(makeLogosLink(match));
last = offset + match.length;
});
if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
node.parentNode.replaceChild(frag, node);
}
function makeReferenceNode(ref){
const el = BibleReferenceTooltips.linkReferences ? document.createElement('a') : document.createElement('span');
el.className = 'brt-ref';
if (!BibleReferenceTooltips.underline) el.className += ' no-underline';
el.dataset.ref = ref;
el.textContent = ref;
if (BibleReferenceTooltips.linkReferences) {
el.href = bibleUrl(ref, BibleReferenceTooltips.bibleVersion || BibleReferenceTooltips.translation || 'WEB');
if (BibleReferenceTooltips.linkTarget === 'new') {
el.target = '_blank';
el.rel = 'noopener noreferrer';
}
} else {
el.tabIndex = 0;
}
return el;
}
function enhanceExistingLinks(root){
if (!BibleReferenceTooltips.addTooltipsToLinks && !BibleReferenceTooltips.logosExisting) return;
const links = root.querySelectorAll ? root.querySelectorAll('a[href]') : [];
links.forEach(link => {
if (link.classList.contains('brt-logos-link')) return;
const href = String(link.getAttribute('href') || '').toLowerCase();
const isBibleLink = href.includes('biblia.com') || href.includes('ref.ly') || href.includes('logos.com') || href.includes('logosres:');
if (!isBibleLink) return;
const ref = extractRef(link.textContent || link.getAttribute('title') || link.getAttribute('aria-label') || '');
if (ref && BibleReferenceTooltips.addTooltipsToLinks && !link.classList.contains('brt-ref')) {
link.classList.add('brt-ref');
if (!BibleReferenceTooltips.underline) link.classList.add('no-underline');
link.dataset.ref = ref;
}
if (BibleReferenceTooltips.logosExisting && href.includes('logos') && !link.nextElementSibling?.classList.contains('brt-logos-link')) {
link.insertAdjacentElement('afterend', makeLogosLink(ref || link.textContent || ''));
}
});
}
function extractRef(text){
refRe.lastIndex = 0;
const match = refRe.exec(String(text || ''));
refRe.lastIndex = 0;
return match ? match[0] : '';
}
function bibleUrl(ref, version){
const code = String(version || 'WEB').trim() || 'WEB';
return 'https://biblia.com/bible/' + encodeURIComponent(code) + '/' + encodeURIComponent(String(ref || '').replace(/\s+/g, ' ').trim());
}
function logosUrl(ref, version){
const compact = String(ref || '').replace(/\s+/g, '');
const code = String(version || 'WEB').trim() || 'WEB';
return 'https://ref.ly/logosref/Bible.' + encodeURIComponent(compact) + '?ver=' + encodeURIComponent(code);
}
function makeLogosLink(ref){
const link = document.createElement('a');
const icon = BibleReferenceTooltips.logosIcon === 'light' ? 'light' : 'dark';
link.className = 'brt-logos-link brt-logos-' + icon;
link.href = logosUrl(ref, BibleReferenceTooltips.logosTranslation || BibleReferenceTooltips.translation || 'WEB');
link.textContent = 'L';
link.setAttribute('aria-label', 'Open ' + String(ref || 'Bible reference') + ' in Logos Bible Software');
link.title = 'Open in Logos Bible Software';
if (BibleReferenceTooltips.linkTarget === 'new') {
link.target = '_blank';
link.rel = 'noopener noreferrer';
}
return link;
}
async function show(e){
if (!BibleReferenceTooltips.showTooltips) return;
const target = e.target.closest('.brt-ref');
if (!target) return;
window.clearTimeout(hideTimer);
window.clearTimeout(loadingTimer);
const token = ++requestToken;
activeRef = target.dataset.ref;
activeTrigger = target;
loadingTimer = window.setTimeout(() => {
if (token !== requestToken || activeTrigger !== target) return;
ensurePop();
pop.className = 'brt-pop ' + resolvedTheme();
pop.innerHTML = '