mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-21 22:11:42 +10:00
Finding 4 — consumer-count verification: @reactive-resume/utils/date → 1 consumer (packages/import/src/json-resume.tsx) @reactive-resume/utils/html → 1 consumer (packages/import/src/json-resume.tsx) @reactive-resume/utils/level → 1 consumer (packages/import/src/json-resume.tsx) @reactive-resume/utils/url → 4 consumers (auth, api/ai, import, server) — SKIPPED, stays in utils @reactive-resume/utils/field → 1 consumer (packages/fonts/src/index.ts) Move date/html/level source + test files into packages/import and update json-resume.tsx imports. Inline the two-line unique() helper into fonts/src/index.ts and drop the ./field subpath. Drop the three moved subpaths from packages/utils exports. Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
200 lines
7.1 KiB
TypeScript
200 lines
7.1 KiB
TypeScript
import type { Locale, Script } from "@reactive-resume/utils/locale";
|
|
import { getLocaleScript, isCjkScript } from "@reactive-resume/utils/locale";
|
|
|
|
// ponytail: inlined from @reactive-resume/utils/field (sole consumer)
|
|
const unique = <T>(items: T[]): T[] => [...new Set(items)];
|
|
|
|
import webFontListJSON from "./webfontlist.json";
|
|
|
|
type FontCategory = "display" | "handwriting" | "monospace" | "serif" | "sans-serif";
|
|
export type FontWeight = "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900";
|
|
type FontFileWeight = FontWeight | `${FontWeight}italic`;
|
|
|
|
type StandardFont = {
|
|
type: "standard";
|
|
category: FontCategory;
|
|
family: string;
|
|
weights: FontWeight[];
|
|
};
|
|
|
|
export type WebFont = {
|
|
type: "web";
|
|
category: FontCategory;
|
|
family: string;
|
|
weights: FontWeight[];
|
|
preview: string;
|
|
files: Record<FontFileWeight, string>;
|
|
};
|
|
|
|
type FontRecord = StandardFont | WebFont;
|
|
|
|
const preferredChineseFontFamilies = [
|
|
"Noto Sans SC",
|
|
"Noto Serif SC",
|
|
"PingFang SC",
|
|
"Microsoft YaHei",
|
|
"Source Han Sans SC",
|
|
"Source Han Serif SC",
|
|
"Songti SC",
|
|
"SimSun",
|
|
"SimHei",
|
|
"KaiTi",
|
|
"FangSong",
|
|
"ZCOOL QingKe HuangYou",
|
|
] as const;
|
|
|
|
const standardPdfFontList = [
|
|
{ type: "standard", category: "sans-serif", family: "Helvetica", weights: ["400", "700"] },
|
|
{ type: "standard", category: "monospace", family: "Courier", weights: ["400", "700"] },
|
|
{ type: "standard", category: "serif", family: "Times-Roman", weights: ["400", "700"] },
|
|
] satisfies StandardFont[];
|
|
|
|
const fontDisplayNames: Partial<Record<string, string>> = {
|
|
FangSong: "仿宋",
|
|
"Hiragino Sans GB": "冬青黑体简体中文",
|
|
KaiTi: "楷体",
|
|
"Microsoft YaHei": "微软雅黑",
|
|
"Noto Sans SC": "思源黑体",
|
|
"Noto Sans TC": "思源黑体(繁中)",
|
|
"Noto Serif SC": "思源宋体",
|
|
"Noto Serif TC": "思源宋体(繁中)",
|
|
"PingFang SC": "苹方",
|
|
SimHei: "黑体",
|
|
SimSun: "宋体",
|
|
"Songti SC": "华文宋体",
|
|
"Source Han Sans SC": "思源黑体(本地)",
|
|
"Source Han Serif SC": "思源宋体(本地)",
|
|
"ZCOOL QingKe HuangYou": "站酷庆科黄油体",
|
|
};
|
|
|
|
// Per-script Noto web font, split by serif/sans category. These match the
|
|
// actual writing system: Hangul lives only in the KR fonts, Kana only in JP,
|
|
// Arabic glyphs only in the Arabic fonts, etc. — so a Latin or Simplified-
|
|
// Chinese font cannot render them and produces tofu. Where Noto ships no serif
|
|
// variant for a script (Hebrew, Thai), the serif slot reuses the sans font so
|
|
// serif resumes still render real glyphs instead of nothing. All entries are
|
|
// present in webfontlist.json.
|
|
const scriptFonts: Record<Script, { serif: string; sansSerif: string }> = {
|
|
hangul: { serif: "Noto Serif KR", sansSerif: "Noto Sans KR" },
|
|
kana: { serif: "Noto Serif JP", sansSerif: "Noto Sans JP" },
|
|
"han-traditional": { serif: "Noto Serif TC", sansSerif: "Noto Sans TC" },
|
|
"han-simplified": { serif: "Noto Serif SC", sansSerif: "Noto Sans SC" },
|
|
arabic: { serif: "Noto Naskh Arabic", sansSerif: "Noto Sans Arabic" },
|
|
hebrew: { serif: "Noto Sans Hebrew", sansSerif: "Noto Sans Hebrew" },
|
|
thai: { serif: "Noto Sans Thai", sansSerif: "Noto Sans Thai" },
|
|
};
|
|
|
|
export const webFontList = webFontListJSON as WebFont[];
|
|
export const webFontMap = new Map<string, WebFont>(webFontList.map((font) => [font.family, font]));
|
|
export const standardFontList = standardPdfFontList.filter((font) => !webFontMap.has(font.family));
|
|
|
|
const fontMap = new Map<string, FontRecord>();
|
|
const chinesePrioritySet = new Set<string>(preferredChineseFontFamilies);
|
|
|
|
function orderFonts(fonts: FontRecord[]) {
|
|
return [...fonts].sort((a, b) => {
|
|
return a.family.localeCompare(b.family, undefined, { sensitivity: "base" });
|
|
});
|
|
}
|
|
|
|
export const fontList = orderFonts([...standardFontList, ...webFontList]);
|
|
|
|
for (const font of fontList) {
|
|
fontMap.set(font.family, font);
|
|
}
|
|
|
|
// Compatibility aliases for fonts that v5.0.x resolved via the browser
|
|
// (Arial, Times New Roman, ...) but that aren't registered with
|
|
// @react-pdf/renderer in v5.1+. Targets are metric-compatible web fonts
|
|
// already shipped in webfontlist (#2989).
|
|
const legacyFontAliases: Record<string, string> = {
|
|
Arial: "Arimo",
|
|
Cambria: "Tinos",
|
|
Calibri: "Carlito",
|
|
Garamond: "EB Garamond",
|
|
"Times New Roman": "Times-Roman",
|
|
};
|
|
|
|
export function resolveLegacyFontAlias(family: string): string | null {
|
|
return legacyFontAliases[family] ?? null;
|
|
}
|
|
|
|
export function getFont(family: string) {
|
|
const direct = fontMap.get(family);
|
|
if (direct) return direct;
|
|
|
|
const alias = legacyFontAliases[family];
|
|
return alias ? fontMap.get(alias) : undefined;
|
|
}
|
|
|
|
function getFontCategory(family: string): FontCategory | null {
|
|
return getFont(family)?.category ?? null;
|
|
}
|
|
|
|
export function getFontDisplayName(family: string) {
|
|
return fontDisplayNames[family] ?? family;
|
|
}
|
|
|
|
export function getFontSearchKeywords(family: string) {
|
|
return unique(
|
|
[family, fontDisplayNames[family], chinesePrioritySet.has(family) ? "中文" : undefined].filter(
|
|
(keyword): keyword is string => Boolean(keyword),
|
|
),
|
|
);
|
|
}
|
|
|
|
function getScriptFont(script: Script, category: FontCategory | null) {
|
|
const variants = scriptFonts[script];
|
|
return category === "serif" ? variants.serif : variants.sansSerif;
|
|
}
|
|
|
|
export function isStandardPdfFontFamily(family: string) {
|
|
return standardFontList.some((font) => font.family === family);
|
|
}
|
|
|
|
export function getWebFont(family: string) {
|
|
return webFontMap.get(family);
|
|
}
|
|
|
|
export function getWebFontSource(family: string, weight: FontWeight = "400", italic = false) {
|
|
const webFont = getWebFont(family);
|
|
if (!webFont) return null;
|
|
|
|
const key = `${weight}${italic ? "italic" : ""}` as FontFileWeight;
|
|
return webFont.files[key] ?? (italic ? webFont.files[weight] : undefined) ?? webFont.preview;
|
|
}
|
|
|
|
export function sortFontWeights<T extends string>(fontWeights: T[]): T[] {
|
|
return [...fontWeights].sort((a, b) => Number(a) - Number(b));
|
|
}
|
|
|
|
/**
|
|
* Returns an ordered stack of Noto web fonts to register as glyph-level
|
|
* fallbacks for PDF rendering. react-pdf resolves the font per-codepoint
|
|
* left-to-right across the stack, so listing one font per writing system lets
|
|
* a single resume mix Latin with Hangul, Kana, Han, Arabic, Hebrew or Thai.
|
|
*
|
|
* Ordering: the locale's primary script first (the dominant language), then
|
|
* any other scripts actually detected in the content. When the stack contains
|
|
* a CJK script, a Simplified Chinese entry is appended as a safety net for
|
|
* stray CJK-Unified ideographs (preserving prior behavior); non-CJK scripts
|
|
* get no such net. The result is deduped, has the primary family removed, and
|
|
* only keeps fonts that exist in the webfontlist.
|
|
*/
|
|
export function getPdfFallbackFontFamilies(
|
|
family: string,
|
|
options: { locale?: Locale; scripts?: Iterable<Script> } = {},
|
|
): string[] {
|
|
const category = getFontCategory(family);
|
|
|
|
const ordered: Script[] = [];
|
|
const localeScript = getLocaleScript(options.locale);
|
|
if (localeScript) ordered.push(localeScript);
|
|
if (options.scripts) ordered.push(...options.scripts);
|
|
if (ordered.some(isCjkScript)) ordered.push("han-simplified");
|
|
|
|
return unique(ordered.map((script) => getScriptFont(script, category)))
|
|
.filter((candidate) => candidate !== family)
|
|
.filter((candidate) => Boolean(getWebFont(candidate)));
|
|
}
|