台灣親屬祖宗十八代稱謂計算器(Web Component)— 開發經驗分享

專案概述

一款完全封裝式的客戶端 Web Component,專為計算中文(含台灣)親屬稱謂所設計。此工具支援無限層級的直系關係(祖先與後代)以及複雜的旁系關係,採用查詢表與演算法推導相結合的混合式方法。



「爸爸的哥哥的老婆的姊姊的兒子,到底要叫什麼?」

如果你曾在過年團圓飯上被長輩考倒,或是在婚禮場合對著陌生親戚尷尬微笑,你就會明白為什麼中文親屬稱謂堪稱人類語言中最精密的分類系統之一。

這篇文章記錄了一個為期一天、歷經 8 輪迭代的開發旅程:從一個滿是 bug 的原型,到一個能在 WordPress 環境中穩定運行、覆蓋率達 93.8% 的中文親屬稱謂計算機 Web Component。過程中踩過的坑,比親戚關係還複雜。


為什麼選擇 Vanilla JS?

這個專案從頭到尾沒有引入任何框架,沒有 React,沒有 Vue,沒有 Svelte。不是因為排斥框架,而是因為使用場景決定了技術選型:

部署目標是 WordPress。 元件需要被貼進任何 WordPress 頁面的 HTML 區塊,立即生效,不需要建構工具、不需要 npm install、不需要 webpack config。一個 <family-calculator></family-calculator> 標籤加一段 <script>,完事。

Custom Elements + Shadow DOM 的組合恰好完美契合這個需求:封裝、隔離、自注冊,瀏覽器原生支援,零依賴。

最終產出是一個 76KB 的單一 HTML 檔案,裡面包含 902 行 JavaScript、完整的 CSS 樣式、120 組 baseMap 對照表、四座推導引擎、以及一套依據台灣民法第 983 條實作的婚姻禁止判定系統。


中文稱謂系統的恐怖之處

在英文裡 "cousin" 一個詞打天下。在中文裡,光是表達「父親的兄弟的兒子」就有四種可能的稱呼:堂哥、堂弟、堂兄、堂弟,而且這還只是父系。母系要另外算,姻親又是另一套體系。

中文親屬稱謂的複雜度來自四個維度的交叉:

  • 父系 vs. 母系:伯父(父親的哥哥)和舅父(母親的兄弟)是完全不同的稱謂。「堂」屬父系同姓,「表」屬母系或異姓。
  • 年齡序位:伯父(父親的「哥哥」)和叔父(父親的「弟弟」)不同;嫂嫂(兄之妻)和弟媳(弟之妻)不同。
  • 性別標記:姪子 vs. 姪女、外甥 vs. 外甥女、孫 vs. 孫女,每一代都要區分。
  • 姻親展開:姑母的丈夫是「姑丈」、姨母的丈夫是「姨丈」、伯父的妻子是「伯母」、叔父的妻子是「嬸嬸」——不是簡單加上「的丈夫/的妻子」就行的。

這意味著計算機不能用簡單的查表法,組合爆炸讓靜態對照表不可能窮舉。10 個關係按鈕,每條鏈路最多可以延伸到 8 步以上,理論組合數是天文數字。


架構設計:查表 + 推導引擎的雙層策略

最終架構分為兩層:

第一層:baseMap 靜態查表

120 組預定義的短鏈對照,涵蓋 1 到 3 步的常見親屬關係:

'f'         → 父親
'f,xb'      → 伯父
'm,xs,h'    → 姨丈
'f,f,xs'    → 姑婆
's,s,s'     → 曾孫

baseMap 的作用是「錨點」,提供精準的起始稱謂,讓推導引擎有所依據。

第二層:四引擎動態推導

當鏈路長度超過 baseMap 覆蓋範圍時,系統會找到最長匹配的子鏈,然後把剩餘步驟交給四座推導引擎逐步衍生:

  • getChildTitle(子女引擎):已知「伯父」,求其「兒子」→ 堂兄弟。9 大類規則,109 個 return 路徑。
  • getSpouseTitle(配偶引擎):已知「姨母」,求其「丈夫」→ 姨丈。13 大類規則,涵蓋直系、旁系、姻親配偶。
  • getSiblingTitle(手足引擎):已知「母親」,求其「姊姊」→ 姨母。12 大類規則,從同輩到堂表到配偶手足。
  • getParentTitle(父母引擎):已知「姪子」,求其「父親」→ 兄弟。15 大類規則,支援反向追溯。

