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
+2
View File
@@ -7,3 +7,5 @@ vendor/
*.zip *.zip
*.log *.log
.env .env
tools/testlab/generated/
+71
View File
@@ -0,0 +1,71 @@
# WordPress Public Workspace Conventions
This workspace prepares Christ IT WordPress plugins and themes for public distribution. Git is the development source of truth. WordPress.org SVN, public plugin directories, and installed production copies are release targets, not the working source.
## Scope
- Public plugin source lives under `plugins/<slug>/`.
- Shared local release and smoke-test tooling lives under `tools/`.
- Do not remove or replace the matching internal GracePress package until the public package is accepted, published, installed on the target production site, and verified on the public update path.
## Version Discipline
Every meaningful public plugin change must update, in the same commit:
- the plugin header `Version`;
- any runtime version constant;
- `readme.txt` `Stable tag`;
- the plugin `CHANGELOG.md`;
- the `readme.txt` changelog when the change is public-facing.
Use one version number per bounded pass. Do not keep piling unrelated verification fixes into the same version after the pass has been declared complete.
## Changelog Discipline
The standalone plugin changelog is the durable engineering record. The WordPress.org readme changelog is the public release summary.
For migrated GracePress plugins:
- keep enough prior internal history to explain origin and compatibility;
- clearly mark the public fork version;
- remove or supersede public claims that no longer match the current package;
- record test-lab evidence in Git when a version is considered a release candidate.
## Bounded Verification
Use this default verification loop unless the user explicitly asks for more:
1. Run PHP syntax checks for touched PHP files.
2. Run the configured Docker smoke matrix once.
3. Save smoke results under `tools/testlab/results/`.
4. Review failures only far enough to identify the next code or test fix.
5. After a passing matrix, commit and stop.
Do not start recursive verification such as verifying the verifier, expanding the matrix, or repeatedly re-running browser checks unless a named release gate fails or the user explicitly asks for a broader pass.
## Test Lab
The local Docker test lab belongs in `tools/testlab/` so scripts, environment definitions, and result JSON stay tied to Git history.
Smoke tests should prove the contract, not exhaustively explore the UI:
- plugin activates;
- supported WordPress/PHP versions boot;
- expected plugin version is active;
- required tables are created;
- supported local Bible texts import;
- representative REST lookups pass;
- a browser-facing test page can be seeded for manual review.
Manual browser review is a separate, explicitly requested step.
## Release Boundary
A "build/test/commit and stop" request means:
- complete the named edits;
- run the bounded verification loop once;
- commit the coherent checkpoint if verification passes;
- report what passed and stop.
Do not publish to WordPress.org, production sites, or public update channels unless the user explicitly asks for that release step.
+30 -1
View File
@@ -1,12 +1,41 @@
# Bible Reference Tooltips Changelog # 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 ## 1.1.0 - 2026-07-11
- Forked the former internal Bible tooltip package into a standalone public plugin named Bible Reference Tooltips. - 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. - 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. - Removed internal menu placement and internal logging dependencies from the public package.
- Added public release metadata, GPL license headers, and WordPress.org-oriented naming. - 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 ## 1.0.14 - 2026-06-25
@@ -3,7 +3,7 @@
* Plugin Name: Bible Reference Tooltips * Plugin Name: Bible Reference Tooltips
* Plugin URI: https://christit.com/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. * 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 * Stage: Stable
* Release Channel: Public * Release Channel: Public
* Public Release: Yes * Public Release: Yes
@@ -26,10 +26,11 @@ if (!defined('ABSPATH')) exit;
if (!class_exists('Bible_Reference_Tooltips')) { if (!class_exists('Bible_Reference_Tooltips')) {
class Bible_Reference_Tooltips { class Bible_Reference_Tooltips {
const VERSION = '1.1.0'; const VERSION = '1.1.5';
const SLUG = 'bible-reference-tooltips'; const SLUG = 'bible-reference-tooltips';
const NONCE = 'brt_nonce'; const NONCE = 'brt_nonce';
const OPTION_KEY = 'brt_settings'; const OPTION_KEY = 'brt_settings';
const SETTINGS_VERSION = '1.1.5';
const TABLE_VERSION = '1'; const TABLE_VERSION = '1';
private $book_map = null; private $book_map = null;
@@ -40,6 +41,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
add_action('admin_init', [$this, 'ensure_schema']); add_action('admin_init', [$this, 'ensure_schema']);
add_action('admin_menu', [$this, 'admin_menu'], 110); add_action('admin_menu', [$this, 'admin_menu'], 110);
add_action('wp_enqueue_scripts', [$this, 'frontend_assets']); 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_action('rest_api_init', [$this, 'rest_routes']);
add_filter('the_content', [$this, 'mark_content_scope'], 1); add_filter('the_content', [$this, 'mark_content_scope'], 1);
add_filter('comment_text', [$this, 'mark_comment_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() { 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() { public function ensure_schema() {
@@ -88,23 +99,17 @@ if (!class_exists('Bible_Reference_Tooltips')) {
public static function defaults() { public static function defaults() {
return [ return [
'enabled' => '1', 'enabled' => '1',
'admin_enabled' => '0',
'translation' => 'WEB', 'translation' => 'WEB',
'scope' => 'body', 'scope' => '.brt-scan-scope, main, .entry-content, .wp-block-post-content',
'admin_scope' => '#wpbody-content',
'theme' => 'auto', 'theme' => 'auto',
'underline' => '1', 'underline' => '1',
'show_translation' => '1', 'show_translation' => '1',
'show_tooltips' => '1', 'show_tooltips' => '1',
'link_references' => '1', 'case_insensitive' => '1',
'link_target' => 'same', 'tag_chapters' => '1',
'bible_version' => '', 'scan_comments' => '1',
'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',
'skip_tags' => ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'SELECT', 'BUTTON', 'CODE', 'PRE', 'H1', 'H2', 'H3'], 'skip_tags' => ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'SELECT', 'BUTTON', 'CODE', 'PRE', 'H1', 'H2', 'H3'],
'max_range' => '12', 'max_range' => '12',
]; ];
@@ -112,9 +117,18 @@ if (!class_exists('Bible_Reference_Tooltips')) {
private function settings() { private function settings() {
$settings = get_option(self::OPTION_KEY, []); $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 : []); $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'] ?? []); $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() { private static function install_schema() {
@@ -150,6 +164,16 @@ if (!class_exists('Bible_Reference_Tooltips')) {
public function frontend_assets() { public function frontend_assets() {
$settings = $this->settings(); $settings = $this->settings();
if (empty($settings['enabled']) || is_admin() || is_feed() || wp_doing_ajax()) return; 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_register_script('brt', '', [], self::VERSION, true);
wp_enqueue_script('brt'); wp_enqueue_script('brt');
wp_register_style('brt', false, [], self::VERSION); wp_register_style('brt', false, [], self::VERSION);
@@ -160,22 +184,14 @@ if (!class_exists('Bible_Reference_Tooltips')) {
wp_localize_script('brt', 'BibleReferenceTooltips', [ wp_localize_script('brt', 'BibleReferenceTooltips', [
'rest' => esc_url_raw(rest_url('bible-reference-tooltips/v1/lookup')), 'rest' => esc_url_raw(rest_url('bible-reference-tooltips/v1/lookup')),
'nonce' => wp_create_nonce('wp_rest'), 'nonce' => wp_create_nonce('wp_rest'),
'scope' => (string)$settings['scope'], 'scope' => $scope,
'translation' => $default_translation, 'translation' => $default_translation,
'translations' => $installed_translations, 'translations' => $installed_translations,
'theme' => (string)$settings['theme'], 'theme' => (string)$settings['theme'],
'underline' => !empty($settings['underline']), 'underline' => !empty($settings['underline']),
'showTranslation' => !empty($settings['show_translation']), 'showTranslation' => !empty($settings['show_translation']),
'showTooltips' => !empty($settings['show_tooltips']), 'showTooltips' => !empty($settings['show_tooltips']),
'linkReferences' => !empty($settings['link_references']), 'caseSensitive' => empty($settings['case_insensitive']),
'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']),
'tagChapters' => !empty($settings['tag_chapters']), 'tagChapters' => !empty($settings['tag_chapters']),
'scanComments' => !empty($settings['scan_comments']), 'scanComments' => !empty($settings['scan_comments']),
'skipTags' => array_values((array)$settings['skip_tags']), '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) { private function book_lookup_candidates($parsed) {
$abbr = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $parsed['book'] ?? '')); $abbr = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $parsed['book'] ?? ''));
$name = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $parsed['book_name'] ?? '')); $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'], 'PHM' => ['PHM', 'PHLM', 'PHILEM'],
'REV' => ['REV', 'RE', 'APO'], '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) { foreach ($this->book_map() as $alias => $entry) {
if (strtoupper($entry['abbr']) !== $abbr) continue; if (strtoupper($entry['abbr']) !== $abbr) continue;
$candidates[] = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $alias)); $candidates[] = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $alias));
@@ -469,7 +465,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
private function parse_reference($reference) { private function parse_reference($reference) {
$reference = trim(preg_replace('/\s+/', ' ', (string)$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]); $book = $this->normalize_book($m[1]);
if (!$book) return null; if (!$book) return null;
return [ return [
@@ -507,6 +503,52 @@ if (!class_exists('Bible_Reference_Tooltips')) {
return null; 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() { private function book_map() {
if ($this->book_map !== null) return $this->book_map; if ($this->book_map !== null) return $this->book_map;
$books = [ $books = [
@@ -535,7 +577,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
['ISA','Isaiah',['isaiah','isa']], ['ISA','Isaiah',['isaiah','isa']],
['JER','Jeremiah',['jeremiah','jer']], ['JER','Jeremiah',['jeremiah','jer']],
['LAM','Lamentations',['lamentations','lam']], ['LAM','Lamentations',['lamentations','lam']],
['EZK','Ezekiel',['ezekiel','ezek','ezk']], ['EZK','Ezekiel',['ezekiel','ezek','ezk','eze']],
['DAN','Daniel',['daniel','dan','dn']], ['DAN','Daniel',['daniel','dan','dn']],
['HOS','Hosea',['hosea','hos']], ['HOS','Hosea',['hosea','hos']],
['JOL','Joel',['joel','jol','joe']], ['JOL','Joel',['joel','jol','joe']],
@@ -550,7 +592,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
['ZEC','Zechariah',['zechariah','zech','zec']], ['ZEC','Zechariah',['zechariah','zech','zec']],
['MAL','Malachi',['malachi','mal']], ['MAL','Malachi',['malachi','mal']],
['MAT','Matthew',['matthew','matt','mat','mt']], ['MAT','Matthew',['matthew','matt','mat','mt']],
['MRK','Mark',['mark','mrk','mk']], ['MRK','Mark',['mark','mrk','mk','mar']],
['LUK','Luke',['luke','luk','lk']], ['LUK','Luke',['luke','luk','lk']],
['JHN','John',['john','jhn','jn','joh']], ['JHN','John',['john','jhn','jn','joh']],
['ACT','Acts',['acts','act']], ['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']], ['JAS','James',['james','jas','ja','jam','jms','jm','jac','jacob','epistleofjames','theepistleofjames']],
['1PE','1 Peter',['1peter','1pet','1pe','ipeter']], ['1PE','1 Peter',['1peter','1pet','1pe','ipeter']],
['2PE','2 Peter',['2peter','2pet','2pe','iipeter']], ['2PE','2 Peter',['2peter','2pet','2pe','iipeter']],
['1JN','1 John',['1john','1jn','1jhn','ijohn']], ['1JN','1 John',['1john','1jn','1jhn','1jo','ijohn','ijn','ijhn','ijo']],
['2JN','2 John',['2john','2jn','2jhn','iijohn']], ['2JN','2 John',['2john','2jn','2jhn','2jo','iijohn','iijn','iijhn','iijo']],
['3JN','3 John',['3john','3jn','3jhn','iiijohn']], ['3JN','3 John',['3john','3jn','3jhn','3jo','iiijohn','iiijn','iiijhn','iiijo']],
['JUD','Jude',['jude','jud']], ['JUD','Jude',['jude','jud']],
['REV','Revelation',['revelation','rev','re']], ['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')); ?>"> <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field(self::NONCE); ?> <?php wp_nonce_field(self::NONCE); ?>
<input type="hidden" name="action" value="brt_save_settings"> <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 <label>Default translation
<select name="translation"> <select name="translation">
<?php <?php
@@ -665,7 +706,10 @@ if (!class_exists('Bible_Reference_Tooltips')) {
</select> </select>
<span>The tooltip opens with this translation first. Other installed translations appear as quick switches inside the card.</span> <span>The tooltip opens with this translation first. Other installed translations appear as quick switches inside the card.</span>
</label> </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"> <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>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> <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="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_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="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> <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_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="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'); ?>> 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="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'); ?>> Scan public comments <span>Adds tooltip detection to WordPress comment text.</span></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>Do not search these HTML tags <label>Do not search these HTML tags
<span>Script, style, form fields, code, and preformatted blocks are always skipped.</span> <span>Script, style, form fields, code, and preformatted blocks are always skipped.</span>
</label> </label>
@@ -711,7 +734,8 @@ if (!class_exists('Bible_Reference_Tooltips')) {
</section> </section>
<section class="brt-card brt-wide"> <section class="brt-card brt-wide">
<h2>Open Text Policy</h2> <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> </section>
</div> </div>
<p class="brt-footer">Bible Reference Tooltips <?php echo esc_html(self::VERSION); ?>. Local open Bible text lookup for WordPress.</p> <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'; if (!in_array($translation, array_unique(array_merge($known, $installed)), true)) $translation = 'WEB';
$theme = sanitize_key($_POST['theme'] ?? 'auto'); $theme = sanitize_key($_POST['theme'] ?? 'auto');
if (!in_array($theme, ['auto', 'dark', 'light'], true)) $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 = [ $settings = [
'enabled' => !empty($_POST['enabled']) ? '1' : '0', 'enabled' => !empty($_POST['enabled']) ? '1' : '0',
'admin_enabled' => !empty($_POST['admin_enabled']) ? '1' : '0',
'translation' => $translation, 'translation' => $translation,
'scope' => sanitize_textarea_field($_POST['scope'] ?? self::defaults()['scope']), 'scope' => sanitize_textarea_field($_POST['scope'] ?? self::defaults()['scope']),
'admin_scope' => sanitize_textarea_field($_POST['admin_scope'] ?? self::defaults()['admin_scope']),
'theme' => $theme, 'theme' => $theme,
'underline' => !empty($_POST['underline']) ? '1' : '0', 'underline' => !empty($_POST['underline']) ? '1' : '0',
'show_translation' => !empty($_POST['show_translation']) ? '1' : '0', 'show_translation' => !empty($_POST['show_translation']) ? '1' : '0',
'show_tooltips' => !empty($_POST['show_tooltips']) ? '1' : '0', 'show_tooltips' => !empty($_POST['show_tooltips']) ? '1' : '0',
'link_references' => !empty($_POST['link_references']) ? '1' : '0', 'case_insensitive' => !empty($_POST['case_insensitive']) ? '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', 'tag_chapters' => !empty($_POST['tag_chapters']) ? '1' : '0',
'scan_comments' => !empty($_POST['scan_comments']) ? '1' : '0', 'scan_comments' => !empty($_POST['scan_comments']) ? '1' : '0',
'skip_tags' => $this->normalize_skip_tags($_POST['skip_tags'] ?? []), 'skip_tags' => $this->normalize_skip_tags($_POST['skip_tags'] ?? []),
@@ -889,15 +898,22 @@ if (!class_exists('Bible_Reference_Tooltips')) {
$zip = new ZipArchive(); $zip = new ZipArchive();
if ($zip->open($tmp) !== true) { if ($zip->open($tmp) !== true) {
@unlink($tmp); @unlink($tmp);
$this->delete_temp_dir($dir);
return 'Could not open downloaded ' . $translation . ' archive.'; return 'Could not open downloaded ' . $translation . ' archive.';
} }
$zip->extractTo($dir); $zip->extractTo($dir);
$zip->close(); $zip->close();
@unlink($tmp); @unlink($tmp);
$file = trailingslashit($dir) . $info['file']; $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'); $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; global $wpdb;
$wpdb->delete(self::table(), ['translation' => $translation], ['%s']); $wpdb->delete(self::table(), ['translation' => $translation], ['%s']);
$batch = []; $batch = [];
@@ -913,6 +929,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
} }
fclose($handle); fclose($handle);
if ($batch) $count += $this->insert_rows($batch, $info['source'], $info['license']); if ($batch) $count += $this->insert_rows($batch, $info['source'], $info['license']);
$this->delete_temp_dir($dir);
if ($make_default) $this->set_active_translation($translation); if ($make_default) $this->set_active_translation($translation);
self::log_event('text_imported', ['translation' => $translation, 'verses' => $count], 'Imported Bible text.'); self::log_event('text_imported', ['translation' => $translation, 'verses' => $count], 'Imported Bible text.');
return $translation . ' import complete: ' . number_format_i18n($count) . ' verses installed.'; return $translation . ' import complete: ' . number_format_i18n($count) . ' verses installed.';
@@ -977,7 +994,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
$line = trim((string)$line); $line = trim((string)$line);
if ($line === '') return null; 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; 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)); $text = trim(wp_specialchars_decode($m[4], ENT_QUOTES));
if ($text === '') return null; if ($text === '') return null;
return [ return [
@@ -1102,7 +1119,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
} }
private function frontend_css() { 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() { 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', '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', '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', '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', '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', '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', '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', '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', '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','3\\s*John','3\\s*Jn','3\\s*Jhn','Jude','Jud','Revelation','Rev','Re' '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 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 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 refPattern = '\\b(' + book + ')[\\s.]*\\d{1,3}' + chapterPattern + '\\b';
const refFlags = BibleReferenceTooltips.caseSensitive ? 'g' : 'gi'; const refFlags = BibleReferenceTooltips.caseSensitive ? 'g' : 'gi';
const refRe = new RegExp(refPattern, refFlags); const refRe = new RegExp(refPattern, refFlags);
const refTestRe = new RegExp(refPattern, BibleReferenceTooltips.caseSensitive ? '' : 'i'); const refTestRe = new RegExp(refPattern, BibleReferenceTooltips.caseSensitive ? '' : 'i');
@@ -1165,7 +1182,7 @@ if (!class_exists('Bible_Reference_Tooltips')) {
refRe.lastIndex = 0; refRe.lastIndex = 0;
let el = node.parentElement; let el = node.parentElement;
while (el) { 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; el = el.parentElement;
} }
return NodeFilter.FILTER_ACCEPT; 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))); if (offset > last) frag.appendChild(document.createTextNode(text.slice(last, offset)));
const ref = makeReferenceNode(match); const ref = makeReferenceNode(match);
frag.appendChild(ref); frag.appendChild(ref);
if (BibleReferenceTooltips.logosInsert) frag.appendChild(makeLogosLink(match));
last = offset + match.length; last = offset + match.length;
}); });
if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last))); if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
node.parentNode.replaceChild(frag, node); node.parentNode.replaceChild(frag, node);
} }
function makeReferenceNode(ref){ function makeReferenceNode(ref){
const el = BibleReferenceTooltips.linkReferences ? document.createElement('a') : document.createElement('span'); const el = document.createElement('span');
el.className = 'brt-ref'; el.className = 'brt-ref';
if (!BibleReferenceTooltips.underline) el.className += ' no-underline'; if (!BibleReferenceTooltips.underline) el.className += ' no-underline';
el.dataset.ref = ref; el.dataset.ref = ref;
el.textContent = ref; el.textContent = ref;
if (BibleReferenceTooltips.linkReferences) { el.tabIndex = 0;
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; 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){ async function show(e){
if (!BibleReferenceTooltips.showTooltips) return; if (!BibleReferenceTooltips.showTooltips) return;
const target = e.target.closest('.brt-ref'); const target = e.target.closest('.brt-ref');
@@ -1296,7 +1256,8 @@ if (!class_exists('Bible_Reference_Tooltips')) {
const code = translation || 'WEB'; const code = translation || 'WEB';
const key = code + ':' + ref.toLowerCase(); const key = code + ':' + ref.toLowerCase();
if (cache.has(key)) return cache.get(key); 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()); const data = await fetch(url, {headers:{'X-WP-Nonce': BibleReferenceTooltips.nonce || ''}}).then(r => r.json());
cache.set(key, data); cache.set(key, data);
return data; return data;
@@ -1383,7 +1344,6 @@ if (!class_exists('Bible_Reference_Tooltips')) {
if (BibleReferenceTooltips.scanComments) selectors.push('.brt-comment-scope'); if (BibleReferenceTooltips.scanComments) selectors.push('.brt-comment-scope');
const roots = selectors.flatMap(s => Array.from(document.querySelectorAll(s))); const roots = selectors.flatMap(s => Array.from(document.querySelectorAll(s)));
Array.from(new Set(roots.length ? roots : [document.body])).forEach(root => { Array.from(new Set(roots.length ? roots : [document.body])).forEach(root => {
enhanceExistingLinks(root);
scan(root); scan(root);
}); });
} }
+33 -4
View File
@@ -4,7 +4,7 @@ Tags: bible, scripture, references, tooltips, verses
Requires at least: 6.0 Requires at least: 6.0
Tested up to: 7.0.1 Tested up to: 7.0.1
Requires PHP: 7.4 Requires PHP: 7.4
Stable tag: 1.1.0 Stable tag: 1.1.5
License: GPLv2 or later License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html 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 == == 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. 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: Features include:
* Frontend Bible reference detection. * Public-site Bible reference detection.
* Optional WordPress admin Bible reference detection.
* Hover and focus tooltip cards. * Hover and focus tooltip cards.
* Local verse storage with no remote lookup required during page views. * Local verse storage with no remote lookup required during page views.
* Multiple installed translations with quick-switch tabs in the card. * 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. * Configurable scan selectors, skipped tags, comment scanning, tooltip theme, and verse range limit.
== Installation == == Installation ==
@@ -49,6 +49,35 @@ Yes. The plugin is standalone and appears under Settings in the WordPress admin.
== Changelog == == 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 = = 1.1.0 =
* Forked the internal package into a standalone public plugin. * Forked the internal package into a standalone public plugin.
+61
View File
@@ -0,0 +1,61 @@
# WordPress Plugin Test Lab
Local Docker test lab for public WordPress plugins.
The lab mounts plugin source from this workspace into disposable WordPress containers, installs WordPress with WP-CLI, activates the selected plugin, and runs smoke tests.
## Environments
Initial matrix:
| Environment | WordPress Image | PHP | Database |
| --- | --- | --- | --- |
| `wp7-php84` | `wordpress:php8.4-apache` | 8.4 | MariaDB 11 |
| `wp6-php74` | `wordpress:php7.4-apache` with WP `6.8.2` download | 7.4 | MariaDB 10.6 |
The `wp7-php84` image installs the latest WordPress available in the image line. The `wp6-php74` environment uses the available PHP 7.4 WordPress image, then forces a WordPress 6.8.2 core download before install. The test script records the actual WordPress version after install.
## Commands
Run all configured environments:
```powershell
cd tools\testlab
.\scripts\run-all.ps1
```
Run one environment:
```powershell
cd tools\testlab
.\scripts\run-env.ps1 -Env wp7-php84
```
Stop and remove one environment:
```powershell
cd tools\testlab
.\scripts\down-env.ps1 -Env wp7-php84
```
Stop and remove all environments:
```powershell
cd tools\testlab
.\scripts\down-all.ps1
```
## What The Smoke Test Does
- Starts WordPress and MariaDB.
- Waits for the database and WordPress container.
- Installs WordPress.
- Activates `bible-reference-tooltips`.
- Confirms PHP, WordPress, and plugin versions.
- Confirms the plugin-created table exists.
- Imports WEB, KJV, ASV, and YLT through the plugin's internal importer.
- Confirms imported verse rows and canonical book coverage exist.
- Confirms representative REST lookups, including compact references such as `Mark10:45`, `Eze 36:26`, and `2Jn1:6`.
- Creates a post containing those references.
This stops before WordPress.org SVN or plugin submission work.
+15
View File
@@ -0,0 +1,15 @@
[
{
"name": "wp7-php84",
"wordpressImage": "wordpress:php8.4-apache",
"dbImage": "mariadb:11",
"hostPort": 8070
},
{
"name": "wp6-php74",
"wordpressImage": "wordpress:php7.4-apache",
"dbImage": "mariadb:10.6",
"hostPort": 8068,
"wordpressVersion": "6.8.2"
}
]
+149
View File
@@ -0,0 +1,149 @@
{
"environment": "wp6-php74",
"wordpressImage": "wordpress:php7.4-apache",
"dbImage": "mariadb:10.6",
"hostPort": 8068,
"startedAt": "2026-07-11T19:49:33.8231872-05:00",
"checks": [
{
"name": "wp-cli available",
"passed": true,
"detail": ""
},
{
"name": "wordpress core version prepared",
"passed": true,
"detail": "target=6.8.2"
},
{
"name": "wordpress installed",
"passed": true,
"detail": "installed"
},
{
"name": "runtime versions",
"passed": true,
"detail": "WP=6.8.2 PHP=7.4.33"
},
{
"name": "plugin activated",
"passed": true,
"detail": ""
},
{
"name": "plugin version detected",
"passed": true,
"detail": "version=1.1.5"
},
{
"name": "verse table created",
"passed": true,
"detail": "wp_brt_verses"
},
{
"name": "WEB import completed",
"passed": true,
"detail": "WEB import complete: 36,601 verses installed."
},
{
"name": "KJV import completed",
"passed": true,
"detail": "KJV import complete: 35,948 verses installed."
},
{
"name": "ASV import completed",
"passed": true,
"detail": "ASV import complete: 31,086 verses installed."
},
{
"name": "YLT import completed",
"passed": true,
"detail": "YLT import complete: 31,102 verses installed."
},
{
"name": "translation rows stored",
"passed": true,
"detail": "[{\"translation\":\"ASV\",\"row_count\":\"31086\"},{\"translation\":\"KJV\",\"row_count\":\"35948\"},{\"translation\":\"WEB\",\"row_count\":\"36601\"},{\"translation\":\"YLT\",\"row_count\":\"31102\"}]"
},
{
"name": "canonical book coverage",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup John 3:16",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup John 3.16",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Jn3:16",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Romans 8:28",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup James 1:5",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Mark 10:45",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Mark10:45",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Mk 10:45",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Ezekiel 36:26",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Eze 36:26",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup 2 John 1:6",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup 2Jn1:6",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup 2 Jo 1:6",
"passed": true,
"detail": "ok"
},
{
"name": "test post created",
"passed": true,
"detail": ""
}
],
"phpVersion": "7.4.33",
"wordpressVersion": "6.8.2",
"pluginVersion": "1.1.5",
"completedAt": "2026-07-11T19:52:42.5557675-05:00",
"passed": true
}
+144
View File
@@ -0,0 +1,144 @@
{
"environment": "wp7-php84",
"wordpressImage": "wordpress:php8.4-apache",
"dbImage": "mariadb:11",
"hostPort": 8070,
"startedAt": "2026-07-11T19:46:20.2262126-05:00",
"checks": [
{
"name": "wp-cli available",
"passed": true,
"detail": ""
},
{
"name": "wordpress installed",
"passed": true,
"detail": "installed"
},
{
"name": "runtime versions",
"passed": true,
"detail": "WP=7.0.1 PHP=8.4.23"
},
{
"name": "plugin activated",
"passed": true,
"detail": ""
},
{
"name": "plugin version detected",
"passed": true,
"detail": "version=1.1.5"
},
{
"name": "verse table created",
"passed": true,
"detail": "wp_brt_verses"
},
{
"name": "WEB import completed",
"passed": true,
"detail": "WEB import complete: 36,601 verses installed."
},
{
"name": "KJV import completed",
"passed": true,
"detail": "KJV import complete: 35,948 verses installed."
},
{
"name": "ASV import completed",
"passed": true,
"detail": "ASV import complete: 31,086 verses installed."
},
{
"name": "YLT import completed",
"passed": true,
"detail": "YLT import complete: 31,102 verses installed."
},
{
"name": "translation rows stored",
"passed": true,
"detail": "[{\"translation\":\"ASV\",\"row_count\":\"31086\"},{\"translation\":\"KJV\",\"row_count\":\"35948\"},{\"translation\":\"WEB\",\"row_count\":\"36601\"},{\"translation\":\"YLT\",\"row_count\":\"31102\"}]"
},
{
"name": "canonical book coverage",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup John 3:16",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup John 3.16",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Jn3:16",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Romans 8:28",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup James 1:5",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Mark 10:45",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Mark10:45",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Mk 10:45",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Ezekiel 36:26",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup Eze 36:26",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup 2 John 1:6",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup 2Jn1:6",
"passed": true,
"detail": "ok"
},
{
"name": "REST lookup 2 Jo 1:6",
"passed": true,
"detail": "ok"
},
{
"name": "test post created",
"passed": true,
"detail": ""
}
],
"phpVersion": "8.4.23",
"wordpressVersion": "7.0.1",
"pluginVersion": "1.1.5",
"completedAt": "2026-07-11T19:49:33.7883010-05:00",
"passed": true
}
+141
View File
@@ -0,0 +1,141 @@
$ErrorActionPreference = 'Stop'
$LabRoot = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
$WorkspaceRoot = Split-Path -Parent (Split-Path -Parent $LabRoot)
$ConfigPath = Join-Path $LabRoot 'config\environments.json'
$GeneratedRoot = Join-Path $LabRoot 'generated'
$ResultsRoot = Join-Path $LabRoot 'results'
$PluginSource = Join-Path $WorkspaceRoot 'plugins\bible-reference-tooltips'
$PluginSlug = 'bible-reference-tooltips'
$ExpectedPluginVersion = '1.1.5'
function Get-TestLabEnvironments {
Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json
}
function Get-TestLabEnvironment {
param([Parameter(Mandatory)][string]$Name)
$envs = Get-TestLabEnvironments
$selected = $envs | Where-Object { $_.name -eq $Name } | Select-Object -First 1
if (-not $selected) {
throw "Unknown environment '$Name'. Available: $($envs.name -join ', ')"
}
return $selected
}
function Convert-ToComposePath {
param([Parameter(Mandatory)][string]$Path)
return ($Path -replace '\\', '/')
}
function Ensure-TestLabDirectories {
New-Item -ItemType Directory -Force -Path $GeneratedRoot, $ResultsRoot | Out-Null
}
function Get-EnvDir {
param([Parameter(Mandatory)][string]$Name)
return Join-Path $GeneratedRoot $Name
}
function Get-ResultFile {
param([Parameter(Mandatory)][string]$Name)
return Join-Path $ResultsRoot "$Name.json"
}
function Write-ComposeFile {
param([Parameter(Mandatory)]$Environment)
Ensure-TestLabDirectories
$envDir = Get-EnvDir $Environment.name
New-Item -ItemType Directory -Force -Path $envDir | Out-Null
$pluginPath = Convert-ToComposePath $PluginSource
$compose = @"
services:
db:
image: $($Environment.dbImage)
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
MYSQL_ROOT_PASSWORD: rootpass
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-uroot", "-prootpass"]
interval: 5s
timeout: 5s
retries: 30
volumes:
- db_data:/var/lib/mysql
wordpress:
image: $($Environment.wordpressImage)
depends_on:
db:
condition: service_healthy
ports:
- "$($Environment.hostPort):80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DEBUG: "1"
volumes:
- wp_data:/var/www/html
- ${pluginPath}:/var/www/html/wp-content/plugins/$PluginSlug
volumes:
db_data:
wp_data:
"@
Set-Content -LiteralPath (Join-Path $envDir 'compose.yml') -Value $compose -Encoding UTF8
}
function Invoke-Compose {
param(
[Parameter(Mandatory)]$Environment,
[Parameter(Mandatory)][string[]]$Arguments
)
$envDir = Get-EnvDir $Environment.name
docker compose -p "wptest-$($Environment.name)" -f (Join-Path $envDir 'compose.yml') @Arguments
if ($LASTEXITCODE -ne 0) {
throw "docker compose failed with exit code ${LASTEXITCODE}: $($Arguments -join ' ')"
}
}
function Invoke-WpCli {
param(
[Parameter(Mandatory)]$Environment,
[Parameter(Mandatory)][string]$Command
)
Invoke-Compose $Environment @('exec', '-T', '-u', '33', 'wordpress', 'bash', '-lc', $Command)
}
function Invoke-WpCliRoot {
param(
[Parameter(Mandatory)]$Environment,
[Parameter(Mandatory)][string]$Command
)
Invoke-Compose $Environment @('exec', '-T', 'wordpress', 'bash', '-lc', $Command)
}
function Write-ContainerPhpFile {
param(
[Parameter(Mandatory)]$Environment,
[Parameter(Mandatory)][string]$ContainerPath,
[Parameter(Mandatory)][string]$Content
)
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Content))
Invoke-WpCliRoot $Environment "rm -f '$ContainerPath' && printf '%s' '$encoded' | base64 -d > '$ContainerPath' && chown www-data:www-data '$ContainerPath'" | Out-Null
}
function Write-ContainerTextFile {
param(
[Parameter(Mandatory)]$Environment,
[Parameter(Mandatory)][string]$ContainerPath,
[Parameter(Mandatory)][string]$Content,
[string]$Mode = '0644'
)
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Content))
Invoke-WpCliRoot $Environment "rm -f '$ContainerPath' && printf '%s' '$encoded' | base64 -d > '$ContainerPath' && chmod '$Mode' '$ContainerPath' && chown www-data:www-data '$ContainerPath'" | Out-Null
}
+6
View File
@@ -0,0 +1,6 @@
. "$PSScriptRoot\common.ps1"
foreach ($environment in Get-TestLabEnvironments) {
Write-Host "=== Removing $($environment.name) ==="
& "$PSScriptRoot\down-env.ps1" -Env $environment.name
}
+9
View File
@@ -0,0 +1,9 @@
param(
[Parameter(Mandatory)][string]$Env
)
. "$PSScriptRoot\common.ps1"
$environment = Get-TestLabEnvironment -Name $Env
Write-ComposeFile -Environment $environment
Invoke-Compose $environment @('down', '-v', '--remove-orphans') | Out-Host
+9
View File
@@ -0,0 +1,9 @@
. "$PSScriptRoot\common.ps1"
$environments = Get-TestLabEnvironments
foreach ($environment in $environments) {
$name = [string]$environment.name
Write-Host ""
Write-Host "=== Running $name ==="
& "$PSScriptRoot\run-env.ps1" -Env $name
}
+158
View File
@@ -0,0 +1,158 @@
param(
[Parameter(Mandatory)][string]$Env
)
. "$PSScriptRoot\common.ps1"
$environment = Get-TestLabEnvironment -Name $Env
Write-ComposeFile -Environment $environment
$result = [ordered]@{
environment = $environment.name
wordpressImage = $environment.wordpressImage
dbImage = $environment.dbImage
hostPort = $environment.hostPort
startedAt = (Get-Date).ToString('o')
checks = @()
}
function Add-Check {
param([string]$Name, [bool]$Passed, [string]$Detail = '')
$script:result.checks += [ordered]@{ name = $Name; passed = $Passed; detail = $Detail }
if ($Passed) {
Write-Host "[PASS] $Name $Detail"
} else {
Write-Host "[FAIL] $Name $Detail"
throw "Check failed: $Name $Detail"
}
}
try {
Invoke-Compose $environment @('up', '-d') | Out-Host
$wpCli = 'if ! command -v wp >/dev/null 2>&1; then curl -fsSL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o /usr/local/bin/wp && chmod +x /usr/local/bin/wp; fi'
Invoke-WpCliRoot $environment $wpCli | Out-Host
Add-Check 'wp-cli available' $true
if ($environment.PSObject.Properties.Name -contains 'wordpressVersion' -and $environment.wordpressVersion) {
Invoke-WpCli $environment "wp core download --version=$($environment.wordpressVersion) --force --path=/var/www/html" | Out-Host
Add-Check 'wordpress core version prepared' $true "target=$($environment.wordpressVersion)"
}
$installScript = @'
#!/usr/bin/env bash
set -e
for i in $(seq 1 40); do
if wp core is-installed --path=/var/www/html >/dev/null 2>&1; then
exit 0
fi
if wp core install --path=/var/www/html --url=http://localhost:__HOST_PORT__ --title=PluginTest --admin_user=admin --admin_password=adminpass --admin_email=admin@example.test --skip-email; then
exit 0
fi
sleep 3
done
exit 1
'@ -replace '__HOST_PORT__', [string]$environment.hostPort
Write-ContainerTextFile $environment '/tmp/brt-install.sh' $installScript '0755'
Invoke-WpCli $environment 'bash /tmp/brt-install.sh' | Out-Host
$installed = (Invoke-WpCli $environment 'wp core is-installed --path=/var/www/html && echo installed' | Out-String).Trim()
Add-Check 'wordpress installed' ($installed -eq 'installed') $installed
$phpVersion = (Invoke-WpCliRoot $environment "php -r 'echo PHP_VERSION;'" | Out-String).Trim()
$wpVersion = (Invoke-WpCli $environment 'wp core version --path=/var/www/html' | Out-String).Trim()
Add-Check 'runtime versions' $true "WP=$wpVersion PHP=$phpVersion"
$result.phpVersion = $phpVersion
$result.wordpressVersion = $wpVersion
Invoke-WpCli $environment "wp plugin activate $PluginSlug --path=/var/www/html" | Out-Host
Add-Check 'plugin activated' $true
$pluginVersion = (Invoke-WpCli $environment "wp plugin get $PluginSlug --field=version --path=/var/www/html" | Out-String).Trim()
Add-Check 'plugin version detected' ($pluginVersion -eq $ExpectedPluginVersion) "version=$pluginVersion"
$result.pluginVersion = $pluginVersion
$tablePhp = @'
<?php
global $wpdb;
echo $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->prefix . 'brt_verses'));
'@
Write-ContainerPhpFile $environment '/tmp/brt-table.php' $tablePhp
$tableExists = (Invoke-WpCli $environment 'wp eval-file /tmp/brt-table.php --path=/var/www/html' | Out-String).Trim()
Add-Check 'verse table created' ($tableExists -eq 'wp_brt_verses') $tableExists
$translationsToImport = @('WEB', 'KJV', 'ASV', 'YLT')
foreach ($translationCode in $translationsToImport) {
$importPhp = @"
<?php
`$plugin = new Bible_Reference_Tooltips();
`$ref = new ReflectionClass(`$plugin);
`$m = `$ref->getMethod('import_vpl_translation');
`$m->setAccessible(true);
`$count = `$m->invoke(`$plugin, '$translationCode', false);
echo `$count;
"@
Write-ContainerPhpFile $environment '/tmp/brt-import.php' $importPhp
$imported = (Invoke-WpCli $environment 'wp eval-file /tmp/brt-import.php --path=/var/www/html' | Out-String).Trim()
$importNumberText = [regex]::Match($imported, '[\d,]+').Value -replace ',', ''
$importNumber = if ($importNumberText) { [int]$importNumberText } else { 0 }
Add-Check "$translationCode import completed" ($importNumber -gt 30000) $imported
}
Invoke-WpCli $environment "wp option patch update brt_settings translation WEB --path=/var/www/html" | Out-Host
$countPhp = @'
<?php
global $wpdb;
$rows = $wpdb->get_results('SELECT translation, COUNT(*) row_count FROM ' . $wpdb->prefix . 'brt_verses GROUP BY translation ORDER BY translation', ARRAY_A);
echo json_encode($rows);
'@
Write-ContainerPhpFile $environment '/tmp/brt-count.php' $countPhp
$rowCount = (Invoke-WpCli $environment 'wp eval-file /tmp/brt-count.php --path=/var/www/html' | Out-String).Trim()
Add-Check 'translation rows stored' ($rowCount -match 'WEB' -and $rowCount -match 'KJV' -and $rowCount -match 'ASV' -and $rowCount -match 'YLT') $rowCount
$bookCoveragePhp = @'
<?php
global $wpdb;
$expected = ['GEN','EXO','LEV','NUM','DEU','JOS','JDG','RUT','1SA','2SA','1KI','2KI','1CH','2CH','EZR','NEH','EST','JOB','PSA','PRO','ECC','SNG','ISA','JER','LAM','EZK','DAN','HOS','JOL','AMO','OBA','JON','MIC','NAM','HAB','ZEP','HAG','ZEC','MAL','MAT','MRK','LUK','JHN','ACT','ROM','1CO','2CO','GAL','EPH','PHP','COL','1TH','2TH','1TI','2TI','TIT','PHM','HEB','JAS','1PE','2PE','1JN','2JN','3JN','JUD','REV'];
$translations = ['WEB','KJV','ASV','YLT'];
$missing = [];
foreach ($translations as $translation) {
$found = $wpdb->get_col($wpdb->prepare('SELECT DISTINCT book FROM ' . $wpdb->prefix . 'brt_verses WHERE translation = %s', $translation));
$diff = array_values(array_diff($expected, $found));
if ($diff) $missing[$translation] = $diff;
}
echo $missing ? json_encode($missing) : 'ok';
'@
Write-ContainerPhpFile $environment '/tmp/brt-book-coverage.php' $bookCoveragePhp
$bookCoverage = (Invoke-WpCli $environment 'wp eval-file /tmp/brt-book-coverage.php --path=/var/www/html' | Out-String).Trim()
Add-Check 'canonical book coverage' ($bookCoverage -eq 'ok') $bookCoverage
$refs = @('John 3:16', 'John 3.16', 'Jn3:16', 'Romans 8:28', 'James 1:5', 'Mark 10:45', 'Mark10:45', 'Mk 10:45', 'Ezekiel 36:26', 'Eze 36:26', '2 John 1:6', '2Jn1:6', '2 Jo 1:6')
foreach ($ref in $refs) {
$lookupPhp = @"
<?php
`$request = new WP_REST_Request('GET', '/bible-reference-tooltips/v1/lookup');
`$request->set_param('ref', '$ref');
`$request->set_param('translation', 'WEB');
`$response = rest_do_request(`$request);
`$data = `$response->get_data();
echo (!empty(`$data['ok']) && !empty(`$data['verses'])) ? 'ok' : json_encode(`$data);
"@
Write-ContainerPhpFile $environment '/tmp/brt-lookup.php' $lookupPhp
$lookupResult = (Invoke-WpCli $environment 'wp eval-file /tmp/brt-lookup.php --path=/var/www/html' | Out-String).Trim()
Add-Check "REST lookup $ref" ($lookupResult -eq 'ok') $lookupResult
}
Invoke-WpCli $environment "wp post create --post_type=post --post_status=publish --post_title='Bible Reference Tooltip Smoke Test' --post_content='John 3:16 Romans 8:28 James 1:5' --path=/var/www/html" | Out-Host
Add-Check 'test post created' $true
$result.completedAt = (Get-Date).ToString('o')
$result.passed = $true
} catch {
$result.completedAt = (Get-Date).ToString('o')
$result.passed = $false
$result.error = $_.Exception.Message
throw
} finally {
Ensure-TestLabDirectories
$result | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Get-ResultFile -Name $environment.name) -Encoding UTF8
}
@@ -0,0 +1,9 @@
. "$PSScriptRoot\common.ps1"
$envs = Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json
foreach ($environment in $envs) {
$name = [string]$environment.name
Write-Host ""
Write-Host "=== Seeding $name ==="
& "$PSScriptRoot\seed-bible-reference-page.ps1" -Env $name
}
@@ -0,0 +1,142 @@
param(
[Parameter(Mandatory)][string]$Env
)
. "$PSScriptRoot\common.ps1"
$environment = Get-TestLabEnvironment -Name $Env
$seedPhp = @'
<?php
$books = [
['Genesis', 'Gen', 'Genesis 1:1', 'Gen 1:1'],
['Exodus', 'Exod', 'Exodus 3:14', 'Exod 3:14'],
['Leviticus', 'Lev', 'Leviticus 19:18', 'Lev 19:18'],
['Numbers', 'Num', 'Numbers 6:24', 'Num 6:24'],
['Deuteronomy', 'Deut', 'Deuteronomy 6:5', 'Deut 6:5'],
['Joshua', 'Josh', 'Joshua 1:9', 'Josh 1:9'],
['Judges', 'Judg', 'Judges 6:12', 'Judg 6:12'],
['Ruth', 'Ruth', 'Ruth 1:16', 'Ruth 1:16'],
['1 Samuel', '1 Sam', '1 Samuel 16:7', '1 Sam 16:7'],
['2 Samuel', '2 Sam', '2 Samuel 7:22', '2 Sam 7:22'],
['1 Kings', '1 Kgs', '1 Kings 8:27', '1 Kgs 8:27'],
['2 Kings', '2 Kgs', '2 Kings 6:17', '2 Kgs 6:17'],
['1 Chronicles', '1 Chr', '1 Chronicles 16:34', '1 Chr 16:34'],
['2 Chronicles', '2 Chr', '2 Chronicles 7:14', '2 Chr 7:14'],
['Ezra', 'Ezra', 'Ezra 7:10', 'Ezra 7:10'],
['Nehemiah', 'Neh', 'Nehemiah 8:10', 'Neh 8:10'],
['Esther', 'Esth', 'Esther 4:14', 'Esth 4:14'],
['Job', 'Job', 'Job 19:25', 'Job 19:25'],
['Psalms', 'Ps', 'Psalms 23:1', 'Ps 23:1'],
['Proverbs', 'Prov', 'Proverbs 3:5', 'Prov 3:5'],
['Ecclesiastes', 'Eccl', 'Ecclesiastes 3:1', 'Eccl 3:1'],
['Song of Solomon', 'Song', 'Song of Solomon 2:4', 'Song 2:4'],
['Isaiah', 'Isa', 'Isaiah 40:31', 'Isa 40:31'],
['Jeremiah', 'Jer', 'Jeremiah 29:11', 'Jer 29:11'],
['Lamentations', 'Lam', 'Lamentations 3:22', 'Lam 3:22'],
['Ezekiel', 'Ezek', 'Ezekiel 36:26', 'Ezek 36:26'],
['Daniel', 'Dan', 'Daniel 3:17', 'Dan 3:17'],
['Hosea', 'Hos', 'Hosea 6:3', 'Hos 6:3'],
['Joel', 'Joel', 'Joel 2:28', 'Joel 2:28'],
['Amos', 'Amos', 'Amos 5:24', 'Amos 5:24'],
['Obadiah', 'Obad', 'Obadiah 1:15', 'Obad 1:15'],
['Jonah', 'Jonah', 'Jonah 2:2', 'Jonah 2:2'],
['Micah', 'Mic', 'Micah 6:8', 'Mic 6:8'],
['Nahum', 'Nah', 'Nahum 1:7', 'Nah 1:7'],
['Habakkuk', 'Hab', 'Habakkuk 2:4', 'Hab 2:4'],
['Zephaniah', 'Zeph', 'Zephaniah 3:17', 'Zeph 3:17'],
['Haggai', 'Hag', 'Haggai 2:9', 'Hag 2:9'],
['Zechariah', 'Zech', 'Zechariah 4:6', 'Zech 4:6'],
['Malachi', 'Mal', 'Malachi 3:10', 'Mal 3:10'],
['Matthew', 'Matt', 'Matthew 5:16', 'Matt 5:16'],
['Mark', 'Mk', 'Mark 10:45', 'Mk 10:45'],
['Luke', 'Lk', 'Luke 2:10', 'Lk 2:10'],
['John', 'Jn', 'John 3:16', 'Jn 3:16'],
['Acts', 'Acts', 'Acts 1:8', 'Acts 1:8'],
['Romans', 'Rom', 'Romans 8:28', 'Rom 8:28'],
['1 Corinthians', '1 Cor', '1 Corinthians 13:4', '1 Cor 13:4'],
['2 Corinthians', '2 Cor', '2 Corinthians 5:17', '2 Cor 5:17'],
['Galatians', 'Gal', 'Galatians 5:22', 'Gal 5:22'],
['Ephesians', 'Eph', 'Ephesians 2:8', 'Eph 2:8'],
['Philippians', 'Phil', 'Philippians 4:13', 'Phil 4:13'],
['Colossians', 'Col', 'Colossians 3:23', 'Col 3:23'],
['1 Thessalonians', '1 Thess', '1 Thessalonians 5:16', '1 Thess 5:16'],
['2 Thessalonians', '2 Thess', '2 Thessalonians 3:3', '2 Thess 3:3'],
['1 Timothy', '1 Tim', '1 Timothy 4:12', '1 Tim 4:12'],
['2 Timothy', '2 Tim', '2 Timothy 1:7', '2 Tim 1:7'],
['Titus', 'Titus', 'Titus 2:11', 'Titus 2:11'],
['Philemon', 'Phlm', 'Philemon 1:6', 'Phlm 1:6'],
['Hebrews', 'Heb', 'Hebrews 11:1', 'Heb 11:1'],
['James', 'Jas', 'James 1:5', 'Jas 1:5'],
['1 Peter', '1 Pet', '1 Peter 5:7', '1 Pet 5:7'],
['2 Peter', '2 Pet', '2 Peter 3:9', '2 Pet 3:9'],
['1 John', '1 Jn', '1 John 1:9', '1 Jn 1:9'],
['2 John', '2 Jn', '2 John 1:6', '2 Jn 1:6'],
['3 John', '3 Jn', '3 John 1:4', '3 Jn 1:4'],
['Jude', 'Jude', 'Jude 1:24', 'Jude 1:24'],
['Revelation', 'Rev', 'Revelation 21:4', 'Rev 21:4'],
];
$translations = [
['WEB', 'World English Bible'],
['KJV', 'King James Version'],
['ASV', 'American Standard Version'],
['YLT', 'Young\'s Literal Translation'],
];
$content = '<div class="brt-test-page">';
$content .= '<style>.brt-test-page{box-sizing:border-box;width:min(1520px,calc(100vw - 120px));max-width:none;margin-left:50%;transform:translateX(-50%);font-family:system-ui,-apple-system,Segoe UI,sans-serif}.brt-test-page *{box-sizing:border-box}.brt-test-page p{max-width:760px;margin:0 0 18px;color:#4b5563}.brt-test-grid{display:grid;grid-template-columns:minmax(120px,.65fr) repeat(4,minmax(130px,1fr));border:1px solid #d7dee8;border-bottom:0;border-right:0;background:#fff}.brt-test-row{display:contents}.brt-test-head,.brt-test-book,.brt-test-cell{border-right:1px solid #d7dee8;border-bottom:1px solid #d7dee8;padding:10px 12px}.brt-test-head{position:sticky;top:0;z-index:1;background:#f3f6fa;font-weight:800}.brt-test-book{font-weight:800;background:#fff}.brt-test-cell{min-width:0}.brt-test-cell .ref-pair{display:flex;flex-direction:column;align-items:flex-start;gap:6px}.brt-test-cell a{display:inline-block;max-width:100%;font-weight:700;line-height:1.25;white-space:normal;overflow-wrap:anywhere}@media(max-width:980px){.brt-test-page{width:min(100%,calc(100vw - 36px))}.brt-test-grid{display:block;border:0}.brt-test-head{display:none}.brt-test-row{display:grid;grid-template-columns:1fr;border:1px solid #d7dee8;margin:0 0 12px}.brt-test-book{background:#f3f6fa}.brt-test-cell{display:grid;grid-template-columns:54px 1fr;gap:10px}.brt-test-cell:before{content:attr(data-version);font-weight:800;color:#4b5563}}}</style>';
$content .= '<p>This page intentionally includes one full-name and one abbreviated Bible reference for every book of the Bible across all installed local translations.</p>';
$content .= '<div class="brt-test-grid">';
$content .= '<div class="brt-test-head">Book</div>';
foreach ($translations as $translation) {
$content .= '<div class="brt-test-head">' . esc_html($translation[0]) . '</div>';
}
foreach ($books as $book) {
[$name, $abbr, $fullRef, $abbrRef] = $book;
$content .= '<div class="brt-test-row">';
$content .= '<div class="brt-test-book">' . esc_html($name) . '</div>';
foreach ($translations as $translation) {
[$code, $label] = $translation;
$content .= '<div class="brt-test-cell" data-version="' . esc_attr($code) . '"><div class="ref-pair">';
$content .= '<span>' . esc_html($fullRef) . '</span>';
$content .= '<span>' . esc_html($abbrRef) . '</span>';
$content .= '</div></div>';
}
$content .= '</div>';
}
$content .= '</div></div>';
$existing = get_page_by_path('bible-reference-tooltip-test');
$post_data = [
'post_title' => 'Bible Reference Tooltip Test',
'post_name' => 'bible-reference-tooltip-test',
'post_type' => 'page',
'post_status' => 'publish',
'post_content' => $content,
];
if ($existing) {
$post_data['ID'] = $existing->ID;
$page_id = wp_update_post($post_data, true);
} else {
$page_id = wp_insert_post($post_data, true);
}
if (is_wp_error($page_id)) {
fwrite(STDERR, $page_id->get_error_message());
exit(1);
}
update_option('show_on_front', 'page');
update_option('page_on_front', (int) $page_id);
$settings = get_option('brt_settings', []);
if (is_array($settings)) {
$settings['translation'] = $settings['translation'] ?? 'WEB';
update_option('brt_settings', $settings, false);
}
echo 'seeded front page ' . $page_id . ' with ' . count($books) . ' books, ' . count($translations) . ' translations, and ' . (count($books) * count($translations) * 2) . ' reference links';
'@
Write-ContainerPhpFile $environment '/tmp/brt-seed-front.php' $seedPhp
Invoke-WpCli $environment 'wp eval-file /tmp/brt-seed-front.php --path=/var/www/html' | Out-Host