譯本解析:YouVersion HTML
translations/scripts/parse-youversion.ts — node-html-parser DOM 走訪法
YouVersion 章節頁是 Next.js SSR 輸出的靜態 HTML,經文以 React component 渲染成帶有語意 CSS class 的 span 樹。解析器用 node-html-parser 走 DOM 樹,依 class suffix 分類處理每個節點——收集 __content、跳過 __label 和 __note、遞迴進入 __pn 和 __add——不依賴正則或元素在字串中的相對位置。
HTML 結構概覽
每個章節頁包含一個 chapter 容器,其 data-usfm 屬性值為 BOOK.CH(例如 1CH.1)。容器內每節是一個 verse span,data-usfm 為 BOOK.CH.V。節內的文字全部存放在 __content leaf span 中;其它 span 是排版或元資料用途。
<div data-usfm="1CH.1" class="...___chapter..."> ← 章容器
<div class="...___s...">
<span class="...___heading...">從亞當到亞伯拉罕</span> ← 段落標題,跳過
<span class="...___heading...">(創5‧1-32)</span> ← 交叉引用標題,跳過
</div>
<div data-usfm="1CH.1.1" class="...___verse...">
<span class="...___label...">1</span> ← 節號,跳過
<span class="...___content..."> </span> ← 空格
<span class="...___pn...">
<span class="...___content...">亞當</span> ← 專有名詞,遞迴收
</span>
<span class="...___add...">
<span class="...___content...">生</span> ← 補充語,遞迴收
</span>
<span class="...___pn...">
<span class="...___content...">塞特</span>
</span>
<span class="...___note..."> ← 腳註,整棵跳過
<span class="...___label...">#</span>
<span class="ft">腳註文字…</span>
</span>
</div>
<div data-usfm="1CH.16.12+1CH.16.13" class="...___verse...">
... ← 合併節:同文字寫入兩節號
</div>
<div class="...___b..."></div> ← 詩歌空行,無子節點
<div class="...___q1...">
<span data-usfm="1CH.12.18" class="...___verse..."> ← 詩行中的節
...
</span>
</div>
</div>CSS class 含有 CSS Module hash(如 ChapterContent-module__cat7xG__content),解析時只判斷 __suffix 是否存在於 class 字串中,不依賴完整 class 名。
Class Suffix 語意對照
| Class suffix | 語意 | 處理方式 | 真實範例(來自抓取 HTML) |
|---|---|---|---|
| __chapter | 章容器 | querySelector 入口;data-usfm="BOOK.CH" |
|
| __verse | 節容器 | 逐一走訪,解析 data-usfm 取節號 |
|
| __content | 實際文字 leaf span | 直接取 .text,終止遞迴 |
|
| __label | 章號、節號、腳註記號 | return "",整棵跳過 |
|
| __note | 腳註全文 | return "",整棵子樹跳過 |
|
| __pn | 專有名詞 wrapper | 遞迴收集子節點 __content |
|
| __add | 補充語 wrapper(斜體顯示) | 遞迴收集子節點 __content |
|
| __heading | 段落標題文字 span | STRUCT 定為 leaf(null)——直接含 DOM 文字節點,無 CC 子元素;不被納入節文 |
|
| __q1 / __q2 | 詩行(第1/2縮排層)container | 照常遞迴,內含 __verse;本身不含文字 |
|
| __qs | 詩結語(Selah、詩中詩)container | 照常遞迴,內含 __content |
|
| __sp | 說話人標籤 container | 含 __heading,遞迴後為空(heading 無 __content) |
|
| __p | 散文段落 container | 照常遞迴,內含 __verse;本身不含文字 |
|
| __b | 詩歌空行(blank line) | 空 div,無子節點,遞迴後為空字串 |
|
| __s / __ms | 段落標題 / 主標題 container | 遞迴,只含 __heading,結果為空 |
|
| __r | 交叉引用 container | 遞迴,只含 __heading,結果為空 |
|
| __s2 | 次級段落標題 container | 遞迴,只含 __heading,結果為空 |
|
合併節(Combined Verses)
部份節的 data-usfm 以 + 連接多個節號,表示同一段文字同時屬於多個節(通常是原文分節方式與英文傳統不同):
data-usfm="1CH.16.12+1CH.16.13" → 以 "+" 分割 → ["1CH.16.12", "1CH.16.13"] → 取各自第三段 → verseNums = [12, 13] → 同一段文字同時寫入 Map.set(12, text) 和 Map.set(13, text)
後處理:cleanSpacing
| 規則 | 對應問題 | 範例(before → after) |
|---|---|---|
| U+2006(six-per-em space)移除 | YouVersion 排版微調用不可見窄空格,常出現在名字前後 | "神 說" → "神說" |
| \s+— → — | em-dash 前多餘空格(NIV 常見,中文版也有) | "the Lord — he" → "the Lord— he" |
| 相鄰引號間空格移除 | 巢狀引號分開兩個 __content span,join 時注入空格 | "“ ‘foo’" → "“‘foo’" |
| closing+opening 引號補空格 | 前一句結束引號與下一句開始引號直接相鄰 | "’“" → "’ “" |
| \s+ 折疊為單一空格,trim() | 多個 __content join 後可能留下多重空白 | "foo bar " → "foo bar" |
結構驗證(STRUCT schema)
解析器內建 STRUCT 常數,定義每種 suffix 允許的直接子 suffix。對全部章節跑 --validate 旗標,可驗證實際 DOM 是否符合預期結構:
npx tsx translations/scripts/parse-youversion.ts --version=CUNP --validate # CUNP 31,092 節,結構驗證通過,無違規
STRUCT 值為 null 表示葉節點(不應有 CC 子元素);值為 [] 表示子元素使用非 ChapterContent class(如 __table 內部)。三譯本共用同一 schema,驗證通過代表三譯本結構均符合預期。
| 父 suffix | 允許的直接子 suffix |
|---|---|
| chapter | label, ms, ms1, s, s1, s2, s4, r, mr, sp, cl, d, p, q1–q4, li1–li2, pm, pi, pi1, mi, nb, m, pr, qr, qc, qa, qm, qm1, qm2, pc, pmc, pmr, iex, b, table |
| verse | label, content, pn, add, note, qs, nd, wj, it, tl, sc |
| wj(耶穌話語) | content, pn, add, note, nd, tl |
| note(腳註) | label, body |
| p / q1 / nb / li1 | content, verse(, note) |
| ms / s / s1 / r / sp | heading(, nd) |
| d(詩篇標題) | content, verse, note, tl, nd |
| content / label / heading | null(葉節點) |
已知特例:node-html-parser 會將腳註體(__body)內的 __tl/__it 提升為兄弟節點,導致其後 __wj 被誤認為子節點——STRUCT 為此允許 it/tl → wj。驗證器不遞迴進入 __note 子樹(NHP 在腳註體內嵌結構不可靠)。
OSIS → YouVersion USFM 書卷代碼
YouVersion URL 與 data-usfm 使用 USFM 書卷縮寫(如 1CH、PSA),與 OSIS 標準(1Chr、Ps)不同。OSIS_TO_YV 對照表統一轉換,不在解析邏輯內硬編碼。
Gen → GEN Exod → EXO Lev → LEV Num → NUM Deut → DEU Josh → JOS Judg → JDG Ruth → RUT 1Sam → 1SA 2Sam → 2SA 1Kgs → 1KI 2Kgs → 2KI 1Chr → 1CH 2Chr → 2CH Ezra → EZR Neh → NEH Esth → EST Job → JOB Ps → PSA Prov → PRO Eccl → ECC Song → SNG Isa → ISA Jer → JER Lam → LAM Ezek → EZK Dan → DAN Hos → HOS Joel → JOL Amos → AMO Obad → OBA Jonah→ JON Mic → MIC Nah → NAM Hab → HAB Zeph → ZEP Hag → HAG Zech → ZEC Mal → MAL Matt → MAT Mark → MRK Luke → LUK John → JHN Acts → ACT Rom → ROM 1Cor → 1CO 2Cor → 2CO Gal → GAL Eph → EPH Phil → PHP Col → COL 1Thess→1TH 2Thess→2TH 1Tim → 1TI 2Tim → 2TI Titus→ TIT Phlm → PHM Heb → HEB Jas → JAS 1Pet → 1PE 2Pet → 2PE 1John→ 1JN 2John→ 2JN 3John→ 3JN Jude → JUD Rev → REV
用法:npx tsx translations/scripts/parse-youversion.ts --version=CUNP [--validate]