返回 DeepSeek-Reasonix
fontAvailability.ts
根目录 / desktop / frontend / src / lib / fontAvailability.ts
1 import { FONT_FAMILIES, MONO_FONT_FAMILIES, type FontFamily, type MonoFontFamily } from "./fontFamily";
2
3 export type TypographyPlatform = "windows" | "darwin" | "linux";
4 type FontProbe = (fontNames: readonly string[]) => boolean;
5
6 const UI_FONT_CANDIDATES: Record<Exclude<FontFamily, "system" | "custom">, { platforms: TypographyPlatform[]; fonts: string[] }> = {
7 yahei: {
8 platforms: ["windows"],
9 fonts: ["Microsoft YaHei UI", "Microsoft YaHei", "微软雅黑"],
10 },
11 pingfang: {
12 platforms: ["darwin"],
13 fonts: ["PingFang SC", "PingFang TC", "苹方-简"],
14 },
15 noto: {
16 platforms: ["linux"],
17 fonts: ["Noto Sans SC", "Noto Sans CJK SC", "Source Han Sans SC", "思源黑体"],
18 },
19 };
20
21 const MONO_FONT_CANDIDATES: Record<Exclude<MonoFontFamily, "system" | "custom">, { platforms: TypographyPlatform[]; fonts: string[] }> = {
22 cascadia: {
23 platforms: ["windows"],
24 fonts: ["Cascadia Code", "Cascadia Mono"],
25 },
26 jetbrains: {
27 platforms: [],
28 fonts: ["JetBrains Mono"],
29 },
30 sfmono: {
31 platforms: ["darwin"],
32 fonts: ["SF Mono", "SFMono-Regular"],
33 },
34 };
35
36 export function getTypographyPlatform(): TypographyPlatform {
37 if (typeof document !== "undefined") {
38 const attr = document.documentElement.getAttribute("data-platform");
39 if (attr === "windows" || attr === "darwin" || attr === "linux") return attr;
40 }
41 if (typeof navigator === "undefined") return "linux";
42 const marker = `${navigator.platform} ${navigator.userAgent}`;
43 if (/Win/i.test(marker)) return "windows";
44 if (/Mac/i.test(marker)) return "darwin";
45 return "linux";
46 }
47
48 export function getAvailableFontFamilies(
49 selected: FontFamily,
50 platform: TypographyPlatform = getTypographyPlatform(),
51 probe: FontProbe = isAnyLocalFontAvailable,
52 ): FontFamily[] {
53 return FONT_FAMILIES.filter((font) => {
54 if (font === "system" || font === "custom" || font === selected) return true;
55 const meta = UI_FONT_CANDIDATES[font];
56 return meta.platforms.includes(platform) || probe(meta.fonts);
57 });
58 }
59
60 export function getAvailableMonoFontFamilies(
61 selected: MonoFontFamily,
62 platform: TypographyPlatform = getTypographyPlatform(),
63 probe: FontProbe = isAnyLocalFontAvailable,
64 ): MonoFontFamily[] {
65 return MONO_FONT_FAMILIES.filter((font) => {
66 if (font === "system" || font === "custom" || font === selected) return true;
67 const meta = MONO_FONT_CANDIDATES[font];
68 return meta.platforms.includes(platform) || probe(meta.fonts);
69 });
70 }
71
72 function isAnyLocalFontAvailable(fontNames: readonly string[]): boolean {
73 if (typeof document === "undefined") return false;
74 return fontNames.some((font) => isLocalFontAvailable(font));
75 }
76
77 function isLocalFontAvailable(fontName: string): boolean {
78 const canvas = document.createElement("canvas");
79 const ctx = canvas.getContext("2d");
80 if (!ctx) return false;
81
82 const sample = "Reasonix 字体检测 0123456789";
83 const size = "72px";
84 const fallbacks = ["monospace", "serif", "sans-serif"] as const;
85 return fallbacks.some((fallback) => {
86 ctx.font = `${size} ${fallback}`;
87 const fallbackWidth = ctx.measureText(sample).width;
88 ctx.font = `${size} ${quoteFontName(fontName)}, ${fallback}`;
89 return Math.abs(ctx.measureText(sample).width - fallbackWidth) > 0.5;
90 });
91 }
92
93 function quoteFontName(name: string): string {
94 return `"${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
95 }
96
96 lines TYPESCRIPT