四引擎透過 deriveRelative 串聯,可以無限鏈接。例如:

妻 → 妹 → 子 → 子 → 子

第 1 步:baseMap 查到 'w,ls' → 小姨子
第 2 步:getChildTitle('小姨子', son) → 姨甥
第 3 步:getChildTitle('姨甥', son) → 姨甥孫
第 4 步:getChildTitle('姨甥孫', son) → 曾姨甥孫 ✅

最精妙的是 spouseEquiv(配偶等價表),它讓子女引擎能穿透姻親。例如「伯母的兒子」:子女引擎看到「伯母」,查 spouseEquiv 得到「伯父」,再算「伯父的兒子」= 堂兄弟。這個設計讓引擎不需要為每個姻親稱謂重複寫規則。


八輪迭代:一天之內的進化史

Round 1:初始除蟲 8 個 Bug 的震撼教育

拿到原始程式碼的第一件事是跑一遍基本測試。結果慘不忍睹。

  • Bug #1:母系後代缺少「外」字前綴。 女→子 應該是「外孫」,程式輸出「孫」。solveDescendant 函式完全沒有追蹤母系路徑。
  • Bug #2:deriveRelative 缺少手足子女規則。 兄→子 應該是「姪子」,程式輸出「遠房親戚」。
  • Bug #3:resolveMixedChain 的分割邏輯有誤。 它只嘗試從頭切分子鏈,無法處理需要從中間斷開的複合路徑。
  • Bug #4-8: baseMap 只有 30 組條目、性別切換邏輯反轉、Grid 最後一列空洞、結果顯示缺少容器。

這一輪的教訓:「能算出東西」和「算對東西」之間隔著一個太平洋。

Round 2:深層鏈路——getChildTitle 的 8 類擴充

使用者報告 妻→妹→子 輸出「遠房親戚」(正確答案:姨甥)。追蹤發現 getChildTitle 不認識配偶手足(小姨子、大伯、小舅子……),全部 return null。

一口氣新增 8 大類姻親子女推導,包含姨甥、姑甥、舅甥、堂姪、表甥等完整衍生體系。

核心洞察: 中文稱謂的子女推導不是「統一加一個後綴」,而是根據前一個稱謂的語系(父系/母系/姻親)和輩分(同輩/長輩/晚輩)決定完全不同的衍生路徑。

Round 3:上行下行鏈——getChildTitle 的全面重寫

父→子 應該是「兄弟」,但程式輸出「遠房親戚」。

這不是缺幾條規則的問題——是 整個類別的缺失。getChildTitle 只會處理「我的配偶/我的手足」的子女,完全不知道如何處理祖先節點的子女(祖父→子→子 = 堂兄弟)。

這一輪從 2 類規則重寫為 9 大類,是整個專案最大規模的邏輯重構。

Round 4:手足引擎——從 2 條規則到 12 大類

母→弟→姊 應該是「姨母」,但 getSiblingTitle 只有 2 條規則(丈夫的手足、妻子的手足),其他所有輸入一律 return null。

補完後的 12 大類涵蓋:配偶手足、手足的手足、父母的手足、父系旁系、母系旁系、祖輩旁系、堂表同輩、堂表上輩、子女手足、姪甥手足、以及斜線分隔稱謂的手足。

Round 5a-5c:WordPress 的三重陷阱

程式碼邏輯修好了,但在 WordPress 上完全無法運行。原因有三:

