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
+30 -1
View File
@@ -1,12 +1,41 @@
# Bible Reference Tooltips Changelog
## 1.1.5 - 2026-07-12
- Adds the public workspace conventions and tracked Docker test lab as the bounded release workflow for this public package.
- Adds a settings migration that removes retired external-link options while preserving active tooltip settings.
- Updates the standalone changelog to match the public 1.1.x transition history.
## 1.1.4 - 2026-07-11
- Removes external Biblia, ref.ly, and Logos link generation from the public plugin.
- Adds optional WordPress admin scanning with separate admin scan selectors.
- Improves matching-rule defaults for capitalization, chapter-only references, and public comments.
- Generalizes the open text policy language.
## 1.1.3 - 2026-07-11
- Fixes browser REST lookups when WordPress provides a query-string REST route.
- Improves local test-page layout and removes native browser title tooltips from test references.
## 1.1.2 - 2026-07-11
- Splits eBible VPL source-code resolution from human Bible reference parsing.
- Adds exact source-code handling for compact eBible book identifiers such as `SOL`, `EZE`, `MAR`, `JOH`, `JAM`, `1JO`, `2JO`, and `3JO`.
- Improves compact reference detection for forms such as `Mark10:45` and `2Jn1:6`.
## 1.1.1 - 2026-07-11
- Adds exact eBible VPL aliases for source abbreviations that were not standard display abbreviations.
- Fixes imports for books such as Mark and Ezekiel when VPL source files use compact book codes.
## 1.1.0 - 2026-07-11
- Forked the former internal Bible tooltip package into a standalone public plugin named Bible Reference Tooltips.
- Renamed the plugin slug, text domain, REST namespace, script/style handles, admin action names, option key, and local verse table for the public package.
- Removed internal menu placement and internal logging dependencies from the public package.
- Added public release metadata, GPL license headers, and WordPress.org-oriented naming.
- Kept local Bible text imports, frontend reference detection, hover/focus cards, translation tabs, Biblia.com links, Logos links, skipped-tag controls, comment scanning, and James alias/backfill repairs.
- Kept local Bible text imports, frontend reference detection, hover/focus cards, translation tabs, skipped-tag controls, comment scanning, and James alias/backfill repairs.
## 1.0.14 - 2026-06-25
@@ -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);
});
}
+33 -4
View File
@@ -4,7 +4,7 @@ Tags: bible, scripture, references, tooltips, verses
Requires at least: 6.0
Tested up to: 7.0.1
Requires PHP: 7.4
Stable tag: 1.1.0
Stable tag: 1.1.5
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -12,17 +12,17 @@ Detect Bible references in WordPress content and show local hover cards from ope
== Description ==
Bible Reference Tooltips scans public WordPress content for Bible references such as John 3:16, Romans 8:28, or James 1:5. Detected references can be underlined, linked, and shown with hover or keyboard-focus tooltip cards.
Bible Reference Tooltips scans WordPress content for Bible references such as John 3:16, Romans 8:28, or James 1:5. Detected references can be underlined and shown with hover or keyboard-focus tooltip cards.
Verse text is looked up from local open/public-domain Bible texts installed into your WordPress database. The plugin currently supports World English Bible, King James Version, American Standard Version, and Young's Literal Translation imports from eBible VPL packages.
Features include:
* Frontend Bible reference detection.
* Public-site Bible reference detection.
* Optional WordPress admin Bible reference detection.
* Hover and focus tooltip cards.
* Local verse storage with no remote lookup required during page views.
* Multiple installed translations with quick-switch tabs in the card.
* Biblia.com links and optional Logos Bible Software links.
* Configurable scan selectors, skipped tags, comment scanning, tooltip theme, and verse range limit.
== Installation ==
@@ -49,6 +49,35 @@ Yes. The plugin is standalone and appears under Settings in the WordPress admin.
== Changelog ==
= 1.1.5 =
* Added the public workspace conventions and tracked Docker test lab as the bounded release workflow.
* Added a settings migration that removes retired external-link options while preserving current tooltip settings.
* Updated the standalone changelog to match the public 1.1.x transition history.
= 1.1.4 =
* Removed external Biblia, ref.ly, and Logos link generation from the public plugin.
* Added optional WordPress admin scanning with separate admin scan selectors.
* Improved matching-rule defaults for capitalization, chapter-only references, and public comments.
* Generalized the open text policy language.
= 1.1.3 =
* Fixed browser REST lookups when WordPress provides a query-string REST route.
* Improved local test-page layout and removed native browser title tooltips from test references.
= 1.1.2 =
* Split eBible VPL source-code resolution from human Bible reference parsing.
* Added exact source-code handling for compact eBible book identifiers such as SOL, EZE, MAR, JOH, JAM, 1JO, 2JO, and 3JO.
* Improved compact reference detection for forms such as Mark10:45 and 2Jn1:6.
= 1.1.1 =
* Added exact eBible VPL aliases for source abbreviations that were not standard display abbreviations.
* Fixed imports for books such as Mark and Ezekiel when VPL source files use compact book codes.
= 1.1.0 =
* Forked the internal package into a standalone public plugin.