My Bible

譯本解析: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-usfmBOOK.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"
  • data-usfm="1CH.1" → 歷代志上第1章
  • data-usfm="PSA.119" → 詩篇第119篇
  • data-usfm="SNG.1" → 雅歌第1章
__verse節容器逐一走訪,解析 data-usfm 取節號
  • data-usfm="1CH.1.1" → 歷代志上1:1
  • data-usfm="SNG.1.1" → 雅歌1:1
  • data-usfm="1CH.16.12+1CH.16.13" → 合併節(見下文)
__content實際文字 leaf span直接取 .text,終止遞迴
  • "亞當"、"塞特"、"以挪士" — 名字(1Chr 1:1)
  • "生" — 動詞補充語(__add 下的 __content)
  • ";"、" " — 標點與空格也是獨立 __content
__label章號、節號、腳註記號return "",整棵跳過
  • "1"、"2"、"3" — 章節號
  • "#" — 腳註點(__note 的第一個子 span)
  • 章首也有 __label 包住章號(如 "1")
__note腳註全文return "",整棵子樹跳過
  • 1Chr 1:1 有 4 個腳註,全在 __note 下
  • 結構:__note > __label("#") + .ft(腳註文字)
  • 若不跳過,腳註文字會混入節文
__pn專有名詞 wrapper遞迴收集子節點 __content
  • "亞當"、"塞特"、"以挪士"、"該南" — 1Chr 1:1-2
  • "瑪勒列"、"雅列"、"以諾"、"瑪土撒拉" — 1Chr 1:3-4
  • NIV: "Adam"、"Seth"、"Enosh" — 同樣用 __pn 包
__add補充語 wrapper(斜體顯示)遞迴收集子節點 __content
  • "生" — 亞當生塞特中的動詞(1Chr 1:1)
  • NIV 中常見於原文無對應、譯者補字的詞
  • 如 "he" 或 "the" 等補充詞
__heading段落標題文字 spanSTRUCT 定為 leaf(null)——直接含 DOM 文字節點,無 CC 子元素;不被納入節文
  • "從亞當到亞伯拉罕" — 1Chr 1 段落標題
  • "(創5‧1-32;10‧1-32;11‧10-26)" — 交叉引用標題
  • "從以掃到以東諸王" — 1Chr 1 下段標題
__q1 / __q2詩行(第1/2縮排層)container照常遞迴,內含 __verse;本身不含文字
  • 1Chr 12:18 詩歌體感謝詞在 __q1 下
  • Ps 1:1 詩篇第1節也在 __q1
  • 約伯記、箴言詩行皆用 __q1/__q2 分層
__qs詩結語(Selah、詩中詩)container照常遞迴,內含 __content
  • Hab 3 哈巴谷書詩歌中有 __qs
  • __qs 下直接有 __content,不透過 __verse
  • Selah(細拉)通常在 __qs 內
__sp說話人標籤 container含 __heading,遞迴後為空(heading 無 __content)
  • "新郎說"、"新娘說" — SNG.1.1 雅歌說話人
  • 該 span 只含 __heading,text 不進節文
  • 判斷說話人需另外處理,本解析器略過
__p散文段落 container照常遞迴,內含 __verse;本身不含文字
  • __p — 一般散文段落,創世記、歷史書常見
  • NIV:__p 前可能有章號腳註(__note),整棵跳過
  • 結構相同,常見於歷史書、書信
__b詩歌空行(blank line)空 div,無子節點,遞迴後為空字串
  • 1Chr 16 詩歌段落間有 __b 空行
  • 視覺上製造段落呼吸感,不含任何文字
  • 詩篇、先知書詩歌段落常見
__s / __ms段落標題 / 主標題 container遞迴,只含 __heading,結果為空
  • __s — 小段標題,如 "從亞當到亞伯拉罕"(1Chr 1)
  • __ms — 詩篇大段標題,如 "第一卷(詩1-41篇)"(Ps 1)
  • 兩者都只含 __heading,不含節文
__r交叉引用 container遞迴,只含 __heading,結果為空
  • "(創5‧1-32;10‧1-32)" — 平行段落引用(1Chr 1)
  • 緊接在 __s 後面,與段落標題相鄰
  • 僅顯示用,不含節文內容
__s2次級段落標題 container遞迴,只含 __heading,結果為空
  • 部份書卷有多層標題層級時出現
  • 結構與 __s 相同,CSS class 不同
  • 罕見,主要在先知書複雜段落結構中

合併節(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
chapterlabel, 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
verselabel, content, pn, add, note, qs, nd, wj, it, tl, sc
wj(耶穌話語)content, pn, add, note, nd, tl
note(腳註)label, body
p / q1 / nb / li1content, verse(, note)
ms / s / s1 / r / spheading(, nd)
d(詩篇標題)content, verse, note, tl, nd
content / label / headingnull(葉節點)

已知特例:node-html-parser 會將腳註體(__body)內的 __tl/__it 提升為兄弟節點,導致其後 __wj 被誤認為子節點——STRUCT 為此允許 it/tl → wj。驗證器不遞迴進入 __note 子樹(NHP 在腳註體內嵌結構不可靠)。

OSIS → YouVersion USFM 書卷代碼

YouVersion URL 與 data-usfm 使用 USFM 書卷縮寫(如 1CHPSA),與 OSIS 標準(1ChrPs)不同。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]