陷阱一:wptexturize WordPress 的文字美化濾鏡會把反引號 ` 轉換成智慧引號,把 -- 轉換成 em dash。任何包含 ES6 模板字串的 JavaScript 都會被破壞。

解法:用老式字串串接取代所有模板字串,用 CSS 十六進位色碼取代 CSS 自訂屬性。

陷阱二:主題 CSS 汙染。 WordPress 主題普遍使用 * { box-sizing: border-box !important } 和各種全域字體覆寫。Shadow DOM 理論上應該隔離,但有些主題的 CSS 會影響 :host 元素。

解法:在 Shadow DOM 內部為所有元素加上明確的 CSS 重置。

陷阱三:&& 被轉譯成 &#038;&#038; WordPress 的 wp_ksesesc_html 濾鏡會把 HTML 中的 & 轉義成 HTML 實體。如果 JavaScript 程式碼中有 &&&=、甚至變數名包含 &,全部會被破壞。

這是最致命的一擊。解法是 Base64 編碼架構 把整段 JavaScript 編碼成 Base64 字串,放進一個 <script type="text/plain"> 標籤(瀏覽器不會執行),然後用一個極簡的 loader 解碼並注入:

(function(d){
  var p = d.getElementById('fc-payload');
  var raw = atob(p.textContent.trim());
  var bytes = new Uint8Array(raw.length);
  for(var i = 0; i < raw.length; i++)
    bytes[i] = raw.charCodeAt(i);
  var code = new TextDecoder('utf-8').decode(bytes);
  var s = d.createElement('script');
  s.text = code;
  d.head.appendChild(s);
})(document);

Loader 本身不包含任何 &"--、或反引號——WordPress 找不到任何東西可以破壞。Base64 字串只是一串 A-Za-z0-9+/= 字元,完全免疫於所有文字處理濾鏡。

WordPress 與 Vanilla JS 的共存之道:不是繞過框架,是繞過框架的副作用。

Round 6:婚姻禁止:台灣民法第 983 條

功能需求:當使用者計算出的親屬關係屬於法律禁止結婚的對象時,顯示警告。

台灣民法第 983 條規定三類禁婚親屬:

  • 第 Ⅰ 款:直系血親及直系姻親(父母、祖父母、公婆、岳父母……)
  • 第 Ⅱ 款:六親等內旁系血親(兄弟姊妹、堂表兄弟、姪子外甥……)
  • 第 Ⅲ 款:五親等內旁系姻親(但輩分相同者不在此限)

實作上使用正則模式匹配,31 個規則覆蓋 131 種稱謂,輸出結構為:

{ level: 'ban', degree: '二親等旁系血親', law: '§983Ⅱ', note: '' }

最棘手的是斜線分隔稱謂的處理。推導引擎經常產生「伯父/叔父」「堂哥/堂弟」這類泛稱,必須把兩半都送進判定。

Round 7:配偶引擎:從 6 條規則到 13 大類

使用者報告 母→姊→夫 應該是「姨丈」,但輸出「姨母」配偶按鈕被禁用了。

原來是婚姻禁止系統的防呆過頭,它禁用了所有的夫/妻按鈕,導致無法計算「姨母的丈夫」。

修正方向:按鈕禁用改為基於性別的邏輯防呆。男性親屬之後禁用「夫」(男性不能有丈夫),女性親屬之後禁用「妻」(女性不能有妻子),配偶之後禁用「夫」和「妻」(避免迴圈)。婚姻警告改為純資訊展示,不阻擋操作。

同時,getSpouseTitle 從 6 條規則擴充到 13 大類,涵蓋長輩配偶(姨丈、姑丈、伯母、嬸嬸)、祖輩旁系配偶(姑丈公、舅婆)、配偶手足的配偶(妯娌、連襟)、堂表配偶、以及孫輩配偶。

Round 8:baseMap 全面稽核與第四引擎

最後一輪是暴力測試,用程式窮舉所有 2-step(110 組)和 3-step(1,000 組)的按鈕組合,找出每一個 fallback 到「遠房親戚」的缺口。

發現 12 個 2-step 缺口,全部是「手足/子女 → 父/母」的反向追溯 deriveRelative 根本不處理 fm 步驟碼。

解法是新增第四引擎 getParentTitle,15 大類規則,涵蓋子女回溯(兒子→父 = 自己)、手足回溯(兄→父 = 父親)、姪甥回溯(姪子→父 = 兄弟)、孫輩回溯(孫→父 = 兒子)、以及姻親回溯(女婿→父 = 親家公)。

同時實作「自己」作為可鏈接的中繼結果——子→父 = 自己自己→兄 = 兄弟自己→妻 = 妻子,四引擎全部支援。

最終暴力測試結果:

深度組合數被禁用可達路徑有解解析率
2-step110228888100%
3-step1,00028871266893.8%

剩餘的 6.2% 全部是「跨入姻親原生家庭」(嫂嫂的父親、女婿的兄弟……),中文稱謂系統本身就不涵蓋這個範圍,fallback 到描述性文字是正確行為。


測試策略:三層防護

整個專案的品質保證分為三層:

  • 第一層:單元測試。 每座引擎各自獨立測試,用 Node.js 直接跑函式級驗證。getSpouseTitle 69 case、getChildTitle 71 case、getSiblingTitle 69 case。
  • 第二層:Playwright E2E。 在模擬的 WordPress 頁面中,透過 Playwright 操作 Shadow DOM 內的按鈕,驗證完整的點擊→計算→顯示流程。覆蓋配偶鏈、深層推導、禁用規則、婚姻警告等場景。
  • 第三層:暴力窮舉。 程式化遍歷所有 2-step 和 3-step 的按鈕排列組合,分類統計 blocked、resolved、fallback。不是「這幾個 case 過了就好」,而是「所有可能的組合都不能有意外」。

技術洞察:過程中學到的事

中文稱謂不是樹,是圖

最初的直覺是「親屬關係是一棵樹,往上走往下走就好」。錯。配偶關係會把兩棵無關的家族樹橋接起來,形成循環:子→父→妻→夫 = 自己→自己→自己→自己。

推導引擎必須能偵測迴圈(妻→夫 = 自己),並讓「自己」成為可繼續鏈接的有效節點。

WordPress 不是平台,是戰場

WordPress 的設計哲學是「幫你把內容變漂亮」。它有至少五層文字處理濾鏡:wptexturize(智慧引號)、wpautop(自動段落)、wp_kses(HTML 過濾)、esc_html(實體轉義)、以及主題自帶的 CSS 全域覆寫。

每一層都可能破壞內嵌的 JavaScript。Base64 編碼方案是唯一能同時免疫所有濾鏡的策略——因為它根本不讓 WordPress 看到任何可以處理的字元。

泛稱是魔鬼

推導引擎經常產生「伯父/叔父」「堂哥/堂弟」這類斜線分隔的泛稱,因為在不知道年齡序位的情況下,無法確定具體是哥還是弟。

每座引擎、婚姻判定系統、甚至 UI 顯示邏輯,都必須正確處理斜線分隔。這包括正則匹配(/伯父\/叔父/)、分割後雙重判定、以及保持泛稱在鏈路中的可推導性。

「不合理的組合」比想像中微妙

第一版的禁用邏輯是:如果結果是禁婚對象,就禁用配偶按鈕。看似合理,但副作用是把「查詢姨母的丈夫」也一起禁了——因為姨母是六親等內血親。

正確的禁用邏輯應該基於性別約束而非婚姻判定:男性不能有丈夫(性別矛盾),配偶不能再接配偶(邏輯迴圈)。這才是真正「不合理的組合」。


最終成果

902 行 Vanilla JavaScript,一個 Custom Element,零框架相依。

120 組 baseMap 靜態對照,覆蓋 1-3 步常見親屬。四座推導引擎共 453 條規則,覆蓋子女、配偶、手足、父母四個方向的任意長度鏈路。31 條婚姻禁止規則,依據台灣民法第 983 條三款判定 131 種親屬關係。Base64 編碼架構,免疫於所有 WordPress 文字濾鏡。Shadow DOM 完全隔離,不受主題 CSS 影響。

暴力測試 1,110 組按鈕組合,可達路徑解析率 93.8%,剩餘的 6.2% 是中文稱謂系統本身的邊界,不是程式的缺陷。

如果你問我這個專案最大的收穫是什麼?

框架解決的是工程規模問題。語言解決的是領域建模問題。 中文親屬稱謂的複雜度不在 UI 層、不在狀態管理層,而在「一個稱謂如何推導出下一個稱謂」的領域邏輯層。這一層,不管你用 React 還是 Vue 還是 Vanilla JS,都得一條一條規則去寫。

而 Vanilla JS 讓我能把全部注意力放在這裡,不用分心去想打包設定、元件生命週期、或是框架版本升級。

最後,下次過年被長輩考親戚稱呼的時候,我終於可以偷偷掏出手機了。