| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- #!/usr/bin/env node
- /**
- * 一次性下载字体到 reports/assets/fonts/,让报告完全离线可用
- *
- * 来源:jsdelivr + @fontsource npm 包(字体的 CDN 包装,稳定)
- * - Inter: 英文正文 + 标题(400/700/800/900)
- * - Noto Sans SC: 中文正文 + 标题(400/500/700/900)— chinese-simplified subset
- * - JetBrains Mono: 代码字体(400/700)
- *
- * 所有字体都是 woff2 格式,浏览器原生支持,体积最小。
- * 下载后生成 fonts.css,HTML 用相对路径 `./assets/fonts/fonts.css` 引用。
- */
- const fs = require('fs');
- const path = require('path');
- const https = require('https');
- const FONTS_DIR = path.join('reports', 'assets', 'fonts');
- fs.mkdirSync(FONTS_DIR, { recursive: true });
- // ============================================================
- // 字体清单(family + weight + subset)
- // ============================================================
- const FONTS = [
- // Inter 英文
- { family: 'Inter', pkg: '@fontsource/inter', weight: 400, subset: 'latin', localName: 'inter-400.woff2' },
- { family: 'Inter', pkg: '@fontsource/inter', weight: 500, subset: 'latin', localName: 'inter-500.woff2' },
- { family: 'Inter', pkg: '@fontsource/inter', weight: 600, subset: 'latin', localName: 'inter-600.woff2' },
- { family: 'Inter', pkg: '@fontsource/inter', weight: 700, subset: 'latin', localName: 'inter-700.woff2' },
- { family: 'Inter', pkg: '@fontsource/inter', weight: 800, subset: 'latin', localName: 'inter-800.woff2' },
- { family: 'Inter', pkg: '@fontsource/inter', weight: 900, subset: 'latin', localName: 'inter-900.woff2' },
- // Noto Sans SC 中文(核心字重:400/500/700/900)
- // chinese-simplified subset 包含 ~7000 常用汉字
- { family: 'Noto Sans SC', pkg: '@fontsource/noto-sans-sc', weight: 400, subset: 'chinese-simplified', localName: 'noto-sc-400.woff2' },
- { family: 'Noto Sans SC', pkg: '@fontsource/noto-sans-sc', weight: 500, subset: 'chinese-simplified', localName: 'noto-sc-500.woff2' },
- { family: 'Noto Sans SC', pkg: '@fontsource/noto-sans-sc', weight: 700, subset: 'chinese-simplified', localName: 'noto-sc-700.woff2' },
- { family: 'Noto Sans SC', pkg: '@fontsource/noto-sans-sc', weight: 900, subset: 'chinese-simplified', localName: 'noto-sc-900.woff2' },
- // latin fallback for numbers/English in Chinese font family
- { family: 'Noto Sans SC', pkg: '@fontsource/noto-sans-sc', weight: 400, subset: 'latin', localName: 'noto-sc-latin-400.woff2' },
- { family: 'Noto Sans SC', pkg: '@fontsource/noto-sans-sc', weight: 700, subset: 'latin', localName: 'noto-sc-latin-700.woff2' },
- { family: 'Noto Sans SC', pkg: '@fontsource/noto-sans-sc', weight: 900, subset: 'latin', localName: 'noto-sc-latin-900.woff2' },
- // JetBrains Mono 代码
- { family: 'JetBrains Mono', pkg: '@fontsource/jetbrains-mono', weight: 400, subset: 'latin', localName: 'jetbrains-400.woff2' },
- { family: 'JetBrains Mono', pkg: '@fontsource/jetbrains-mono', weight: 500, subset: 'latin', localName: 'jetbrains-500.woff2' },
- { family: 'JetBrains Mono', pkg: '@fontsource/jetbrains-mono', weight: 700, subset: 'latin', localName: 'jetbrains-700.woff2' },
- ];
- function buildUrl(font) {
- // jsdelivr 对 @fontsource npm 包的路径格式:
- // https://cdn.jsdelivr.net/npm/@fontsource/<name>/files/<name>-<subset>-<weight>-normal.woff2
- const pkgName = font.pkg.replace('@fontsource/', '');
- return `https://cdn.jsdelivr.net/npm/${font.pkg}/files/${pkgName}-${font.subset}-${font.weight}-normal.woff2`;
- }
- function download(url, dest) {
- return new Promise((resolve, reject) => {
- const doDownload = (u, redirects = 0) => {
- if (redirects > 3) return reject(new Error('too many redirects'));
- https.get(u, (res) => {
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
- res.resume();
- return doDownload(res.headers.location, redirects + 1);
- }
- if (res.statusCode !== 200) {
- res.resume();
- return reject(new Error(`HTTP ${res.statusCode} for ${u}`));
- }
- const chunks = [];
- res.on('data', (c) => chunks.push(c));
- res.on('end', () => {
- fs.writeFileSync(dest, Buffer.concat(chunks));
- resolve(Buffer.concat(chunks).length);
- });
- res.on('error', reject);
- }).on('error', reject);
- };
- doDownload(url);
- });
- }
- async function main() {
- console.log(`╔═══ 字体离线化:下载 ${FONTS.length} 个 woff2 → ${FONTS_DIR} ═══╗\n`);
- const results = [];
- for (const font of FONTS) {
- const url = buildUrl(font);
- const dest = path.join(FONTS_DIR, font.localName);
- if (fs.existsSync(dest) && fs.statSync(dest).size > 1000) {
- const kb = (fs.statSync(dest).size / 1024).toFixed(0);
- console.log(` ⏭ ${font.localName.padEnd(26)} 已存在 ${kb}KB`);
- results.push({ ...font, size: fs.statSync(dest).size, ok: true });
- continue;
- }
- try {
- const size = await download(url, dest);
- const kb = (size / 1024).toFixed(0);
- console.log(` ✅ ${font.localName.padEnd(26)} ${kb}KB`);
- results.push({ ...font, size, ok: true });
- } catch (e) {
- console.log(` ❌ ${font.localName.padEnd(26)} ${e.message}`);
- results.push({ ...font, ok: false, err: e.message });
- }
- }
- // ============================================================
- // 生成 fonts.css
- // ============================================================
- const okFonts = results.filter((r) => r.ok);
- const cssParts = [
- '/* 本地离线字体 · 由 scripts/tools/setup-fonts.js 自动生成 */',
- '/* 相对路径引用:HTML 文件应放在 reports/ 下,字体在 reports/assets/fonts/ */',
- '',
- ];
- // Inter 的 @font-face(英文基本拉丁)
- const interLatinRange = 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD';
- for (const f of okFonts.filter((x) => x.family === 'Inter')) {
- cssParts.push(`@font-face {
- font-family: 'Inter';
- font-style: normal;
- font-weight: ${f.weight};
- font-display: swap;
- src: url('./${f.localName}') format('woff2');
- unicode-range: ${interLatinRange};
- }`);
- }
- // Noto Sans SC 的 @font-face(中文简体 + 西文拉丁各自 unicode-range)
- const nsLatinRange = 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD';
- // 中文 + 中日韩符号常用范围
- const nsCnRange = 'U+4E00-9FFF, U+3000-303F, U+FF00-FFEF, U+2E80-2EFF, U+31C0-31EF, U+3200-33FF, U+3400-4DBF';
- for (const f of okFonts.filter((x) => x.family === 'Noto Sans SC')) {
- const range = f.subset === 'latin' ? nsLatinRange : nsCnRange;
- cssParts.push(`@font-face {
- font-family: 'Noto Sans SC';
- font-style: normal;
- font-weight: ${f.weight};
- font-display: swap;
- src: url('./${f.localName}') format('woff2');
- unicode-range: ${range};
- }`);
- }
- // JetBrains Mono
- for (const f of okFonts.filter((x) => x.family === 'JetBrains Mono')) {
- cssParts.push(`@font-face {
- font-family: 'JetBrains Mono';
- font-style: normal;
- font-weight: ${f.weight};
- font-display: swap;
- src: url('./${f.localName}') format('woff2');
- unicode-range: ${interLatinRange};
- }`);
- }
- const cssPath = path.join(FONTS_DIR, 'fonts.css');
- fs.writeFileSync(cssPath, cssParts.join('\n\n') + '\n', 'utf-8');
- const totalKB = (okFonts.reduce((s, f) => s + f.size, 0) / 1024).toFixed(0);
- console.log(`\n✅ fonts.css → ${cssPath}`);
- console.log(` 字体总量: ${okFonts.length} 个 woff2 · ${totalKB} KB`);
- const failed = results.filter((r) => !r.ok);
- if (failed.length) {
- console.log(`\n⚠ 失败 ${failed.length} 个:`);
- failed.forEach((f) => console.log(` · ${f.localName}: ${f.err}`));
- }
- console.log(`\n╚═══ 完成。现在可在 HTML 里用 <link href="./assets/fonts/fonts.css"> 引用 ═══╝`);
- }
- main().catch((e) => { console.error(e); process.exit(1); });
|