ontoref-outreach/scripts/decks/lib/fetch-fonts.mjs
2026-07-17 01:00:32 +01:00

73 lines
3.4 KiB
JavaScript

// Self-host the deck's webfonts.
//
// The Slidev build links a cross-origin Google Fonts stylesheet, so document.styleSheets
// cannot read it and the capture drops its @font-face rules with no error. On macOS
// nothing looks wrong — Avenir Next is a system font — and everywhere else the deck
// silently falls back to whatever the OS happens to have.
//
// Two facts, both measured:
// · Google serves only Nunito Sans and Victor Mono for that URL. It IGNORES
// `Avenir+Next` — a proprietary Linotype face, absent from Google Fonts and not
// licensable for self-hosting. It stays in the stack as a local preference; the
// stack's next entry is what we now guarantee for everyone else. (Decided 2026-07-14:
// Mac readers see Avenir Next, everyone else Nunito Sans — a conscious split.)
// · The User-Agent decides the format. A generic UA gets .ttf; a modern Chrome UA gets
// .woff2, roughly a third of the bytes.
import { writeFile, mkdir } from 'node:fs/promises'
import { readFile } from 'node:fs/promises'
import { basename, join } from 'node:path'
import { postcss, publicBase, publicDir } from './paths.mjs'
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
+ '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
// latin + latin-ext. Greek, cyrillic and vietnamese are subsets an es/en deck never
// types. Select on the range's own start codepoint — the file hashes carry no meaning,
// and guessing from them is how a whole family gets deleted by accident.
const LATIN = /^U\+0000-00FF/i
const LATIN_EXT = /^U\+0100-02BA/i
const wantedRange = (r) => LATIN.test(r.trim()) || LATIN_EXT.test(r.trim())
export async function fetchFonts(id, { apply = true, dist } = {}) {
const index = await readFile(join(dist, 'index.html'), 'utf8')
const href = index.match(/<link[^>]+href="(https:\/\/fonts\.googleapis\.com[^"]+)"/)?.[1]
if (!href) return { skipped: 'el build no enlaza Google Fonts', css: '', files: [] }
const res = await fetch(href.replaceAll('&amp;', '&'), { headers: { 'User-Agent': UA } })
if (!res.ok) throw new Error(`Google Fonts: HTTP ${res.status}`)
let css = await res.text()
const requested = [...href.matchAll(/family=([^:&]+)/g)].map(m => decodeURIComponent(m[1]).replace(/\+/g, ' '))
const served = [...new Set([...css.matchAll(/font-family:\s*'([^']+)'/g)].map(m => m[1]))]
const notServed = requested.filter(f => !served.includes(f))
const base = `${publicBase(id)}/fonts`
const dir = join(publicDir(id), 'fonts')
if (apply) await mkdir(dir, { recursive: true })
const root = postcss.parse(css)
const keep = []
root.walkAtRules('font-face', at => {
const src = at.toString()
const range = src.match(/unicode-range:\s*([^;]+)/i)?.[1]
if (range && !wantedRange(range)) { at.remove(); return }
const url = src.match(/url\((https:\/\/fonts\.gstatic\.com[^)]+)\)/)?.[1]
if (url) keep.push(url)
})
const files = []
for (const url of [...new Set(keep)]) {
const name = basename(new URL(url).pathname)
if (apply) {
const r = await fetch(url, { headers: { 'User-Agent': UA } })
if (!r.ok) throw new Error(`fuente ${url}: HTTP ${r.status}`)
await writeFile(join(dir, name), Buffer.from(await r.arrayBuffer()))
}
files.push(name)
}
let out = root.toString()
for (const url of [...new Set(keep)]) out = out.replaceAll(url, `${base}/${basename(new URL(url).pathname)}`)
return { css: out, files, served, notServed }
}