返回作品列表
開源套件 · dotnet tool NuGet 已上架

Cornhsu.XamlContrast

XAML 對比度靜態稽核 · dotnet tool

桌面端的對比度工具全是「執行期+手動+一次一個元素」——滑鼠停在元素上、點兩個像素,等於開 App、切深淺主題、一頁一頁用眼睛看。XamlContrast 反過來:不開 App,直接解析 XAML 原始碼,算出每段文字實際疊在什麼顏色上,深淺主題各算一次 WCAG 比值,低於 AA 就讓 CI 紅燈。

類型
.NET 全域工具(dotnet tool)
版本
v0.3.0 · MIT(0.x,介面 1.0 凍結)
發佈
NuGet · OIDC 零金鑰 Trusted Publishing
測試
61 條 · 雙實作數字互證
解析規則
13 條 · 每條都來自真實產品實查
真實驗證
四個已出貨 WPF 產品 · 250+ 處修畢
Terminal — Cornhsu.XamlContrast
$ dotnet tool install -g Cornhsu.XamlContrast

$ xamlcontrast src/MyApp        # 全專案掃描 → 報告 + exit code(CI 把關)
palette: auto-detected theme pair: DarkTheme.xaml + LightTheme.xaml (6 keys)
files 30 | text-on-background pairs 706
exempted 21 disabled-state pair(s)  # WCAG 1.4.3 豁免,但一樣計數回報

  ok 589    fail 3    warn 0    decorative 115

===== fail (3) =====
MainWindow.xaml:30  TextBlock       fg=White       bg={Surface}  dark=16.72:1  light= 1.00:1  [light-fails]
MainWindow.xaml:26  TextBlock       fg={DimText}   bg={Bg}       dark= 2.11:1  light= 1.60:1  [both-low]
Theme.xaml:6   Style[HoverBtn]/trigger  fg=#9A9A9A   bg={Surface}  dark= 5.92:1  light= 2.81:1  [light-fails]

exit 1: 3 pair(s) below threshold × 2/3 (0 warn)
專案概覽

別人幫你「看見」對比度不足,XamlContrast 幫你「擋住」它

桌面端的無障礙對比度檢查,工具其實不少——Accessibility Insights for Windows、Colour Contrast Analyser 都很成熟。但它們有一個共同形狀:執行期、手動、一次一個元素。要先把程式跑起來,把滑鼠停在那個元素上,或是用滴管去點兩個像素;要檢查深淺兩套主題,就把整套流程再走一遍。實務上的結果是——沒有人真的每次改動都做這件事

XamlContrast 換一個切入點:對比度的答案,其實在原始碼裡就算得出來。解析 XAML 樹,沿著父節點往上找出每段文字實際生效的背景Transparent 要穿透、半透明要合成、Opacity 沿樹累乘、ControlTemplate 自成子樹、Style 與觸發器要逐狀態解析),再套 WCAG 2.x 公式,深淺兩套主題各算一次。整個過程是純 XML 處理(詳見下方技術亮點)——不開 App、不需要 Windows,Linux CI 上也跑得動,一個指令掃完整個專案,低於 AA 就 exit 1,並在 PR 的那一行留下註記。

稽核工具最大的風險不是漏報,是謊報健康。

這句話是整個專案的憲法,也決定了它每一個設計:豁免、排除、壓抑、解析失敗全部計數並喊出來;解析不到的顏色標成 unresolved 而不是用猜的;連「一個配對都沒解析到」也是 exit 1——因為綠燈的意思必須是「我看過了而且沒問題」,不能是「我什麼都沒看到」。

問題長什麼樣

它守的是「你沒在看的那個主題」

你在深色模式下調了一個顏色,看起來很好。同一個色票在淺色主題底下,可能已經掉到 1:1——白字白底,完全看不見,而你不會發現,因為你今天沒切到淺色模式。每個配對都在兩套主題各算一次,對稱欄位會直接告訴你是哪一邊壞了。

MainWindow.xaml:30 · [light-fails] · 深色主題下的設計意圖沒有跨到另一套主題

成果一覽

v0.3.0,四個真實產品驗證過

發佈NuGet dotnet tool(v0.1.0 → v0.3.0),推 tag 即 OIDC Trusted Publishing 自動上架(repo 零長效金鑰);分析本身是純 XML,Linux CI 也能跑,不需要 Windows 或 WPF 執行期
真實驗證四個已出貨的 WPF 產品、250+ 處真實對比問題修畢——工具驅動修正,前後成績由工具自己稽核
雙實作互證PowerShell 原型(1,080 行,當規格)與 .NET 移植在四個真實專案上數字完全一致——含每筆 fail 的 file:line 與退出碼,verify-baselines.ps1 隨時可重跑
測試61 條,含 WCAG 標準值、13 條解析規則、色盤偵測四形狀、baseline/ignore,以及八個「看起來健康但錯」報告的回歸形狀
解析規則13 條,每一條都來自真實產品的實查(誤報或漏報),沒有一條是在白板上想出來的
零設定色盤偵測 4 種形狀:主題配對/C# 真相源/單一主題/無色盤退路(大聲警告)——四個受測專案全部零設定跑得起來
CI 整合GitHub Action(PR 行內註記)、SARIF → Security 分頁、baseline ratchet;自家 CI 用故意壞掉的 demo 專案預期 exit 1——跑出 0 就是守門員自己壞了
真實驗證

