|
| 1 | +/** |
| 2 | + * EmDash Noto Sans font provider |
| 3 | + * |
| 4 | + * A custom Astro font provider that wraps Google Fonts to resolve |
| 5 | + * multiple Noto Sans families (Latin, Arabic, JP, etc.) under a |
| 6 | + * single logical font entry. This lets all @font-face blocks share |
| 7 | + * the same font-family name, so the browser picks the right file |
| 8 | + * per character via unicode-range. |
| 9 | + * |
| 10 | + * Without this, registering "Noto Sans" and "Noto Sans Arabic" as |
| 11 | + * separate font entries on the same cssVariable triggers an Astro |
| 12 | + * warning and the last entry overwrites the first. |
| 13 | + */ |
| 14 | + |
| 15 | +import { fontProviders } from "astro/config"; |
| 16 | + |
| 17 | +/** |
| 18 | + * All subset names used by Google Fonts CSS responses. |
| 19 | + * Passed when resolving extra script families so the unifont |
| 20 | + * provider doesn't filter out any faces. |
| 21 | + */ |
| 22 | +const ALL_GOOGLE_SUBSETS = [ |
| 23 | + "arabic", |
| 24 | + "armenian", |
| 25 | + "bengali", |
| 26 | + "chinese-simplified", |
| 27 | + "chinese-traditional", |
| 28 | + "chinese-hongkong", |
| 29 | + "cyrillic", |
| 30 | + "cyrillic-ext", |
| 31 | + "devanagari", |
| 32 | + "ethiopic", |
| 33 | + "georgian", |
| 34 | + "greek", |
| 35 | + "greek-ext", |
| 36 | + "gujarati", |
| 37 | + "gurmukhi", |
| 38 | + "hebrew", |
| 39 | + "japanese", |
| 40 | + "kannada", |
| 41 | + "khmer", |
| 42 | + "korean", |
| 43 | + "lao", |
| 44 | + "latin", |
| 45 | + "latin-ext", |
| 46 | + "malayalam", |
| 47 | + "math", |
| 48 | + "myanmar", |
| 49 | + "oriya", |
| 50 | + "sinhala", |
| 51 | + "symbols", |
| 52 | + "tamil", |
| 53 | + "telugu", |
| 54 | + "thai", |
| 55 | + "tibetan", |
| 56 | + "vietnamese", |
| 57 | +]; |
| 58 | + |
| 59 | +/** |
| 60 | + * Known Noto Sans script families on Google Fonts. |
| 61 | + * Maps user-friendly script names to Google Fonts family names. |
| 62 | + */ |
| 63 | +const NOTO_SCRIPT_FAMILIES: Record<string, string> = { |
| 64 | + arabic: "Noto Sans Arabic", |
| 65 | + armenian: "Noto Sans Armenian", |
| 66 | + bengali: "Noto Sans Bengali", |
| 67 | + "chinese-simplified": "Noto Sans SC", |
| 68 | + "chinese-traditional": "Noto Sans TC", |
| 69 | + "chinese-hongkong": "Noto Sans HK", |
| 70 | + devanagari: "Noto Sans Devanagari", |
| 71 | + ethiopic: "Noto Sans Ethiopic", |
| 72 | + georgian: "Noto Sans Georgian", |
| 73 | + gujarati: "Noto Sans Gujarati", |
| 74 | + gurmukhi: "Noto Sans Gurmukhi", |
| 75 | + hebrew: "Noto Sans Hebrew", |
| 76 | + japanese: "Noto Sans JP", |
| 77 | + kannada: "Noto Sans Kannada", |
| 78 | + khmer: "Noto Sans Khmer", |
| 79 | + korean: "Noto Sans KR", |
| 80 | + lao: "Noto Sans Lao", |
| 81 | + malayalam: "Noto Sans Malayalam", |
| 82 | + myanmar: "Noto Sans Myanmar", |
| 83 | + oriya: "Noto Sans Oriya", |
| 84 | + sinhala: "Noto Sans Sinhala", |
| 85 | + tamil: "Noto Sans Tamil", |
| 86 | + telugu: "Noto Sans Telugu", |
| 87 | + thai: "Noto Sans Thai", |
| 88 | + tibetan: "Noto Sans Tibetan", |
| 89 | +}; |
| 90 | + |
| 91 | +export interface NotoSansProviderOptions { |
| 92 | + /** |
| 93 | + * Additional Noto Sans script families to include. |
| 94 | + * Use script names like "arabic", "japanese", "chinese-simplified". |
| 95 | + * |
| 96 | + * @see {@link NOTO_SCRIPT_FAMILIES} for the full list of supported scripts. |
| 97 | + */ |
| 98 | + scripts?: string[]; |
| 99 | +} |
| 100 | + |
| 101 | +// Use ReturnType to get the provider type without importing it directly. |
| 102 | +// The Astro FontProvider type is not part of the public API surface. |
| 103 | +type GoogleProvider = ReturnType<typeof fontProviders.google>; |
| 104 | + |
| 105 | +/** |
| 106 | + * Create a font provider that resolves Noto Sans plus additional |
| 107 | + * script-specific Noto families from Google Fonts, all under one |
| 108 | + * font-family name. |
| 109 | + */ |
| 110 | +export function notoSans(options?: NotoSansProviderOptions): GoogleProvider { |
| 111 | + // Create a single Google provider instance to share initialization |
| 112 | + const googleProvider = fontProviders.google(); |
| 113 | + |
| 114 | + return { |
| 115 | + name: "emdash-noto", |
| 116 | + async init(context) { |
| 117 | + await googleProvider.init?.(context); |
| 118 | + }, |
| 119 | + async resolveFont(resolveFontOptions) { |
| 120 | + // Resolve the base Noto Sans (Latin, Cyrillic, Greek, etc.) |
| 121 | + const base = await googleProvider.resolveFont(resolveFontOptions); |
| 122 | + const baseFonts = base?.fonts ?? []; |
| 123 | + |
| 124 | + if (!options?.scripts?.length) { |
| 125 | + return base; |
| 126 | + } |
| 127 | + |
| 128 | + // Collect subset names already covered by the base font so we |
| 129 | + // can filter out duplicate faces from extra script families. |
| 130 | + // e.g. Noto Sans Arabic includes latin/latin-ext faces that |
| 131 | + // would otherwise override the base Noto Sans latin faces. |
| 132 | + const baseSubsets = new Set(baseFonts.map((f) => f.meta?.subset).filter(Boolean)); |
| 133 | + |
| 134 | + // Resolve additional script families |
| 135 | + const extraFonts = await Promise.all( |
| 136 | + options.scripts.map(async (script) => { |
| 137 | + const family = NOTO_SCRIPT_FAMILIES[script]; |
| 138 | + if (!family) { |
| 139 | + // Silently skip subset names that are already covered |
| 140 | + // by the base Noto Sans font (latin, cyrillic, etc.) |
| 141 | + if (ALL_GOOGLE_SUBSETS.includes(script)) { |
| 142 | + return undefined; |
| 143 | + } |
| 144 | + console.warn( |
| 145 | + `[emdash] Unknown Noto Sans script "${script}". ` + |
| 146 | + `Available: ${Object.keys(NOTO_SCRIPT_FAMILIES).join(", ")}`, |
| 147 | + ); |
| 148 | + return undefined; |
| 149 | + } |
| 150 | + return googleProvider.resolveFont({ |
| 151 | + ...resolveFontOptions, |
| 152 | + familyName: family, |
| 153 | + // Pass all known subset names so the unifont provider |
| 154 | + // doesn't filter out any faces. Each script family |
| 155 | + // only returns faces for its own subsets anyway. |
| 156 | + subsets: ALL_GOOGLE_SUBSETS, |
| 157 | + }); |
| 158 | + }), |
| 159 | + ); |
| 160 | + |
| 161 | + // Merge, dropping faces from extra fonts that duplicate base subsets |
| 162 | + const extraFaces = extraFonts.flatMap((r) => |
| 163 | + (r?.fonts ?? []).filter((f) => !f.meta?.subset || !baseSubsets.has(f.meta.subset)), |
| 164 | + ); |
| 165 | + |
| 166 | + return { |
| 167 | + fonts: [...baseFonts, ...extraFaces], |
| 168 | + }; |
| 169 | + }, |
| 170 | + }; |
| 171 | +} |
| 172 | + |
| 173 | +/** Get the list of available Noto Sans script names */ |
| 174 | +export function getAvailableNotoScripts(): string[] { |
| 175 | + return Object.keys(NOTO_SCRIPT_FAMILIES); |
| 176 | +} |
0 commit comments