隱藏模式原理:為什麼寬度計算如此重要
學完你能做什麼
- 理解 OpenCode 隱藏模式的工作原理
- 知道為什麼普通格式化工具在隱藏模式下會對齊錯位
- 掌握外掛的寬度計算演算法(三步走)
- 了解
Bun.stringWidth的作用
你現在的困境
你用 OpenCode 寫程式碼,AI 生成了一個漂亮的表格:
| 欄位 | 類型 | 說明 |
|--- | --- | ---|
| **name** | string | 使用者名稱 |
| age | number | 年齡 |在原始碼視圖裡看著挺整齊。但切到預覽模式,表格卻錯位了:
| 欄位 | 類型 | 說明 |
|--- | --- | ---|
| name | string | 使用者名稱 | ← 怎麼短了?
| age | number | 年齡 |問題出在哪?隱藏模式。
什麼是隱藏模式
OpenCode 預設開啟隱藏模式(Concealment Mode),它會在渲染時隱藏 Markdown 語法符號:
| 原始碼 | 隱藏模式下顯示 |
|---|---|
**粗體** | 粗體(4 個字元) |
*斜體* | 斜體(4 個字元) |
~~刪除線~~ | 刪除線(6 個字元) |
`程式碼` | 程式碼(4 個字元 + 背景色) |
隱藏模式的好處
讓你專注於內容本身,而不是被一堆 **、* 符號干擾視線。
為什麼普通格式化工具會出問題
普通的表格格式化工具計算寬度時,會把 **name** 當作 8 個字元:
** n a m e ** = 8 字元但在隱藏模式下,使用者看到的是 name,只有 4 個字元。
結果就是:格式化工具按 8 字元對齊,使用者看到的卻是 4 字元,表格自然就錯位了。
核心思路:計算"顯示寬度"而非"字元長度"
這個外掛的核心思路是:計算使用者實際看到的寬度,而不是原始碼的字元數。
演算法分三步:
第 1 步:保護程式碼區塊(程式碼區塊裡的符號不剝離)
第 2 步:剝離 Markdown 符號(**、*、~~ 等)
第 3 步:用 Bun.stringWidth 計算最終寬度跟我做:理解三步演算法
第 1 步:保護程式碼區塊
為什麼
行內程式碼(用反引號包裹)裡的 Markdown 符號是"字面量",使用者會看到 **bold** 這 8 個字元,而不是 bold 這 4 個字元。
所以在剝離 Markdown 符號之前,要先把程式碼區塊內容"藏起來"。
原始碼實作
// 第 1 步:提取並保護行內程式碼
const codeBlocks: string[] = []
let textWithPlaceholders = text.replace(/`(.+?)`/g, (match, content) => {
codeBlocks.push(content)
return `\x00CODE${codeBlocks.length - 1}\x00`
})工作原理
| 輸入 | 處理後 | codeBlocks 陣列 |
|---|---|---|
`**bold**` | \x00CODE0\x00 | ["**bold**"] |
`a` and `b` | \x00CODE0\x00 and \x00CODE1\x00 | ["a", "b"] |
用 \x00CODE0\x00 這種特殊佔位符替換程式碼區塊,後面剝離 Markdown 符號時就不會誤傷它們。
第 2 步:剝離 Markdown 符號
為什麼
隱藏模式下,**粗體** 顯示為 粗體,*斜體* 顯示為 斜體。計算寬度時要把這些符號去掉。
原始碼實作
// 第 2 步:剝離非程式碼部分的 Markdown 符號
let visualText = textWithPlaceholders
let previousText = ""
while (visualText !== previousText) {
previousText = visualText
visualText = visualText
.replace(/\*\*\*(.+?)\*\*\*/g, "$1") // ***粗斜體*** → 文字
.replace(/\*\*(.+?)\*\*/g, "$1") // **粗體** → 粗體
.replace(/\*(.+?)\*/g, "$1") // *斜體* → 斜體
.replace(/~~(.+?)~~/g, "$1") // ~~刪除線~~ → 刪除線
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1") //  → alt
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)") // [text](url) → text (url)
}為什麼用 while 迴圈?
處理巢狀語法。比如 ***粗斜體***:
第 1 輪:***粗斜體*** → **粗斜體**(剝離最外層 ***)
第 2 輪:**粗斜體** → *粗斜體*(剝離 **)
第 3 輪:*粗斜體* → 粗斜體(剝離 *)
第 4 輪:粗斜體 = 粗斜體(無變化,退出迴圈)圖片和連結的處理
- 圖片
:OpenCode 只顯示 alt 文字,所以替換為alt - 連結
[text](url):顯示為text (url),保留 URL 資訊
第 3 步:恢復程式碼區塊 + 計算寬度
為什麼
程式碼區塊內容要放回去,然後用 Bun.stringWidth 計算最終的顯示寬度。
原始碼實作
// 第 3 步:恢復程式碼區塊內容
visualText = visualText.replace(/\x00CODE(\d+)\x00/g, (match, index) => {
return codeBlocks[parseInt(index)]
})
return Bun.stringWidth(visualText)為什麼用 Bun.stringWidth?
Bun.stringWidth 能正確計算:
| 字元類型 | 範例 | 字元數 | 顯示寬度 |
|---|---|---|---|
| ASCII | abc | 3 | 3 |
| 中文 | 你好 | 2 | 4(每個佔 2 格) |
| Emoji | 😀 | 1 | 2(佔 2 格) |
| 零寬字元 | a\u200Bb | 3 | 2(零寬字元不佔位) |
普通的 text.length 只能數字元個數,無法處理這些特殊情況。
完整範例
假設儲存格內容是:**`code`** and *text*
第 1 步:保護程式碼區塊
輸入:**`code`** and *text*
輸出:**\x00CODE0\x00** and *text*
codeBlocks = ["code"]第 2 步:剝離 Markdown 符號
第 1 輪:**\x00CODE0\x00** and *text* → \x00CODE0\x00 and text
第 2 輪:無變化,退出第 3 步:恢復程式碼區塊 + 計算寬度
恢復後:code and text
寬度:Bun.stringWidth("code and text") = 13最終,外掛按 13 字元的寬度來對齊這個儲存格,而不是原始碼的 22 字元。
檢查點
完成本課後,你應該能回答:
- [ ] 隱藏模式會隱藏哪些符號?(答:
**、*、~~等 Markdown 語法符號) - [ ] 為什麼要先保護程式碼區塊?(答:程式碼區塊裡的符號是字面量,不應被剝離)
- [ ] 為什麼用 while 迴圈剝離符號?(答:處理巢狀語法,如
***粗斜體***) - [ ]
Bun.stringWidth比text.length好在哪?(答:能正確計算中文、Emoji、零寬字元的顯示寬度)
踩坑提醒
常見誤解
誤解:程式碼區塊裡的 ** 也會被剝離
事實:不會。外掛會先用佔位符保護程式碼區塊內容,剝離完其他部分的符號後再恢復。
所以 `**bold**` 的寬度是 8(**bold**),不是 4(bold)。
本課小結
| 步驟 | 作用 | 關鍵程式碼 |
|---|---|---|
| 保護程式碼區塊 | 防止程式碼區塊內的符號被誤剝離 | text.replace(/\(.+?)`/g, ...)` |
| 剝離 Markdown | 計算隱藏模式下的實際顯示內容 | 多輪正則替換 |
| 計算寬度 | 處理中文、Emoji 等特殊字元 | Bun.stringWidth() |
下一課預告
下一課我們學習 表格規範。
你會學到:
- 什麼樣的表格能被格式化
- 表格驗證的 4 條規則
- 如何避免"無效表格"錯誤