拿自己已經出貨的三個產品當白老鼠

不是「跑得動 demo」就宣稱有用。三個已經在用的 WPF 產品交給它掃,掃出來的問題一路修到底,前後成績由工具自己稽核。其中一個專案的 23 處真問題全部是具名 Style 的觸發態——hover、pressed 這些手動測試很難逐一走到、作者自己也不知道存在的狀態。

專案導入前導入後備註
CelFlow破 39 / 偏低 57021 組停用態依 WCAG 1.4.3 豁免(計數回報,不評分)
Kindling破 41 / 偏低 14真問題 0殘餘 3 筆全數對得上文件化的假警報類型
QuillNest破 13 / 偏低 31真問題 0殘餘 7 筆同上;一併被 baseline 吸收

順帶的收穫有兩個。其一,某個專案的寫死色碼從 572 個降到 0——寫死的顏色不跟主題走,在報告裡會整片亮起來,等於順手把色盤治理做完了。其二,一次真實修正中,直覺的方向(把紗罩調深)在數學上是錯的:淺色主題的前景本來就是深灰,紗罩要更淡才對。「這樣好讀嗎」可以吵一整天,「2.54:1,需要 4.5」是一個決定——數字讓推理可以被驗證

技術亮點

六個值得一提的設計

1

真正的難題不是 WCAG 公式

對比度公式只有 20 行,網路上到處都是;價值全部在「這段文字實際疊在什麼顏色上」——Transparent 要往上穿透、半透明 ARGB 要合成、Opacity 沿樹累乘(Border 0.5 裡的 TextBlock 0.5 = 0.25)、ControlTemplate 自成子樹、Style 的 Background/Foreground 兩個 Setter 要配對、觸發器逐狀態檢、具名 Style 跨檔索引 + BasedOn 鏈揉平 + 同條件觸發器合併、模板根背景、TargetName 指向模板根的 Setter(依 WPF 優先序蓋過 TemplateBinding 進來的宿主本地值)。抄公式的實作全都會漏掉這些。

2

「對稱」是獨立維度,不能當成「刻意」的證據

原型犯過最嚴重的錯,是把「深淺兩邊都低而且差不多」判成設計意圖並藏起來。實測後果:一個專案 52 組低於 AA 被吃掉;另一個單一主題專案(深淺同值,差距恆為 0)96 組全被吃掉,含 9pt 小字 1.75:1。改成兩個維度各答各的:等級(絕對對比:fail/warn/ok/裝飾)× 對稱(兩邊皆低=色票本身不夠、換主題救不了;深色壞/淺色壞=設計意圖沒跨主題保住)。兩邊一樣糟不是意圖,只是糟兩次。

3

零設定的色盤偵測

工具得自己找到色盤在哪:主題配對(DarkTheme.xaml + LightTheme.xaml 鍵重疊)→ C# 真相源("Key","#dark","#light") 元組陣列;關鍵規則是「提供兩套主題的來源,優先於只有一套的」)→ 單一主題 → 找不到就退回只算寫死色碼並大聲警告。四個受測專案剛好四種形狀,全部零設定。猜錯可用 xamlcontrast.config.json 覆寫,設定寫錯是 exit 2 加欄位級訊息——被靜默忽略的設定,等於你以為改了稽核標準但其實沒改。

4

退出碼堵住「空掃綠燈」

有 fail → exit 1;配對數為 0 也 → exit 1(一次重構讓所有配對變成無法解析,綠燈放行就是謊報健康);用法錯誤 → exit 2。--fail-on warn 給法遵場景(AA 全過才算過)、--strict-palette 讓「色盤沒偵測到」也能是紅燈。

5

導入模式:baseline ratchet

真實專案第一次跑就是破 39~55 組,第一天全紅的 gate 會被關掉,關掉的 gate 價值是 0。第一次跑凍結既有債並進 repo,之後只擋新增與惡化——債只准減不准增。鍵不含行號(行號會漂移)但記錄最差比值:色票值被調暗時鍵不變,少了比值欄位就會靜默放行。xamlcontrast-ignore 註解理由必填、壓掉的項目計入 summary.suppressed——ignore 不准變成新開的靜默通道。

6

退化必須機器可讀

