Skip to content

Instantly share code, notes, and snippets.

@rishubil
Last active December 10, 2025 06:49
Show Gist options
  • Select an option

  • Save rishubil/65221c1da6e384e67ae9cb3739ae5f90 to your computer and use it in GitHub Desktop.

Select an option

Save rishubil/65221c1da6e384e67ae9cb3739ae5f90 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Custom Fallback Font
// @version 0.7
// @description Replace system fallback fonts with custom fonts
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// @updateURL https://gist.github.com/rishubil/65221c1da6e384e67ae9cb3739ae5f90/raw/custom-fallback-font.user.js
// @downloadURL https://gist.github.com/rishubil/65221c1da6e384e67ae9cb3739ae5f90/raw/custom-fallback-font.user.js
// ==/UserScript==
(function () {
"use strict";
// ===== 설정 영역 =====
const SHOW_TOAST = false; // 토스트 알림 표시 여부
const DEBUG_MODE = false; // 디버그 모드
const WEB_FONTS = `
@import url("https://cdn.jsdelivr.net/gh/orioncactus/[email protected]/dist/web/variable/pretendardvariable-gov.min.css");
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+KR:[email protected]&display=swap');
@font-face {
font-family: 'D2Coding';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/[email protected]/D2Coding.woff') format('woff');
font-weight: normal;
font-display: swap;
}
`;
const CUSTOM_FONTS = {
sansSerif: "Pretendard GOV Variable",
serif: "Noto Serif KR",
monospace: "D2Coding",
};
const EXCLUDED_SITES_KEY = "customFallbackFont_excludedSites";
// ====================
function getExcludedSites() {
const data = GM_getValue(EXCLUDED_SITES_KEY, null);
if (data === null) {
// 저장된 값이 없으면 기본 예외 사이트를 저장하고 반환
const defaultSites = ["*.github.com", "*.stackblitz.com"];
saveExcludedSites(defaultSites);
return defaultSites;
}
try {
return JSON.parse(data);
} catch (e) {
// 파싱 오류 시 기본값으로 복구
const defaultSites = ["*.github.com", "*.stackblitz.com"];
saveExcludedSites(defaultSites);
return defaultSites;
}
}
function saveExcludedSites(sites) {
GM_setValue(EXCLUDED_SITES_KEY, JSON.stringify(sites));
}
function getCurrentDomain() {
return window.location.hostname;
}
function isCurrentSiteExcluded() {
const excludedSites = getExcludedSites();
const currentDomain = getCurrentDomain();
return excludedSites.some((site) => {
if (site.includes("*")) {
const regex = new RegExp(
"^" + site.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
);
return regex.test(currentDomain);
}
return site === currentDomain;
});
}
function toggleCurrentSite() {
const excludedSites = getExcludedSites();
const currentDomain = getCurrentDomain();
const index = excludedSites.indexOf(currentDomain);
if (index === -1) {
excludedSites.push(currentDomain);
alert(`"${currentDomain}"이(가) 예외 목록에 추가되었습니다.\n페이지를 새로고침하세요.`);
} else {
excludedSites.splice(index, 1);
alert(`"${currentDomain}"이(가) 예외 목록에서 제거되었습니다.\n페이지를 새로고침하세요.`);
}
saveExcludedSites(excludedSites);
}
function manageExcludedSites() {
const excludedSites = getExcludedSites();
const newList = prompt(
"예외 사이트 목록 (쉼표로 구분, *를 와일드카드로 사용 가능):\n예: *.github.com, *.example.com",
excludedSites.join(", ")
);
if (newList !== null) {
const sites = newList
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
saveExcludedSites(sites);
alert("예외 사이트 목록이 업데이트되었습니다.\n페이지를 새로고침하세요.");
}
}
// 메뉴 명령어 등록 (항상 사용 가능)
GM_registerMenuCommand("현재 사이트 예외 추가/제거", toggleCurrentSite);
GM_registerMenuCommand("예외 사이트 관리", manageExcludedSites);
// 한국어 지원 폰트 캐시
const fontSupportCache = new Map();
let replacedFonts = new Set();
function showToast(message) {
const toastContainer =
document.getElementById("custom-font-toast-container") ||
(() => {
const container = document.createElement("div");
container.id = "custom-font-toast-container";
container.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 999999;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
`;
document.body.appendChild(container);
return container;
})();
const toast = document.createElement("div");
toast.textContent = message;
toast.style.cssText = `
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 20px;
border-radius: 8px;
font-size: 13px;
font-family: sans-serif;
opacity: 0;
transition: opacity 0.3s;
pointer-events: auto;
`;
toastContainer.appendChild(toast);
// 페이드 인
setTimeout(() => (toast.style.opacity = "1"), 10);
// 3초 후 페이드 아웃 및 제거
setTimeout(() => {
toast.style.opacity = "0";
setTimeout(() => {
toast.remove();
// 컨테이너가 비었으면 제거
if (toastContainer.children.length === 0) {
toastContainer.remove();
}
}, 300);
}, 3000);
}
function supportsKorean(fontName) {
if (fontSupportCache.has(fontName)) {
return fontSupportCache.get(fontName);
}
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const testChar = "한";
// fallback 폰트로 렌더링
context.font = "72px monospace";
const fallbackWidth = context.measureText(testChar).width;
// 테스트할 폰트로 렌더링
context.font = `72px "${fontName}", monospace`;
const testWidth = context.measureText(testChar).width;
// 너비가 다르면 해당 폰트가 한글을 지원
const supports = testWidth !== fallbackWidth;
fontSupportCache.set(fontName, supports);
return supports;
}
function changeFontFamily(element) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
const computedStyle = window.getComputedStyle(element);
const fontFamily = computedStyle.fontFamily
.toLowerCase()
.replace(/['"]/g, "");
// CSS generic family names + 시스템 기본 폰트
const systemFonts = [
"serif",
"sans-serif",
"monospace",
"cursive",
"fantasy",
"system-ui",
"ui-serif",
"ui-sans-serif",
"ui-monospace",
"ui-rounded",
"math",
"fangsong",
"-apple-system",
"blinkmacsystemfont",
"segoe ui",
"malgun gothic",
"dotum",
"arial",
];
const fonts = fontFamily.split(",").map((f) => f.trim());
// 이미 커스텀 폰트가 적용되어 있는지 확인
const customFontNames = Object.values(CUSTOM_FONTS).map((f) =>
f.toLowerCase()
);
if (fonts.some((font) => customFontNames.includes(font))) {
// 자식 요소만 처리
for (const child of element.children) {
changeFontFamily(child);
}
return;
}
const hasExplicitFontFamily =
element.style.fontFamily !== "" ||
!element.parentElement ||
getComputedStyle(element).getPropertyValue("font-family") !==
getComputedStyle(element.parentElement).getPropertyValue("font-family");
if (!hasExplicitFontFamily) {
// 자식 요소만 처리
for (const child of element.children) {
changeFontFamily(child);
}
return;
}
// 전체 fontFamily에서 generic family 찾기 (마지막 항목이 보통 generic family)
const genericFamily = fonts[fonts.length - 1];
let targetGenericFamily = "sans-serif"; // 기본값
if (systemFonts.includes(genericFamily)) {
targetGenericFamily = genericFamily;
}
// 각 폰트를 분류
let hasKoreanSupportFont = false;
let firstSystemFontIndex = -1;
let lastUnsupportedFontIndex = -1;
for (let i = 0; i < fonts.length; i++) {
const font = fonts[i];
if (systemFonts.includes(font)) {
if (firstSystemFontIndex === -1) {
firstSystemFontIndex = i;
}
} else if (supportsKorean(font)) {
hasKoreanSupportFont = true;
break; // 한글 지원 폰트가 있으면 더 이상 확인 불필요
} else {
// 한글 미지원 폰트 (마지막 인덱스 계속 갱신)
lastUnsupportedFontIndex = i;
}
}
// 한글 지원 폰트가 이미 있으면 대체하지 않음
if (hasKoreanSupportFont) {
// 자식 요소만 처리
for (const child of element.children) {
changeFontFamily(child);
}
return;
}
// 삽입할 커스텀 폰트 결정
let customFont = "";
const isMonospace =
targetGenericFamily === "monospace" ||
targetGenericFamily === "ui-monospace";
if (isMonospace) {
customFont = CUSTOM_FONTS.monospace;
} else if (
targetGenericFamily === "serif" ||
targetGenericFamily === "ui-serif"
) {
customFont = CUSTOM_FONTS.serif;
} else {
// sans-serif, ui-sans-serif, system-ui, cursive, fantasy, ui-rounded, math, fangsong 등
customFont = CUSTOM_FONTS.sansSerif;
}
// 삽입 위치 결정
let insertIndex = -1;
let triggerFont = "";
// 모노스페이스 폰트이고 첫 번째 폰트가 한글 미지원인 경우 제일 앞에 삽입
if (
isMonospace &&
fonts.length > 0 &&
!systemFonts.includes(fonts[0]) &&
!supportsKorean(fonts[0])
) {
insertIndex = 0;
triggerFont = fonts[0];
} else if (lastUnsupportedFontIndex !== -1 && firstSystemFontIndex !== -1) {
// 한글 미지원 폰트와 시스템 폰트 모두 있는 경우
// 마지막 한글 미지원 폰트 뒤, 첫 번째 시스템 폰트 앞 중 적절한 위치
if (lastUnsupportedFontIndex < firstSystemFontIndex) {
// 한글 미지원 폰트가 시스템 폰트보다 앞에 있음 → 마지막 한글 미지원 폰트 뒤에 삽입
insertIndex = lastUnsupportedFontIndex + 1;
triggerFont = fonts[lastUnsupportedFontIndex];
} else {
// 시스템 폰트가 한글 미지원 폰트보다 앞에 있음 → 첫 번째 시스템 폰트 앞에 삽입
insertIndex = firstSystemFontIndex;
triggerFont = fonts[firstSystemFontIndex];
}
} else if (lastUnsupportedFontIndex !== -1) {
// 한글 미지원 폰트만 있으면 마지막 한글 미지원 폰트 뒤에 삽입
insertIndex = lastUnsupportedFontIndex + 1;
triggerFont = fonts[lastUnsupportedFontIndex];
} else if (firstSystemFontIndex !== -1) {
// 시스템 폰트만 있으면 첫 번째 시스템 폰트 앞에 삽입
insertIndex = firstSystemFontIndex;
triggerFont = fonts[firstSystemFontIndex];
}
// 커스텀 폰트 삽입
if (insertIndex !== -1 && customFont) {
const newFonts = [...fonts];
newFonts.splice(insertIndex, 0, customFont.toLowerCase());
element.style.fontFamily = newFonts.join(", ");
if (DEBUG_MODE) {
element.style.background = "red";
}
// 토스트 표시
if (!replacedFonts.has(triggerFont)) {
replacedFonts.add(triggerFont);
if (SHOW_TOAST) {
showToast(`폰트 대체: ${triggerFont}${customFont} fallback 추가`);
}
}
}
// 자식 요소 처리
for (const child of element.children) {
changeFontFamily(child);
}
}
// 초기 페이지 로드
function init() {
// 웹 폰트 로드 대기 후 실행
document.fonts.ready.then(() => {
changeFontFamily(document.body);
});
}
if (!isCurrentSiteExcluded()) {
// 웹 폰트 로드
if (WEB_FONTS.trim()) {
const style = document.createElement("style");
style.textContent = WEB_FONTS;
(document.head || document.documentElement).appendChild(style);
}
// DOM 변경 감지
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
changeFontFamily(node);
}
}
}
});
// 스크립트 시작
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
// 웹 폰트 로드 후 observer 시작
document.fonts.ready.then(() => {
observer.observe(document.body || document.documentElement, {
childList: true,
subtree: true,
});
});
} else {
if (DEBUG_MODE) {
console.log(
"[Custom Fallback Font] 현재 사이트는 예외 목록에 있어 스크립트가 비활성화되었습니다."
);
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment