Repository navigation
Expand file tree
/
Copy pathforce-fonts-applegothic.user.js
More file actions
203 lines (186 loc) · 8.94 KB
/
Copy pathforce-fonts-applegothic.user.js
File metadata and controls
203 lines (186 loc) · 8.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// ==UserScript==
// @name 替換字體為 AppleGothic
// @namespace https://chris.taipei
// @version 0.4.17
// @description 使用 CSS 將一般頁面字體改為 AppleGothic,並保留常見 Icon Font 與程式碼字體
// @author chris1004tw
// @match *://*/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-start
// @updateURL https://github.com/chris1004tw/userscripts/raw/main/force-fonts-applegothic.user.js
// @downloadURL https://github.com/chris1004tw/userscripts/raw/main/force-fonts-applegothic.user.js
// ==/UserScript==
// Co-authored with Claude Opus 4.6 Thinking
// Co-authored with ChatGPT 5.6 Sol Ultra
// 維護索引:README.md「維護索引」
(function () {
'use strict';
const TARGET_FONT = 'AppleGothic, AppleGothicSC, "Malgun Gothic", "Apple Monochrome Emoji Ind", "SF Pro Icons", "SF Pro Text", sans-serif';
const CODE_FONT = '"Cascadia Code", "Cascadia Mono", Consolas, "SF Mono", "JetBrains Mono", AppleGothic, AppleGothicSC, monospace';
// CSS 無法直接以 computed font-family 判斷圖示字型;保留高召回率的圖示元素與已確認 class token。
// Trip.com 飯店頁以 smarticon、ic 與 hotel_desktop_trip- 承載 PUA 偽元素圖示。
const ICON_CLASS_TOKENS = Object.freeze([
'icon', 'ficon', 'iconfont', 'icomoon', 'fontawesome', 'material-icons', 'material-icons-extended',
'material-icons-outlined', 'glyphicon', 'ottd', 'smarticon', 'ic', 'hotel_desktop_trip-',
'google-material-icons', 'google-symbols',
'devsite-nav-toggle',
'material-symbols-outlined', 'material-symbols-rounded', 'material-symbols-sharp',
'octicon', 'feather', 'ionicon', 'themify', 'anticon', 'boxicon',
'global-iconfont', 'woo-font', 'x-tool', 'x-tool-tool-el', 'x-tool-img'
]);
const ICON_CLASS_PREFIXES = Object.freeze([
'fa-', 'fas-', 'far-', 'fal-', 'fad-', 'fab-', 'bi-', 'ri-', 'mdi-', 'mi-',
'oi-', 'ti-', 'si-', 'gi-', 'ai-', 'di-', 'fi-', 'hi-', 'pi-', 'vi-', 'wi-',
'ci-', 'bx-', 'bxs-', 'bxl-', 'pe-7s-'
]);
// 天貓以自訂字型把隨機漢字碼位映射為價格數字;必須保留其原始 font-family 才能正確解碼。
const PRESERVED_FONT_CLASS_TOKENS = Object.freeze(['c-price']);
/**
* 將需保留頁面原始字型的完整 class token 轉為不分大小寫的 CSS selector。
*
* @param {string} token 要排除全域字型覆寫的完整 class token。
* @returns {string} 使用 `~=` 比對單一 class token 的 selector;不會產生副作用。
*/
function createClassTokenSelector(token) {
return `[class~="${token}" i]`;
}
/**
* 將 Icon class prefix 轉為首個與後續 class token 的兩個 CSS selector。
*
* @param {string} prefix 要排除的 class token 前綴。
* @returns {string[]} 分別涵蓋 class 屬性開頭與空白後 token 的 selector;不會產生副作用。
*/
function createClassPrefixSelectors(prefix) {
return [`[class^="${prefix}" i]`, `[class*=" ${prefix}" i]`];
}
const ICON_SELECTORS = Object.freeze([
// 多數 Icon Font 使用 <i> 或在該元素的偽元素上繪製;保留整個元素以避免誤套一般字型。
'i',
'[data-icon]',
// Claude 的 Anthropicons glyph 使用 data-cds="Icon",不是 inline font-family。
'[data-cds="Icon"]',
'i[style*="font-family" i]',
'[style*="Anthropicons-Variable" i]',
// Google Workspace 說明中心的篩選器清除圖示沒有 material-icons class。
'.devsite-book-nav-filter .filter-clear-button',
// Gemini 搜尋元件內的裝飾圖示;不依賴生成 class 或介面語言。
'[data-search_modal-island-root] button[aria-haspopup="dialog"] > span[aria-hidden="true"]',
'[class^="icon-" i]',
'[class*=" icon-" i]',
'[class$="-icon" i]',
'[class*="-icon " i]',
'[class*="-icon-" i]',
...ICON_CLASS_TOKENS.map(createClassTokenSelector),
...ICON_CLASS_PREFIXES.flatMap(createClassPrefixSelectors)
]);
const PRESERVED_FONT_SELECTORS = Object.freeze([
// SVG 圖示可能以文字節點搭配頁面原始字型繪製;保留整棵圖形子樹。
'svg',
'svg *',
// xterm 的繪製與字格量測必須使用同一字型;保留整棵子樹,不另指定等寬字型。
'.xterm',
'.xterm *',
...ICON_SELECTORS,
...PRESERVED_FONT_CLASS_TOKENS.map(createClassTokenSelector),
// 天貓價格的自訂字型常掛在容器上;其子元素也必須沿用原始字型。
...PRESERVED_FONT_CLASS_TOKENS.map(token => `${createClassTokenSelector(token)} *`)
]);
const PRESERVED_FONT_NOT_SELECTOR = `:not(${PRESERVED_FONT_SELECTORS.join(', ')})`;
const NATIVE_CONTROL_NOT_SELECTOR = ':not(input[type="checkbox"]):not(input[type="radio"])';
const TEXT_SELECTOR = `:where(html body *${PRESERVED_FONT_NOT_SELECTOR}${NATIVE_CONTROL_NOT_SELECTOR})`;
/**
* 建立一般文字、程式碼及需保留頁面原始字型的排除規則。
*
* @returns {string} 可直接交給 GM_addStyle 的完整 CSS;不讀取或修改 DOM。
*/
function buildStyles() {
return `
@font-face { font-family: 'AppleGothic'; src: local('AppleGothic'); }
@font-face { font-family: 'AppleGothicSC'; src: local('AppleGothicSC'); }
/* AppleGothic 的 cmap 可能宣告卻缺少韓文字形,以相同 family 的 unicode-range 修正回退。 */
@font-face {
font-family: 'AppleGothic';
src: local('Malgun Gothic');
unicode-range: U+1100-11FF, U+3130-318F, U+A960-A97F, U+AC00-D7AF, U+D7B0-D7FF;
}
html,
body,
${TEXT_SELECTOR} {
font-family: ${TARGET_FONT} !important;
}
/* 放在一般規則之後,確保程式碼英數仍優先使用等寬字體。 */
/* GitHub 只使用程式碼語意 class;不得用會命中首頁版面 utility 的 class 子字串。 */
#read-only-cursor-text-area,
:where(.react-code-lines *),
:where(.blob-code *),
:where(pre *),
:where(code *),
.react-code-lines,
.react-code-text,
.react-file-line,
.react-line-number,
.blob-code,
.blob-num,
.highlight pre,
.highlight code,
code,
pre:has(code),
kbd,
samp,
tt {
font-family: ${CODE_FONT} !important;
}
`;
}
/**
* 註冊目前網站切換、查看及清空黑名單選單。
*
* @param {string} currentHost 目前頁面的 hostname。
* @param {boolean} isEnabled 目前網站是否啟用字體替換。
* @returns {void} 會註冊三個 Tampermonkey 選單;callback 可能更新儲存值並顯示提示。
*/
function registerMenuCommands(currentHost, isEnabled) {
GM_registerMenuCommand(isEnabled ? '停用此網站' : '啟用此網站', () => {
// 執行時重新讀取,避免其他分頁更新後沿用過期快照。
const latestBlacklist = new Set(GM_getValue('blacklist', []));
if (isEnabled) {
latestBlacklist.add(currentHost);
GM_setValue('blacklist', Array.from(latestBlacklist));
alert(`已將 ${currentHost} 加入黑名單,重新整理後生效`);
} else {
latestBlacklist.delete(currentHost);
GM_setValue('blacklist', Array.from(latestBlacklist));
alert(`已將 ${currentHost} 從黑名單移除,重新整理後生效`);
}
});
GM_registerMenuCommand('查看黑名單', () => {
const list = Array.from(new Set(GM_getValue('blacklist', [])));
alert(list.length ? `黑名單:\n${list.join('\n')}` : '黑名單是空的');
});
GM_registerMenuCommand('清空黑名單', () => {
if (!confirm('確定要清空黑名單嗎?')) return;
GM_setValue('blacklist', []);
alert('黑名單已清空,重新整理後生效');
});
}
/**
* 依目前 hostname 初始化選單與 CSS 字體規則。
*
* @returns {void} 主框架會註冊選單;未列入黑名單的所有框架會注入一份 CSS。
*/
function init() {
const currentHost = location.hostname;
const blacklist = new Set(GM_getValue('blacklist', []));
const isEnabled = !blacklist.has(currentHost);
if (window.self === window.top) {
registerMenuCommands(currentHost, isEnabled);
}
if (isEnabled) {
GM_addStyle(buildStyles());
}
}
init();
})();