JSON 分兩層:findings 之外一定有 summary,帶齊每一個退化計數器(paletteSourceunresolvedskippedsuppressedparseErrorsdisabledExempt)。CI 只消費 findings 的話,「工具有東西沒看到」這件事等於不存在。另有 --sarif 直接進 GitHub code scanning。解析不到的顏色(BindingTemplateBinding)一律標 unresolved,絕不推測——拿誠實的不確定去換好看的確定性,是這個專案唯一不能犯的錯。

工程方法

規畫書 → 先修工具 → 每條規則都來自實查 → 雙實作互證

M0

先修工具,再修專案

逐行審查原型,抓到四個報告層 bug(Style 配對被 $null -le 0 靜默全判成裝飾、「低於 AA」把裝飾算進去…)。修完重出基準線的那一刻,「已經修好」的專案裡浮出一整批被吃掉的真問題——這是整個專案的定調事件:先讓工具值得相信,再拿它去下判斷。

規則來源

每條解析規則都指得出實查來源

規則從 8 條長到 13 條,來源全部有名有姓:QuillNest CheckBox 的死 setter 誤報、CelFlow 觸發態與停用態、Kindling 模板根背景……第 12 條是在驗收工具自己驅動的修正時發現的。沒有白老鼠的平台支援只是沒驗證過的猜測——所以 WinUI 3/Avalonia 的擴充也明文卡在「要有真實專案」這個前置條件上。

移植驗收

兩套獨立實作互證

移植時刻意逐位元組複刻即將丟棄的原型輸出,而是用 C# 直接實作最終合約(英文+JSON),驗收改用語意比對。結果是這個工具最強的正確性證據:兩個不同語言的獨立實作,在四個真實專案上數字完全一致,連每筆 fail 的 file:line 與退出碼都對得上,而且隨時可重跑。

回歸

每次「看起來健康但錯」都變成測試

開發期間前後八次產出健康但錯誤的報告,每一次都固化成回歸測試。這類 bug 的可怕之處在於它不會讓你發現——報告漂漂亮亮,問題還在產品裡。

不做清單

「不做」也明文記錄

npm 通路暫緩(受眾是 XAML/.NET 開發者,人人都有 dotnetdotnet tool install 不構成採用門檻——受眾論證不成立就不付維護成本)、TargetName 指向模板內部元素不做、自動修不做(換哪個色票是設計決定)、執行期取樣不做(那是 Accessibility Insights 的地盤)、APCA/WCAG 3 不追草案。每條都有寫下來的理由。

設計取捨

取了什麼、捨了什麼、為什麼

決定理由
靜態解析原始碼,非執行期取樣不開 App、掃全專案、進得了 CI、Linux 也能跑圖片上的文字、執行期才決定的顏色執行期取樣是 Accessibility Insights 的地盤,重做一次沒有價值
對稱不得用來隱藏 finding報告可信報告數字好看「兩邊一樣糟」不是意圖的證據,只是糟兩次
解析不到就標 unresolved誠實的不確定覆蓋率數字猜錯的顏色會產生假警報,信任一旦掉了就回不來
0 配對 = exit 1堵住空掃綠燈「至少不會誤擋」的直覺綠燈的意思必須是「我看過了而且沒問題」
baseline ratchet舊專案今天就能導入「零 fail 才給過」的純粹第一天全紅的 gate 會被關掉,關掉的 gate 價值是 0
ignore 理由必填+計數壓抑行為留下痕跡一行註解就靜音的方便否則 ignore 就是自己新開的一條靜默退化通道
技術棧總覽

從引擎到上架的具體組成

套件Cornhsu.XamlContrast(dotnet tool,指令 xamlcontrast) · MIT · .NET 10
架構Core/CLI 分離:解析引擎(XAML 樹走訪 · Style 跨檔索引 · 色盤偵測 · WCAG 計算 · 分級)+薄 CLI 外殼;純 XML 處理,不依賴 WPF 執行期
解析規則13 條:透明穿透 · alpha 合成 · Opacity 累乘 · ControlTemplate 子樹 · Style setter 配對 · 觸發器狀態 · 死 setter 過濾 · BasedOn 鏈揉平 · 逐狀態觸發器合併 · 停用態豁免 · 半透明色票 · 模板根背景 · 模板根 TargetName
色盤偵測主題配對 · C# 真相源 · 單一主題 · 無色盤退路(皆可用 xamlcontrast.config.json 覆寫)
輸出Console · JSON(兩層,帶 schemaVersion 與全部退化計數器)· SARIF 2.1.0(GitHub code scanning)· 退出碼合約
品質61 條測試 · 四個真實產品驗證 · 兩套獨立實作數字互證 · CI 用故意壞掉的 demo 專案守門(預期 exit 1)
發佈OIDC Trusted Publishing(無長效金鑰) · 推 tag 即自動測試、打包、上架 NuGet · GitHub Action(PR 行內註記)