This commit is contained in:
2026-07-27 00:56:18 +03:30
commit 1c75afe0fd
244 changed files with 27710 additions and 0 deletions
@@ -0,0 +1,38 @@
{
"name": "@ohmycv/google-fonts-loader",
"version": "0.1.1",
"description": "Load Goolge Fonts with ease.",
"keywords": [
"google fonts",
"fonts"
],
"homepage": "https://github.com/Renovamen/oh-my-cv/tree/main/packages/gfonts-loader",
"bugs": {
"url": "https://github.com/Renovamen/oh-my-cv"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Renovamen/oh-my-cv.git",
"directory": "packages/gfonts-loader"
},
"license": "MIT",
"author": "Renovamen <renovamenzxh@gmail.com>",
"files": [
"dist"
],
"exports": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"scripts": {
"build-fast:pkg": "tsup src/index.ts --format cjs,esm",
"build:pkg": "pnpm run build-fast:pkg --dts"
},
"publishConfig": {
"access": "public"
}
}
@@ -0,0 +1,117 @@
import { fetchFontList, loadFontStylesheet } from "./utils";
import type { Font, FontMap, Options } from "./types";
export class GoogleFontsLoader {
private readonly apiKey: string;
private readonly options: Options;
private activeFontFamily: string; // Name of currently applied font
private onChange: (font: Font) => void;
private fontMap: FontMap = new Map<string, Font>(); // Map from font families to font objects
constructor(
apiKey: string,
{
families = [],
categories = [],
subsets = ["latin"],
variants = ["regular"],
filter = () => true,
limit = -1,
sort = "alphabet"
}: Options = {},
onChange: (font: Font) => void = (): void => {}
) {
this.apiKey = apiKey;
this.options = {
families,
categories,
subsets,
variants,
filter,
limit,
sort
};
this.onChange = onChange;
this.activeFontFamily = "";
}
public async init(): Promise<FontMap> {
// Load all fonts
const fonts = await fetchFontList(this.apiKey);
// Function to check if a font satisfies the filter criteria
const isFontIncluded = (font: Font) =>
// Only keep fonts whose names are included in the provided array
(this.options.families!.length === 0 ||
this.options.families!.includes(font.family)) &&
// Only keep fonts in categories from the provided array
(this.options.categories!.length === 0 ||
this.options.categories!.includes(font.category)) &&
// Only keep fonts which are available in all specified subsets
this.options.subsets!.every((subset) => font.subsets.includes(subset)) &&
// Only keep fonts which contain all specified variants
this.options.variants!.every((variant) => font.variants.includes(variant)) &&
// Only keep fonts for which the `filter` function evaluates to `true`
this.options.filter!(font) === true;
// Save desired fonts in the font map
for (const font of fonts) {
// Exit once specified limit of number of fonts is reached
if (this.options.limit! >= 0 && this.fontMap.size >= this.options.limit!) break;
// Only keep fonts that satisfy the filter criteria
if (isFontIncluded(font)) this.fontMap.set(font.family, font);
}
// Sort the font map if required
if (this.options.sort === "alphabet") {
this.fontMap = new Map(
[...this.fontMap.entries()].sort(([family1], [family2]) =>
family1.localeCompare(family2)
)
);
}
return this.fontMap;
}
public getFontMap(): FontMap {
return this.fontMap;
}
public getActiveFont(): Font {
const activeFont = this.fontMap.get(this.activeFontFamily);
if (!activeFont) {
throw Error(
`Cannot get active font: "${this.activeFontFamily}" is not in the font list`
);
} else {
return activeFont;
}
}
/**
* Set the specified font as the active font and download it
*/
public async setActiveFont(fontFamily: string) {
const activeFont = this.fontMap.get(fontFamily);
if (!activeFont) {
// Font is not in fontList: Keep current activeFont and log error
throw Error(`Cannot update active font: "${fontFamily}" is not in the font list`);
}
this.activeFontFamily = fontFamily;
await loadFontStylesheet(activeFont, this.options.subsets!, this.options.variants!);
this.onChange(activeFont);
}
public setOnChange(onChange: (font: Font) => void) {
this.onChange = onChange;
}
}
export * from "./types";
export default GoogleFontsLoader;
@@ -0,0 +1,76 @@
export type Category = "sans-serif" | "serif" | "display" | "handwriting" | "monospace";
export type Subset =
| "arabic"
| "bengali"
| "chinese-simplified"
| "chinese-traditional"
| "cyrillic"
| "cyrillic-ext"
| "devanagari"
| "greek"
| "greek-ext"
| "gujarati"
| "gurmukhi"
| "hebrew"
| "japanese"
| "kannada"
| "khmer"
| "korean"
| "latin"
| "latin-ext"
| "malayalam"
| "myanmar"
| "oriya"
| "sinhala"
| "tamil"
| "telugu"
| "thai"
| "vietnamese";
export type Variant =
| "100"
| "100italic"
| "200"
| "200italic"
| "300"
| "300italic"
| "regular"
| "italic"
| "500"
| "500italic"
| "600"
| "600italic"
| "700"
| "700italic"
| "800"
| "800italic"
| "900"
| "900italic";
export type Font = {
family: string;
id: string;
category: Category;
subsets: Subset[];
variants: Variant[];
kind?: string; // Usually "webfonts#webfont"
version?: string; // Version number
lastModified?: string; // Date of last modification (yyyy-MM-dd)
files?: Record<Variant, string>; // Font file for each variant
};
export type FontMap = Map<string, Font>;
export type SortOption = "alphabet" | "popularity";
export type Options = {
families?: string[];
categories?: Category[];
subsets?: Subset[];
variants?: Variant[];
filter?: (font: Font) => boolean;
limit?: number;
sort?: SortOption;
};
@@ -0,0 +1,76 @@
import type { Font, Variant, Subset } from "../types";
import { hasStylesheet, createStylesheet } from "./stylesheets";
const GOOGLE_FONTS_API = "https://www.googleapis.com/webfonts/v1/webfonts";
const GOOGLE_FONTS_CSS = "https://fonts.googleapis.com/css";
const get = (url: string): Promise<string> =>
new Promise((resolve, reject): void => {
const request = new XMLHttpRequest();
request.overrideMimeType("application/json");
request.open("GET", url, true);
request.onreadystatechange = () => {
if (request.readyState === 4) {
if (request.status !== 200) {
reject(new Error(`Response has status code ${request.status}`));
} else {
resolve(request.responseText);
}
}
};
request.send();
});
const getFontStylesheet = async (
fonts: Font[],
subsets: Subset[],
variants: Variant[]
): Promise<string> => {
const url = new URL(GOOGLE_FONTS_CSS);
const variantsStr = variants.join(",");
const familiesStr = fonts.map((font): string => `${font.family}:${variantsStr}`);
url.searchParams.append("family", familiesStr.join("|"));
url.searchParams.append("subset", subsets.join(","));
// Tell browser to render fallback font immediately and swap in the new font once it's loaded
url.searchParams.append("font-display", "swap");
return get(url.href);
};
const getFontId = (fontFamily: string) => fontFamily.replace(/\s+/g, "-").toLowerCase();
export const fetchFontList = async (apiKey: string): Promise<Font[]> => {
// Request a list of all available Google Fonts, sorted by popularity
const url = new URL(GOOGLE_FONTS_API);
url.searchParams.append("sort", "popularity");
url.searchParams.append("key", apiKey);
const response = await get(url.href);
const fonts: Font[] = JSON.parse(response).items;
// Generate an ID for each font
return fonts.map((font) => ({
...font,
id: getFontId(font.family)
}));
};
/**
* Add a stylesheet for the given font to the document head
*/
export const loadFontStylesheet = async (
font: Font,
subsets: Subset[],
variants: Variant[]
) => {
// Load font stylesheet if it hasn't been loaded yet
if (!hasStylesheet(font.id)) {
const fontStyle = await getFontStylesheet([font], subsets, variants);
createStylesheet(font.id, fontStyle);
}
};
@@ -0,0 +1 @@
export * from "./fonts";
@@ -0,0 +1,13 @@
const _stylesheetId = (fontId: string) => `font-${fontId}`;
export const hasStylesheet = (fontId: string) =>
document.getElementById(_stylesheetId(fontId)) !== null;
export const createStylesheet = (fontId: string, styles: string) => {
const stylesheet = document.createElement("style");
stylesheet.id = _stylesheetId(fontId);
stylesheet.textContent = styles;
document.head.appendChild(stylesheet);
};
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"include": ["./src"]
}