Add public WordPress release conventions

This commit is contained in:
2026-07-11 19:53:52 -05:00
parent c517c9ee1c
commit 5835e5a3b2
16 changed files with 1109 additions and 175 deletions
@@ -3,7 +3,7 @@
* Plugin Name: Bible Reference Tooltips
* Plugin URI: https://christit.com/bible-reference-tooltips
* Description: Local, open Bible reference tooltips for WordPress content. Detects verse references on page load and serves hover cards from a local verse table.
* Version: 1.1.0
* Version: 1.1.5
* Stage: Stable
* Release Channel: Public
* Public Release: Yes
@@ -26,10 +26,11 @@ if (!defined('ABSPATH')) exit;
if (!class_exists('Bible_Reference_Tooltips')) {
class Bible_Reference_Tooltips {
const VERSION = '1.1.0';
const VERSION = '1.1.5';
const SLUG = 'bible-reference-tooltips';
const NONCE = 'brt_nonce';
const OPTION_KEY = 'brt_settings';
const SETTINGS_VERSION = '1.1.5';
const TABLE_VERSION = '1';
private $book_map = null;
@@ -40,6 +41,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
add_action('admin_init', [$this, 'ensure_schema']);
add_action('admin_menu', [$this, 'admin_menu'], 110);
add_action('wp_enqueue_scripts', [$this, 'frontend_assets']);
add_action('admin_enqueue_scripts', [$this, 'admin_assets']);
add_action('rest_api_init', [$this, 'rest_routes']);
add_filter('the_content', [$this, 'mark_content_scope'], 1);
add_filter('comment_text', [$this, 'mark_comment_scope'], 1);
@@ -72,7 +74,16 @@ if (!class_exists('Bible_Reference_Tooltips')) {
}
public function ensure_defaults() {
if (!get_option(self::OPTION_KEY)) update_option(self::OPTION_KEY, self::defaults(), false);
$settings = get_option(self::OPTION_KEY);
if (!is_array($settings)) {
update_option(self::OPTION_KEY, self::defaults(), false);
update_option('brt_settings_version', self::SETTINGS_VERSION, false);
return;
}
if (get_option('brt_settings_version') !== self::SETTINGS_VERSION) {
update_option(self::OPTION_KEY, $this->normalize_settings($settings), false);
update_option('brt_settings_version', self::SETTINGS_VERSION, false);
}
}
public function ensure_schema() {
@@ -88,23 +99,17 @@ if (!class_exists('Bible_Reference_Tooltips')) {
public static function defaults() {
return [
'enabled' => '1',
'admin_enabled' => '0',
'translation' => 'WEB',
'scope' => 'body',
'scope' => '.brt-scan-scope, main, .entry-content, .wp-block-post-content',
'admin_scope' => '#wpbody-content',
'theme' => 'auto',
'underline' => '1',
'show_translation' => '1',
'show_tooltips' => '1',
'link_references' => '1',
'link_target' => 'same',
'bible_version' => '',
'logos_insert' => '0',
'logos_existing' => '0',
'logos_translation' => '',
'logos_icon' => 'dark',
'add_tooltips_to_links' => '0',
'case_sensitive' => '0',
'tag_chapters' => '0',
'scan_comments' => '0',
'case_insensitive' => '1',
'tag_chapters' => '1',
'scan_comments' => '1',
'skip_tags' => ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'SELECT', 'BUTTON', 'CODE', 'PRE', 'H1', 'H2', 'H3'],
'max_range' => '12',
];
@@ -112,9 +117,18 @@ if (!class_exists('Bible_Reference_Tooltips')) {
private function settings() {
$settings = get_option(self::OPTION_KEY, []);
return $this->normalize_settings(is_array($settings) ? $settings : []);
}
private function normalize_settings($settings) {
$settings = array_merge(self::defaults(), is_array($settings) ? $settings : []);
if (array_key_exists('case_sensitive', $settings) && !array_key_exists('case_insensitive', $settings)) {
$settings['case_insensitive'] = empty($settings['case_sensitive']) ? '1' : '0';
}
$settings['skip_tags'] = $this->normalize_skip_tags($settings['skip_tags'] ?? []);
return $settings;
$clean = [];
foreach (array_keys(self::defaults()) as $key) $clean[$key] = $settings[$key];
return $clean;
}
private static function install_schema() {
@@ -150,6 +164,16 @@ if (!class_exists('Bible_Reference_Tooltips')) {
public function frontend_assets() {
$settings = $this->settings();
if (empty($settings['enabled']) || is_admin() || is_feed() || wp_doing_ajax()) return;
$this->enqueue_tooltip_assets($settings, (string)$settings['scope']);
}
public function admin_assets($hook_suffix = '') {
$settings = $this->settings();
if (empty($settings['admin_enabled']) || wp_doing_ajax()) return;
$this->enqueue_tooltip_assets($settings, (string)$settings['admin_scope']);
}
private function enqueue_tooltip_assets($settings, $scope) {
wp_register_script('brt', '', [], self::VERSION, true);
wp_enqueue_script('brt');
wp_register_style('brt', false, [], self::VERSION);
@@ -160,22 +184,14 @@ if (!class_exists('Bible_Reference_Tooltips')) {
wp_localize_script('brt', 'BibleReferenceTooltips', [
'rest' => esc_url_raw(rest_url('bible-reference-tooltips/v1/lookup')),
'nonce' => wp_create_nonce('wp_rest'),
'scope' => (string)$settings['scope'],
'scope' => $scope,
'translation' => $default_translation,
'translations' => $installed_translations,
'theme' => (string)$settings['theme'],
'underline' => !empty($settings['underline']),
'showTranslation' => !empty($settings['show_translation']),
'showTooltips' => !empty($settings['show_tooltips']),
'linkReferences' => !empty($settings['link_references']),
'linkTarget' => (string)$settings['link_target'],
'bibleVersion' => (string)($settings['bible_version'] ?: $default_translation),
'logosInsert' => !empty($settings['logos_insert']),
'logosExisting' => !empty($settings['logos_existing']),
'logosTranslation' => (string)($settings['logos_translation'] ?: $default_translation),
'logosIcon' => (string)$settings['logos_icon'],
'addTooltipsToLinks' => !empty($settings['add_tooltips_to_links']),
'caseSensitive' => !empty($settings['case_sensitive']),
'caseSensitive' => empty($settings['case_insensitive']),
'tagChapters' => !empty($settings['tag_chapters']),
'scanComments' => !empty($settings['scan_comments']),
'skipTags' => array_values((array)$settings['skip_tags']),
@@ -409,26 +425,6 @@ if (!class_exists('Bible_Reference_Tooltips')) {
];
}
private function bible_link_versions() {
return [
'WEB' => 'WEB',
'KJV' => 'KJV',
'ASV' => 'ASV',
'YLT' => 'YLT',
'ESV' => 'ESV',
'NIV' => 'NIV',
'NKJV' => 'NKJV',
'NLT' => 'NLT',
'NASB' => 'NASB',
'CSB' => 'CSB',
'NET' => 'NET',
'RSV' => 'RSV',
'NRSV' => 'NRSV',
'LEB' => 'LEB',
'MSG' => 'The Message',
];
}
private function book_lookup_candidates($parsed) {
$abbr = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $parsed['book'] ?? ''));
$name = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $parsed['book_name'] ?? ''));
@@ -442,7 +438,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
'PHM' => ['PHM', 'PHLM', 'PHILEM'],
'REV' => ['REV', 'RE', 'APO'],
];
$candidates = array_merge([$abbr, $name], $variants[$abbr] ?? []);
$candidates = array_merge([$abbr, $name], $variants[$abbr] ?? [], $this->source_book_lookup_candidates($abbr));
foreach ($this->book_map() as $alias => $entry) {
if (strtoupper($entry['abbr']) !== $abbr) continue;
$candidates[] = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $alias));
@@ -469,7 +465,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
private function parse_reference($reference) {
$reference = trim(preg_replace('/\s+/', ' ', (string)$reference));
if (!preg_match('/^((?:[1-3]\s*)?[A-Za-z][A-Za-z .]+?)\s+(\d{1,3})(?:\s*:\s*(\d{1,3})(?:\s*[-\x{2013}\x{2014}]\s*(\d{1,3}))?)?$/u', $reference, $m)) return null;
if (!preg_match('/^((?:(?:[1-3]|I{1,3})\s*)?[A-Za-z][A-Za-z .\'-]*?)[\s.]*?(\d{1,3})(?:\s*[:.]\s*(\d{1,3})(?:\s*[-\x{2013}\x{2014}]\s*(\d{1,3}))?)?$/iu', $reference, $m)) return null;
$book = $this->normalize_book($m[1]);
if (!$book) return null;
return [
@@ -507,6 +503,52 @@ if (!class_exists('Bible_Reference_Tooltips')) {
return null;
}
private function normalize_source_book($book, $source = '') {
$key = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string)$book));
if ($source === 'ebible_vpl') {
$map = $this->ebible_vpl_book_map();
if (isset($map[$key])) return $map[$key];
return ['abbr' => $key, 'name' => $this->book_name_from_abbr($key)];
}
$human = $this->normalize_book($book);
if ($human) return $human;
return ['abbr' => $key, 'name' => $this->book_name_from_abbr($key)];
}
private function source_book_lookup_candidates($canonical_abbr) {
$canonical_abbr = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string)$canonical_abbr));
$candidates = [];
foreach ($this->ebible_vpl_book_map() as $source_abbr => $book) {
if (strtoupper($book['abbr']) === $canonical_abbr) $candidates[] = $source_abbr;
}
return $candidates;
}
private function ebible_vpl_book_map() {
$source_to_canonical = [
'GEN' => 'GEN', 'EXO' => 'EXO', 'LEV' => 'LEV', 'NUM' => 'NUM', 'DEU' => 'DEU',
'JOS' => 'JOS', 'JDG' => 'JDG', 'RUT' => 'RUT', '1SA' => '1SA', '2SA' => '2SA',
'1KI' => '1KI', '2KI' => '2KI', '1CH' => '1CH', '2CH' => '2CH', 'EZR' => 'EZR',
'NEH' => 'NEH', 'EST' => 'EST', 'JOB' => 'JOB', 'PSA' => 'PSA', 'PRO' => 'PRO',
'ECC' => 'ECC', 'SNG' => 'SNG', 'SOL' => 'SNG', 'ISA' => 'ISA', 'JER' => 'JER', 'LAM' => 'LAM',
'EZE' => 'EZK', 'EZK' => 'EZK', 'DAN' => 'DAN', 'HOS' => 'HOS', 'JOL' => 'JOL', 'JOE' => 'JOL',
'AMO' => 'AMO', 'OBA' => 'OBA', 'JON' => 'JON', 'MIC' => 'MIC', 'NAM' => 'NAM', 'NAH' => 'NAM',
'HAB' => 'HAB', 'ZEP' => 'ZEP', 'HAG' => 'HAG', 'ZEC' => 'ZEC', 'MAL' => 'MAL',
'MAT' => 'MAT', 'MAR' => 'MRK', 'MRK' => 'MRK', 'LUK' => 'LUK', 'JHN' => 'JHN', 'JOH' => 'JHN',
'ACT' => 'ACT', 'ROM' => 'ROM', '1CO' => '1CO', '2CO' => '2CO', 'GAL' => 'GAL',
'EPH' => 'EPH', 'PHP' => 'PHP', 'PHI' => 'PHP', 'COL' => 'COL', '1TH' => '1TH', '2TH' => '2TH',
'1TI' => '1TI', '2TI' => '2TI', 'TIT' => 'TIT', 'PHM' => 'PHM', 'HEB' => 'HEB',
'JAS' => 'JAS', 'JAM' => 'JAS', '1PE' => '1PE', '2PE' => '2PE', '1JO' => '1JN', '1JN' => '1JN',
'2JO' => '2JN', '2JN' => '2JN', '3JO' => '3JN', '3JN' => '3JN', 'JUD' => 'JUD',
'REV' => 'REV',
];
$map = [];
foreach ($source_to_canonical as $source_abbr => $canonical_abbr) {
$map[$source_abbr] = ['abbr' => $canonical_abbr, 'name' => $this->book_name_from_abbr($canonical_abbr)];
}
return $map;
}
private function book_map() {
if ($this->book_map !== null) return $this->book_map;
$books = [
@@ -535,7 +577,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
['ISA','Isaiah',['isaiah','isa']],
['JER','Jeremiah',['jeremiah','jer']],
['LAM','Lamentations',['lamentations','lam']],
['EZK','Ezekiel',['ezekiel','ezek','ezk']],
['EZK','Ezekiel',['ezekiel','ezek','ezk','eze']],
['DAN','Daniel',['daniel','dan','dn']],
['HOS','Hosea',['hosea','hos']],
['JOL','Joel',['joel','jol','joe']],
@@ -550,7 +592,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
['ZEC','Zechariah',['zechariah','zech','zec']],
['MAL','Malachi',['malachi','mal']],
['MAT','Matthew',['matthew','matt','mat','mt']],
['MRK','Mark',['mark','mrk','mk']],
['MRK','Mark',['mark','mrk','mk','mar']],
['LUK','Luke',['luke','luk','lk']],
['JHN','John',['john','jhn','jn','joh']],
['ACT','Acts',['acts','act']],
@@ -571,9 +613,9 @@ if (!class_exists('Bible_Reference_Tooltips')) {
['JAS','James',['james','jas','ja','jam','jms','jm','jac','jacob','epistleofjames','theepistleofjames']],
['1PE','1 Peter',['1peter','1pet','1pe','ipeter']],
['2PE','2 Peter',['2peter','2pet','2pe','iipeter']],
['1JN','1 John',['1john','1jn','1jhn','ijohn']],
['2JN','2 John',['2john','2jn','2jhn','iijohn']],
['3JN','3 John',['3john','3jn','3jhn','iiijohn']],
['1JN','1 John',['1john','1jn','1jhn','1jo','ijohn','ijn','ijhn','ijo']],
['2JN','2 John',['2john','2jn','2jhn','2jo','iijohn','iijn','iijhn','iijo']],
['3JN','3 John',['3john','3jn','3jhn','3jo','iiijohn','iiijn','iiijhn','iiijo']],
['JUD','Jude',['jude','jud']],
['REV','Revelation',['revelation','rev','re']],
];
@@ -649,7 +691,6 @@ if (!class_exists('Bible_Reference_Tooltips')) {
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field(self::NONCE); ?>
<input type="hidden" name="action" value="brt_save_settings">
<label class="brt-check"><input type="checkbox" name="enabled" value="1" <?php checked($settings['enabled'], '1'); ?>> Enable frontend scanning</label>
<label>Default translation
<select name="translation">
<?php
@@ -665,7 +706,10 @@ if (!class_exists('Bible_Reference_Tooltips')) {
</select>
<span>The tooltip opens with this translation first. Other installed translations appear as quick switches inside the card.</span>
</label>
<label>Scan selectors <textarea name="scope" rows="3"><?php echo esc_textarea($settings['scope']); ?></textarea><span>Comma-separated CSS selectors. Use body for the whole public page, or narrower selectors for faster scanning.</span></label>
<label class="brt-check"><input type="checkbox" name="enabled" value="1" <?php checked($settings['enabled'], '1'); ?>> Enable public-site scanning</label>
<label class="brt-check"><input type="checkbox" name="admin_enabled" value="1" <?php checked($settings['admin_enabled'], '1'); ?>> Enable WordPress admin scanning</label>
<label>Public scan selectors <textarea name="scope" rows="3"><?php echo esc_textarea($settings['scope']); ?></textarea><span>Comma-separated CSS selectors for public pages. Examples: body scans the full page; main scans the main layout; .entry-content scans classic post content; .wp-block-post-content scans block-theme content; .entry-content, .wp-block-post-content scans either area.</span></label>
<label>Admin scan selectors <textarea name="admin_scope" rows="2"><?php echo esc_textarea($settings['admin_scope']); ?></textarea><span>Comma-separated CSS selectors for wp-admin pages. #wpbody-content scans the main admin content area and avoids the WordPress toolbar and menu.</span></label>
<div class="brt-row">
<label>Card theme <select name="theme"><option value="auto" <?php selected($settings['theme'], 'auto'); ?>>Match site</option><option value="dark" <?php selected($settings['theme'], 'dark'); ?>>Dark</option><option value="light" <?php selected($settings['theme'], 'light'); ?>>Light</option></select></label>
<label>Range limit <input type="number" min="1" max="50" name="max_range" value="<?php echo esc_attr($settings['max_range']); ?>"><span>Maximum verses shown for references like John 3:16-20.</span></label>
@@ -673,31 +717,10 @@ if (!class_exists('Bible_Reference_Tooltips')) {
<label class="brt-check"><input type="checkbox" name="underline" value="1" <?php checked($settings['underline'], '1'); ?>> Underline detected references</label>
<label class="brt-check"><input type="checkbox" name="show_translation" value="1" <?php checked($settings['show_translation'], '1'); ?>> Show translation in card header</label>
<label class="brt-check"><input type="checkbox" name="show_tooltips" value="1" <?php checked($settings['show_tooltips'], '1'); ?>> Show hover/focus tooltip cards</label>
<label class="brt-check"><input type="checkbox" name="link_references" value="1" <?php checked($settings['link_references'], '1'); ?>> Link detected Bible references to Biblia.com</label>
<div class="brt-row">
<label>Links open in <select name="link_target"><option value="same" <?php selected($settings['link_target'], 'same'); ?>>Existing window</option><option value="new" <?php selected($settings['link_target'], 'new'); ?>>New window</option></select></label>
<label>Bible link version <select name="bible_version">
<?php foreach ($this->bible_link_versions() as $code => $label): ?>
<option value="<?php echo esc_attr($code); ?>" <?php selected($settings['bible_version'] ?: $settings['translation'], $code); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select></label>
</div>
<h3>Logos Links</h3>
<label class="brt-check"><input type="checkbox" name="logos_insert" value="1" <?php checked($settings['logos_insert'], '1'); ?>> Insert a small Logos Bible Software link icon beside detected references</label>
<label class="brt-check"><input type="checkbox" name="logos_existing" value="1" <?php checked($settings['logos_existing'], '1'); ?>> Add a Logos icon to existing Logos Bible Software links</label>
<div class="brt-row">
<label>Logos Bible version <select name="logos_translation">
<?php foreach ($this->bible_link_versions() as $code => $label): ?>
<option value="<?php echo esc_attr($code); ?>" <?php selected($settings['logos_translation'] ?: $settings['translation'], $code); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select></label>
<label>Logos icon <select name="logos_icon"><option value="dark" <?php selected($settings['logos_icon'], 'dark'); ?>>Dark icon for light backgrounds</option><option value="light" <?php selected($settings['logos_icon'], 'light'); ?>>Light icon for dark backgrounds</option></select></label>
</div>
<h3>Matching Rules</h3>
<label class="brt-check"><input type="checkbox" name="add_tooltips_to_links" value="1" <?php checked($settings['add_tooltips_to_links'], '1'); ?>> Add tooltips to existing Biblia.com and Ref.ly links</label>
<label class="brt-check"><input type="checkbox" name="case_sensitive" value="1" <?php checked($settings['case_sensitive'], '1'); ?>> Tag Bible references with improper casing, such as jn 3:16 or JOHN 3:16</label>
<label class="brt-check"><input type="checkbox" name="tag_chapters" value="1" <?php checked($settings['tag_chapters'], '1'); ?>> Tag chapter references, such as Genesis 1</label>
<label class="brt-check"><input type="checkbox" name="scan_comments" value="1" <?php checked($settings['scan_comments'], '1'); ?>> Search for Bible references in user comments</label>
<label class="brt-check"><input type="checkbox" name="case_insensitive" value="1" <?php checked($settings['case_insensitive'], '1'); ?>> Match references regardless of capitalization <span>Detects forms such as jn 3:16 and JOHN 3:16.</span></label>
<label class="brt-check"><input type="checkbox" name="tag_chapters" value="1" <?php checked($settings['tag_chapters'], '1'); ?>> Detect chapter-only references <span>Allows references like Genesis 1. The tooltip shows the first verses up to the range limit.</span></label>
<label class="brt-check"><input type="checkbox" name="scan_comments" value="1" <?php checked($settings['scan_comments'], '1'); ?>> Scan public comments <span>Adds tooltip detection to WordPress comment text.</span></label>
<label>Do not search these HTML tags
<span>Script, style, form fields, code, and preformatted blocks are always skipped.</span>
</label>
@@ -711,7 +734,8 @@ if (!class_exists('Bible_Reference_Tooltips')) {
</section>
<section class="brt-card brt-wide">
<h2>Open Text Policy</h2>
<p>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.</p>
<p>This plugin is designed for Bible texts that are public domain, openly licensed, or otherwise legally permitted for local storage and display. It does not provide import tools for restricted commercial translations unless a future integration explicitly supports proper licensing.</p>
<p>The built-in importer currently supports selected open/public-domain eBible VPL packages.</p>
</section>
</div>
<p class="brt-footer">Bible Reference Tooltips <?php echo esc_html(self::VERSION); ?>. Local open Bible text lookup for WordPress.</p>
@@ -765,32 +789,17 @@ if (!class_exists('Bible_Reference_Tooltips')) {
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',
'admin_enabled' => !empty($_POST['admin_enabled']) ? '1' : '0',
'translation' => $translation,
'scope' => sanitize_textarea_field($_POST['scope'] ?? self::defaults()['scope']),
'admin_scope' => sanitize_textarea_field($_POST['admin_scope'] ?? self::defaults()['admin_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',
'case_insensitive' => !empty($_POST['case_insensitive']) ? '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'] ?? []),
@@ -889,15 +898,22 @@ if (!class_exists('Bible_Reference_Tooltips')) {
$zip = new ZipArchive();
if ($zip->open($tmp) !== true) {
@unlink($tmp);
$this->delete_temp_dir($dir);
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.';
if (!file_exists($file)) {
$this->delete_temp_dir($dir);
return $translation . ' VPL file was not found in archive.';
}
$handle = fopen($file, 'r');
if (!$handle) return 'Could not read ' . $translation . ' VPL file.';
if (!$handle) {
$this->delete_temp_dir($dir);
return 'Could not read ' . $translation . ' VPL file.';
}
global $wpdb;
$wpdb->delete(self::table(), ['translation' => $translation], ['%s']);
$batch = [];
@@ -913,6 +929,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
}
fclose($handle);
if ($batch) $count += $this->insert_rows($batch, $info['source'], $info['license']);
$this->delete_temp_dir($dir);
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.';
@@ -977,7 +994,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
$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])];
$book = $this->normalize_source_book($m[1], 'ebible_vpl');
$text = trim(wp_specialchars_decode($m[4], ENT_QUOTES));
if ($text === '') return null;
return [
@@ -1102,7 +1119,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
}
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}}';
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-pop{box-sizing:border-box;position:fixed;z-index:999999;width:max-content;max-width:min(560px,calc(100vw - 28px));min-width:min(320px,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 *{box-sizing:border-box}.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:flex-start;justify-content:space-between;gap:10px;margin:0 0 12px}.brt-title{display:block;min-width:0;flex:1 1 auto;font-weight:800;font-size:15px;line-height:1.3;color:inherit;white-space:normal;overflow-wrap:anywhere}.brt-label{font-weight:700;color:#475569}.brt-pop.dark .brt-label{color:#a8b7cc}.brt-tabs{display:flex;flex:0 0 auto;max-width:210px;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-body{clear:both;display:block;min-height:1.55em}.brt-verse{display:block;margin:0 0 9px;color:inherit}.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{flex-direction:column}.brt-tabs{justify-content:flex-start;max-width:none}.brt-pop{min-width:min(280px,calc(100vw - 28px))}}';
}
private function frontend_js() {
@@ -1114,18 +1131,18 @@ if (!class_exists('Bible_Reference_Tooltips')) {
'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',
'Song\\s*of\\s*Solomon','Song\\s*of\\s*Songs','Song','Sos','Sng','Isaiah','Isa','Jeremiah','Jer','Lamentations','Lam','Ezekiel','Ezek','Ezk','Eze',
'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',
'Haggai','Hag','Zechariah','Zech','Zec','Malachi','Mal','Matthew','Matt','Mat','Mt','Mark','Mrk','Mk','Mar','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'
'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','1\\s*Jo',
'2\\s*John','2\\s*Jn','2\\s*Jhn','2\\s*Jo','3\\s*John','3\\s*Jn','3\\s*Jhn','3\\s*Jo','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 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');
@@ -1165,7 +1182,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
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;
if (skip.has(el.tagName) || el.classList.contains('brt-ref') || el.classList.contains('brt-pop')) return NodeFilter.FILTER_REJECT;
el = el.parentElement;
}
return NodeFilter.FILTER_ACCEPT;
@@ -1185,77 +1202,20 @@ if (!class_exists('Bible_Reference_Tooltips')) {
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');
const el = 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;
}
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');
@@ -1296,7 +1256,8 @@ if (!class_exists('Bible_Reference_Tooltips')) {
const code = translation || 'WEB';
const key = code + ':' + ref.toLowerCase();
if (cache.has(key)) return cache.get(key);
const url = BibleReferenceTooltips.rest + '?ref=' + encodeURIComponent(ref) + '&translation=' + encodeURIComponent(code);
const separator = String(BibleReferenceTooltips.rest).includes('?') ? '&' : '?';
const url = BibleReferenceTooltips.rest + separator + 'ref=' + encodeURIComponent(ref) + '&translation=' + encodeURIComponent(code);
const data = await fetch(url, {headers:{'X-WP-Nonce': BibleReferenceTooltips.nonce || ''}}).then(r => r.json());
cache.set(key, data);
return data;
@@ -1383,7 +1344,6 @@ if (!class_exists('Bible_Reference_Tooltips')) {
if (BibleReferenceTooltips.scanComments) selectors.push('.brt-comment-scope');
const roots = selectors.flatMap(s => Array.from(document.querySelectorAll(s)));
Array.from(new Set(roots.length ? roots : [document.body])).forEach(root => {
enhanceExistingLinks(root);
scan(root);
});
}