73 lines
3.4 KiB
JavaScript
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('&', '&'), { 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 }
|
|
}
|