Bible.com iframe specific styles

Adds a class to html if bible.com is in an iframe. Adjusts font size of parallel versions to fit the left column. Accepts parent scrolling messages.

当前为 2025-05-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Bible.com iframe specific styles
  3. // @namespace Violentmonkey Scripts
  4. // @match *://www.bible.com/*
  5. // @grant none
  6. // @version 2.0b
  7. // 改善字體大細个調整成功率、增加單節頁面連結撳鈕
  8. // @author Aiuanyu x Gemini
  9. // @description Adds a class to html if bible.com is in an iframe. Adjusts font size of parallel versions to fit the left column. Accepts parent scrolling messages.
  10. // @description:zh-TW 當 bible.com 在 iframe 裡時,給 <html> 加個 class。調整並列版本个字體大小,讓佇左邊个欄位內看起來較好。接受上層網頁共下捲動个命令。
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const multiVersionLinkId = 'multi-version-link'; // Define globally within the IIFE
  17. let topWindowObserver = null; // To hold the observer for the top window
  18. let lastCheckedHrefForPolling = location.href; // For URL polling
  19.  
  20. if (window.self !== window.top) {
  21. document.documentElement.classList.add('is-in-iframe');
  22. console.log('Bible.com 在 iframe 內,已為 <html> 加入 "is-in-iframe" class。');
  23.  
  24. // 當 iframe 內容載入完成後,嘗試調整並列版本个字體大小
  25. window.addEventListener('load', function () {
  26. // 等待所有字體載入完成 (e.g., web fonts used by bible.com itself)
  27. document.fonts.ready.then(function () {
  28. // Add a small delay AFTER fonts are ready and page is loaded,
  29. // to give bible.com's own scripts more time to render dynamic content
  30. // before we start measuring heights.
  31. const initialAdjustmentDelay = 1500; // 1.5 秒
  32. console.log(`所有字體載入完成。等待 ${initialAdjustmentDelay}ms 後嘗試調整字體。`);
  33. setTimeout(function() {
  34. adjustParallelFontSize();
  35. }, initialAdjustmentDelay);
  36. }).catch(function (error) {
  37. console.warn('字體載入錯誤或超時,仍嘗試調整字體:', error);
  38. setTimeout(function() { adjustParallelFontSize(); }, 1500); // 若有錯誤,也延遲一下再試
  39. });
  40. });
  41. // 監聽來自父視窗 (index.html) 的訊息
  42. window.addEventListener('message', function(event) {
  43. // 為著安全,可以檢查訊息來源 event.origin
  44. // 但因為 index.html 可能係 file:// 協定,event.origin 會係 'null'
  45. // 所以,檢查 event.source 是不是 window.top 會較穩當
  46. // if (event.source !== window.top) { // 暫時允許任何來源,方便本地測試
  47. // console.log('Userscript: Message ignored, not from top window or expected origin.');
  48. // return;
  49. // }
  50.  
  51. if (event.data && event.data.type === 'SYNC_SCROLL_TO_PERCENTAGE') {
  52. const percentage = parseFloat(event.data.percentage);
  53. if (isNaN(percentage) || percentage < 0 || percentage > 1) {
  54. console.warn('Userscript: 收到無效个捲動百分比:', event.data.percentage);
  55. return;
  56. }
  57.  
  58. const de = document.documentElement;
  59. const scrollableDistance = de.scrollHeight - de.clientHeight;
  60. if (scrollableDistance <= 0) {
  61. // console.log('Userscript: 內容毋需捲動。');
  62. return;
  63. }
  64. const scrollToY = scrollableDistance * percentage;
  65. // console.log(`Userscript: Scrolling to ${percentage*100}%, ${scrollToY}px. Scrollable: ${scrollableDistance}, Total: ${de.scrollHeight}, Visible: ${de.clientHeight}`);
  66. window.scrollTo({ top: scrollToY, behavior: 'auto' }); // 'auto' 表示立即捲動
  67. }
  68. });
  69.  
  70. } else {
  71. console.log('Bible.com 為頂層視窗。');
  72. // 當 bible.com 在頂層視窗時,執行个邏輯 (等待 DOM 載入完成)
  73. if (document.readyState === 'loading') {
  74. document.addEventListener('DOMContentLoaded', mainTopWindowLogic);
  75. } else {
  76. mainTopWindowLogic(); // DOM 已載入
  77. }
  78. }
  79.  
  80. function mainTopWindowLogic() {
  81. console.log('DOM 已就緒,初始化頂層視窗个多版本連結邏輯。');
  82. // 初次嘗試加入/更新連結
  83. addOrUpdateMultiVersionLink();
  84.  
  85. // 設定 MutationObserver 來監測 DOM 變化
  86. if (topWindowObserver) {
  87. topWindowObserver.disconnect();
  88. }
  89. topWindowObserver = new MutationObserver((mutations) => {
  90. // 任何 DOM 變化都可能影響目標或 URL,所以重新執行檢查邏輯
  91. // console.log('偵測到 Body 內容變動,重新評估連結狀態。'); // 這訊息可能會太頻繁
  92. addOrUpdateMultiVersionLink();
  93. });
  94.  
  95. // 監測 body 元素个子節點列表及子樹變化
  96. topWindowObserver.observe(document.body, { childList: true, subtree: true });
  97.  
  98. // 監聽瀏覽歷史變化事件
  99. window.addEventListener('popstate', () => { console.log('popstate 事件觸發,重新評估連結狀態。'); addOrUpdateMultiVersionLink(); });
  100. window.addEventListener('hashchange', () => { console.log('hashchange 事件觸發,重新評估連結狀態。'); addOrUpdateMultiVersionLink(); });
  101.  
  102. // 定時檢查 URL 變化 (作為 pushState 等事件个備援方案)
  103. setInterval(() => {
  104. if (location.href !== lastCheckedHrefForPolling) {
  105. console.log(`偵測到 URL 變化 (輪詢):${lastCheckedHrefForPolling} -> ${location.href},重新評估連結狀態。`);
  106. lastCheckedHrefForPolling = location.href;
  107. addOrUpdateMultiVersionLink();
  108. }
  109. }, 1000); // 每秒檢查一次
  110. }
  111.  
  112. // 取得書卷个中文顯示名稱 (Hakka/Traditional Chinese)
  113. function getBookDisplayName(bookCode) {
  114. const bookMap = {
  115. // Old Testament - 舊約
  116. "GEN": "創世記", "EXO": "出埃及記", "LEV": "利未記", "NUM": "民數記", "DEU": "申命記",
  117. "JOS": "約書亞記", "JDG": "士師記", "RUT": "路得記", "1SA": "撒母耳記上", "2SA": "撒母耳記下",
  118. "1KI": "列王紀上", "2KI": "列王紀下", "1CH": "歷代志上", "2CH": "歷代志下", "EZR": "以斯拉記",
  119. "NEH": "尼希米記", "EST": "以斯帖記", "JOB": "約伯記", "PSA": "詩篇", "PRO": "箴言",
  120. "ECC": "傳道書", "SNG": "雅歌", "ISA": "以賽亞書", "JER": "耶利米書", "LAM": "耶利米哀歌",
  121. "EZK": "以西結書", "DAN": "但以理書", "HOS": "何西阿書", "JOL": "約珥書", "AMO": "阿摩司書",
  122. "OBA": "俄巴底亞書", "JON": "約拿書", "MIC": "彌迦書", "NAM": "那鴻書", "HAB": "哈巴谷書",
  123. "ZEP": "西番雅書", "HAG": "哈該書", "ZEC": "撒迦利亞書", "MAL": "瑪拉基書",
  124. // New Testament - 新約
  125. "MAT": "馬太福音", "MRK": "馬可福音", "LUK": "路加福音", "JHN": "約翰福音", "ACT": "使徒行傳",
  126. "ROM": "羅馬書", "1CO": "哥林多前書", "2CO": "哥林多後書", "GAL": "加拉太書", "EPH": "以弗所書",
  127. "PHP": "腓立比書", "COL": "歌羅西書", "1TH": "帖撒羅尼迦前書", "2TH": "帖撒羅尼迦後書",
  128. "1TI": "提摩太前書", "2TI": "提摩太後書", "TIT": "提多書", "PHM": "腓利門書", "HEB": "希伯來書",
  129. "JAS": "雅各書", "1PE": "彼得前書", "2PE": "彼得後書", "1JN": "約翰一書", "2JN": "約翰二書",
  130. "3JN": "約翰三書", "JUD": "猶大書", "REV": "啟示錄"
  131. };
  132. return bookMap[bookCode.toUpperCase()] || bookCode; // 若尋無對應,就用原底个 bookCode
  133. }
  134.  
  135. function addOrUpdateMultiVersionLink() {
  136. const url = window.location.href;
  137. // Regex to capture book (e.g., PSA), chapter (e.g., 18), and verse (e.g., 2)
  138. // The presence of a verse number indicates a single verse page.
  139. const singleVerseRegex = /\/bible\/\d+\/([A-Z1-3]{3})\.(\d+)\.(\d+)/;
  140. const match = url.match(singleVerseRegex);
  141. const targetDivSelector = 'div.flex.flex-col.md\\:flex-row.items-center.gap-2.mbs-3';
  142. const existingLink = document.getElementById(multiVersionLinkId);
  143.  
  144. if (!match) {
  145. // 非單節經文頁面
  146. if (existingLink) {
  147. console.log('非單節經文頁面,移除已存在个多版本對照連結。');
  148. existingLink.remove();
  149. }
  150. return;
  151. }
  152.  
  153. // 係單節經文頁面
  154. const book = match[1];
  155. const chapter = match[2];
  156. const bookNameToDisplay = getBookDisplayName(book);
  157. const targetDiv = document.querySelector(targetDivSelector);
  158.  
  159. if (!targetDiv) {
  160. // console.warn('目標 <div> (' + targetDivSelector + ') 未尋到。連結暫時無法加入。'); // 可能太頻繁
  161. if (existingLink) {
  162. // 這情況較少見,若目標 div 毋見忒,照理講連結也應該毋在,但做為清理
  163. console.log('目標 div 未尋到,移除可能殘留个連結。');
  164. existingLink.remove();
  165. }
  166. return;
  167. }
  168.  
  169. // 目標 div 存在
  170. const expectedHref = `https://aiuanyu.github.io/6BibleVersions/?book=${book}&chapter=${chapter}`;
  171. const expectedTextContent = `4 語言 6 版本對照讀 ${bookNameToDisplay} ${chapter}`;
  172.  
  173. if (existingLink) {
  174. // 連結已存在,檢查並更新
  175. let updated = false;
  176. if (existingLink.parentElement !== targetDiv) {
  177. // console.log('連結存在但毋在正確个父元素內,重新插入。');
  178. targetDiv.insertBefore(existingLink, targetDiv.firstChild); // 移到正確位置
  179. updated = true;
  180. }
  181. if (existingLink.href !== expectedHref) {
  182. existingLink.href = expectedHref;
  183. // console.log(`更新連結 href: ${expectedHref}`);
  184. updated = true;
  185. }
  186. const pElement = existingLink.querySelector('p');
  187. if (pElement && pElement.textContent !== expectedTextContent) {
  188. pElement.textContent = expectedTextContent;
  189. // console.log(`更新連結文字: ${expectedTextContent}`);
  190. updated = true;
  191. }
  192. if (updated) console.log(`多版本對照連結已更新: ${bookNameToDisplay} ${chapter}`);
  193. return;
  194. }
  195.  
  196. // 連結毋存在,建立並加入
  197. // console.log(`準備為 ${bookNameToDisplay} ${chapter} 建立多版本對照連結。`);
  198. const newLink = document.createElement('a');
  199. newLink.className = "overflow-hidden font-bold ease-in-out duration-100 focus:outline-2 focus:outline-info-light dark:focus:outline-info-dark hover:shadow-light-2 disabled:text-gray-50 dark:disabled:bg-gray-40 dark:disabled:text-white disabled:hover:shadow-none disabled:opacity-50 disabled:bg-gray-10 disabled:cursor-not-allowed w-full max-w-fit bg-gray-15 dark:bg-gray-35 text-gray-50 dark:text-white hover:bg-gray-10 dark:hover:bg-gray-30 active:bg-gray-20 dark:active:bg-gray-40 rounded-3 text-xs pis-2 pie-3 h-6 cursor-pointer flex md:w-min items-center group static no-underline";
  200. newLink.href = expectedHref;
  201. newLink.target = '_blank'; // 在新分頁打開
  202. newLink.rel = 'noopener noreferrer';
  203. newLink.id = multiVersionLinkId; // 設定 ID
  204.  
  205. const svgString = `
  206. <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 mie-0.5" size="24">
  207. <path d="M16 3H5C3.89543 3 3 3.89543 3 5V16H5V5H16V3Z" style="--darkreader-inline-fill: currentColor;" data-darkreader-inline-fill=""></path>
  208. <path d="M20 7H9C7.89543 7 7 7.89543 7 9V20C7 21.1046 7.89543 22 9 22H20C21.1046 22 22 21.1046 22 20V9C22 7.89543 21.1046 7 20 7ZM20 20H9V9H20V20Z" style="--darkreader-inline-fill: currentColor;" data-darkreader-inline-fill=""></path>
  209. </svg>`;
  210. const tempDiv = document.createElement('div');
  211. tempDiv.innerHTML = svgString.trim();
  212. const svgElement = tempDiv.firstChild;
  213. if (svgElement) {
  214. newLink.appendChild(svgElement);
  215. }
  216.  
  217. const textSpan = document.createElement('span');
  218. textSpan.className = 'truncate';
  219. const pElement = document.createElement('p');
  220. pElement.className = 'text-text-light dark:text-text-dark font-aktiv-grotesk mis-1';
  221. pElement.textContent = expectedTextContent;
  222. textSpan.appendChild(pElement);
  223. newLink.appendChild(textSpan);
  224.  
  225. // 使用 requestAnimationFrame 來確保插入操作在瀏覽器繪製下一幀前進行
  226. requestAnimationFrame(() => {
  227. // 再次檢查目標 div 同連結狀態,避免在 rAF 等待期間發生變化
  228. const currentTargetDiv = document.querySelector(targetDivSelector);
  229. if (currentTargetDiv && !document.getElementById(multiVersionLinkId)) {
  230. currentTargetDiv.insertBefore(newLink, currentTargetDiv.firstChild);
  231. console.log(`已為 ${bookNameToDisplay} ${chapter} 加入「4語言6版本對照讀」連結。`);
  232. } else if (!currentTargetDiv) {
  233. // console.log('目標 div 在 rAF 執行前毋見忒。');
  234. } else {
  235. // console.log('連結在 rAF 執行前已經分其他程序加入。');
  236. }
  237. });
  238. }
  239.  
  240. function adjustParallelFontSize(retryAttempt = 0) {
  241. const MAX_RETRIES = 10; // 增加重試次數
  242. const RETRY_DELAY = 1200; // 每次重試之間等 1.2 秒鐘
  243. const MIN_COLUMN_HEIGHT_THRESHOLD = 50; // px, 用來判斷內容敢有顯示出來个基本高度
  244.  
  245. try { // try...catch 包住整個函數个內容
  246. const params = new URLSearchParams(window.location.search);
  247. const parallelVersionId = params.get('parallel');
  248.  
  249. if (!parallelVersionId) {
  250. console.log('網址中無尋到並列版本 ID,跳過字體調整。');
  251. return;
  252. }
  253.  
  254. // 取得左欄主要版本个 ID
  255. const pathSegments = window.location.pathname.match(/\/bible\/(\d+)\//);
  256. if (!pathSegments || !pathSegments[1]) {
  257. console.log('無法從路徑中提取主要版本 ID,跳過字體調整。');
  258. return;
  259. }
  260. const mainVersionId = pathSegments[1];
  261.  
  262. const leftDataVidSelector = `[data-vid="${mainVersionId}"]`;
  263. const rightDataVidSelector = `[data-vid="${parallelVersionId}"]`;
  264.  
  265. // 找出並列閱讀个主要容器 (根據先前个 HTML 結構)
  266. const parallelContainer = document.querySelector('div.grid.md\\:grid-cols-2, div.grid.grid-cols-1.md\\:grid-cols-2');
  267. if (!parallelContainer) {
  268. console.log('並列容器 (例如 div.grid.md:grid-cols-2) 未尋到。');
  269. return;
  270. }
  271.  
  272. const columns = Array.from(parallelContainer.children).filter(el => getComputedStyle(el).display !== 'none');
  273. if (columns.length < 2) {
  274. console.log('在並列容器中尋到少於兩个可見欄位。');
  275. return;
  276. }
  277.  
  278. const leftColumnEl = columns[0];
  279. const rightColumnEl = columns[1];
  280.  
  281. const leftVersionDiv = leftColumnEl.querySelector(leftDataVidSelector);
  282. const rightVersionDiv = rightColumnEl.querySelector(rightDataVidSelector);
  283.  
  284. if (!leftVersionDiv || !rightVersionDiv) {
  285. console.log(`左欄 (${leftDataVidSelector}) 或右欄 (${rightDataVidSelector}) 个內容 div 未尋到。`);
  286. if (retryAttempt < MAX_RETRIES -1) { // 為元素搜尋保留一些重試次數
  287. console.warn(`在 ${RETRY_DELAY}ms 後重試元素搜尋 (嘗試 ${retryAttempt + 1}/${MAX_RETRIES})`);
  288. setTimeout(() => adjustParallelFontSize(retryAttempt + 1), RETRY_DELAY);
  289. } else {
  290. console.error('內容 div 元素搜尋在最大重試次數後失敗。中止字體調整。');
  291. }
  292. return; // 若元素無尋到,愛 return 避免錯誤
  293. }
  294.  
  295. // At this point, elements are found. Now check if they have rendered content.
  296. // Force reflow before measurement
  297. leftVersionDiv.offsetHeight;
  298. rightVersionDiv.offsetHeight; // Ensure reflow before measurement
  299. const currentLeftHeight = leftVersionDiv.offsetHeight;
  300.  
  301. if (currentLeftHeight < MIN_COLUMN_HEIGHT_THRESHOLD && retryAttempt < MAX_RETRIES) { // 若左欄高度無夠
  302. console.warn(`左欄高度 (${currentLeftHeight}px) 低於門檻值 (${MIN_COLUMN_HEIGHT_THRESHOLD}px)。內容可能尚未完全渲染。在 ${RETRY_DELAY}ms 後重試 (嘗試 ${retryAttempt + 1}/${MAX_RETRIES})`);
  303. setTimeout(() => adjustParallelFontSize(retryAttempt + 1), RETRY_DELAY);
  304. return;
  305. }
  306. if (currentLeftHeight < MIN_COLUMN_HEIGHT_THRESHOLD && retryAttempt >= MAX_RETRIES) { // 重試了後還係無夠高
  307. console.error(`左欄高度 (${currentLeftHeight}px) ${MAX_RETRIES} 次重試後仍低於門檻值。中止字體調整。`);
  308. return; // 放棄調整
  309. }
  310.  
  311. console.log('尋到左欄版本 Div:', leftVersionDiv, '尋到右欄版本 Div:', rightVersionDiv);
  312.  
  313. // 使用 requestAnimationFrame 來確保 DOM 操作和測量是在瀏覽器準備好繪製下一幀之前進行
  314. // If we reach here, elements are found and left column has some content.
  315. requestAnimationFrame(() => {
  316. // 開始調整字體
  317. let currentFontSize = 90; // 初始字體大小
  318. rightVersionDiv.style.fontSize = currentFontSize + '%';
  319.  
  320. // The leftHeight from *before* rAF (currentLeftHeight) should be the reference.
  321. let leftHeight = currentLeftHeight;
  322. // Ensure right column also reflows with its new font size
  323. let rightHeight = rightVersionDiv.offsetHeight; // 獲取初始高度
  324.  
  325. console.log(`初始檢查字體 ${currentFontSize}%:右欄高度 ${rightHeight}px,左欄高度 ${leftHeight}px (參考值)`);
  326.  
  327. // 如果初始字體大小就已經讓右邊內容不比左邊長,就不用調整了
  328. if (rightHeight <= leftHeight) {
  329. console.log('初始字體大小 ' + currentFontSize + '% 已足夠或更短。');
  330. return;
  331. }
  332.  
  333. // 如果初始字體大小讓右邊內容比左邊長,就開始縮小字體
  334. // 預設使用最小个測試字體 (50%),假使所有測試過个字體都還係分右邊太長。
  335. let bestFitFontSize = 50;
  336. let foundOptimalAdjustment = false;
  337.  
  338. for (let testSize = currentFontSize - 1; testSize >= 50; testSize--) { // 從比初始值小1%開始,最細到 50%
  339. rightVersionDiv.style.fontSize = testSize + '%';
  340. rightHeight = rightVersionDiv.offsetHeight; // 每次改變字體大小後,重新獲取高度 (強制 reflow)
  341.  
  342. if (rightHeight > leftHeight) {
  343. // 這隻 testSize 還係分右邊太長,繼續試較細个字體。
  344. // 假使這係迴圈最後一次 (testSize == 50) 而且還係太長,
  345. // bestFitFontSize 會維持在 50%。
  346. } else {
  347. // 這隻 testSize 分右邊內容變到毋比左邊長了 (<=)。
  348. // 照你个要求,𠊎等愛用前一隻字體大細 (testSize + 1),
  349. // 因為該隻字體大細會分右邊「略略仔長過左邊」。
  350. bestFitFontSize = testSize + 1;
  351. foundOptimalAdjustment = true;
  352. console.log(`右欄內容在 ${testSize}% 時變短/相等 (高度 ${rightHeight}px)。套用前一個較大个字體 ${bestFitFontSize}%。`);
  353. break; // 尋到臨界點了,跳出迴圈
  354. }
  355. }
  356.  
  357. if (!foundOptimalAdjustment && currentFontSize > 50) {
  358. // 假使迴圈跑完,foundOptimalAdjustment 還係 false,
  359. // 表示從 (currentFontSize - 1) 到 50% 所有字體都還係分右邊太長。
  360. // 在這情況下,bestFitFontSize 已經係 50%。
  361. console.log(`所有測試過个字體 (從 ${currentFontSize - 1}% 50%) 仍使右欄內容過長。使用最小測試字體:50%。`);
  362. }
  363.  
  364. // 迴圈結束後,將字體設定為決定好个大小
  365. rightVersionDiv.style.fontSize = bestFitFontSize + '%';
  366. // 為著準確記錄最終狀態,重新量一次高度
  367. const finalRightHeight = rightVersionDiv.offsetHeight;
  368. console.log('' + rightDataVidSelector + ' 最終調整後字體大小為 ' + bestFitFontSize + '%。最終右欄內容高度:' + finalRightHeight + 'px,左欄內容高度:' + leftHeight + 'px');
  369. });
  370. } catch (error) {
  371. console.error('調整字體大小期間發生錯誤:', error);
  372. }
  373. }
  374. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址