Files
xll-mini-program/scripts/subset-beginner-guide-fonts.mjs
王冕 a619938e0c Initial commit: 小羚羚小程序 Axhub Make workspace.
Include xll-miniapp prototype, PRD resources, annotation directory sync, agent skills, and cloud publishing setup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 15:32:30 +08:00

269 lines
7.0 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { TextDecoder } from 'node:util';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DEFAULT_APP_ROOT = path.resolve(__dirname, '..');
const SOURCE_FILES = [
'index.tsx',
'style.css',
'index.html',
'README.md',
];
const EXTRA_CODE_POINTS = [
0x00a0,
0x2013,
0x2014,
0x2018,
0x2019,
0x201c,
0x201d,
0x2022,
0x2026,
0x3000,
0x3001,
0x3002,
0x300a,
0x300b,
0x300c,
0x300d,
0xff01,
0xff08,
0xff09,
0xff0c,
0xff1a,
0xff1b,
0xff1f,
];
const ASCII_FALLBACK =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' +
' ~`!@#$%^&*()-_=+[]{}\\|;:\'",.<>/?';
function uniqueCharacters(input) {
const seen = new Set();
const output = [];
for (const character of input) {
if (!seen.has(character)) {
seen.add(character);
output.push(character);
}
}
return output;
}
export function parseSubsetArgs(argv = process.argv) {
const args = {
appRoot: DEFAULT_APP_ROOT,
dryRun: false,
sourceFontDir: process.env.AXHUB_BEGINNER_GUIDE_FONT_SOURCE_DIR || '',
};
const values = argv.slice(2);
for (let index = 0; index < values.length; index += 1) {
const value = values[index];
if (value === '--dry-run') {
args.dryRun = true;
continue;
}
if (value === '--app-root') {
args.appRoot = path.resolve(values[index + 1] || '');
index += 1;
continue;
}
if (value === '--source-font-dir') {
args.sourceFontDir = path.resolve(values[index + 1] || '');
index += 1;
continue;
}
if (value === '--help' || value === '-h') {
args.help = true;
continue;
}
throw new Error(`Unknown option: ${value}`);
}
return args;
}
export function resolveBeginnerGuidePaths(options = {}) {
const appRoot = path.resolve(options.appRoot || DEFAULT_APP_ROOT);
const prototypeRoot = path.join(appRoot, 'src/prototypes/beginner-guide');
const localSourceFontDir = path.join(appRoot, '.local/font-sources/beginner-guide');
const sourceFontDir = options.sourceFontDir
? path.resolve(options.sourceFontDir)
: fs.existsSync(localSourceFontDir)
? localSourceFontDir
: prototypeRoot;
return {
appRoot,
prototypeRoot,
sourceFontDir,
};
}
export function collectCharactersFromFiles(filePaths) {
const content = filePaths
.filter((filePath) => fs.existsSync(filePath))
.map((filePath) => fs.readFileSync(filePath, 'utf8'))
.join('\n');
return uniqueCharacters(content);
}
export function getGb2312LevelOneCharacters() {
const decoder = new TextDecoder('gb18030');
const characters = [];
for (let high = 0xb0; high <= 0xd7; high += 1) {
for (let low = 0xa1; low <= 0xfe; low += 1) {
characters.push(decoder.decode(Buffer.from([high, low])));
}
}
return uniqueCharacters(characters).join('');
}
export function collectSubsetCharacters(options = {}) {
const pageCharacters = Array.isArray(options.pageCharacters)
? options.pageCharacters
: [...String(options.pageCharacters || '')];
const commonCharacters = String(
options.commonCharacters ?? getGb2312LevelOneCharacters(),
);
const extraCharacters = String(
options.extraCharacters
?? `${ASCII_FALLBACK}${String.fromCodePoint(...EXTRA_CODE_POINTS)}`,
);
return uniqueCharacters(`${commonCharacters}${extraCharacters}${pageCharacters.join('')}`);
}
export function getFontJobs(paths) {
return [
{
weight: 400,
inputPath: path.join(paths.sourceFontDir, 'TsangerJinKai02-W04.ttf'),
outputPath: path.join(paths.prototypeRoot, 'TsangerJinKai02-W04.subset.woff2'),
},
{
weight: 500,
inputPath: path.join(paths.sourceFontDir, 'TsangerJinKai02-W05.ttf'),
outputPath: path.join(paths.prototypeRoot, 'TsangerJinKai02-W05.subset.woff2'),
},
];
}
function getSourceFilePaths(paths) {
return SOURCE_FILES.map((fileName) => path.join(paths.prototypeRoot, fileName));
}
function formatSize(byteLength) {
if (byteLength < 1024) return `${byteLength} B`;
if (byteLength < 1024 * 1024) return `${(byteLength / 1024).toFixed(1)} KB`;
return `${(byteLength / 1024 / 1024).toFixed(1)} MB`;
}
function printHelp() {
console.log(`Usage: node scripts/subset-beginner-guide-fonts.mjs [options]
Options:
--dry-run Print the plan without writing fonts.
--app-root <path> Client app root. Defaults to the current client.
--source-font-dir <path> Directory containing TsangerJinKai02-W04.ttf and W05.ttf.
Environment:
AXHUB_BEGINNER_GUIDE_FONT_SOURCE_DIR can provide the source font directory.
`);
}
export async function subsetBeginnerGuideFonts(options = {}) {
const paths = resolveBeginnerGuidePaths(options);
const sourceFilePaths = getSourceFilePaths(paths);
const pageCharacters = collectCharactersFromFiles(sourceFilePaths);
const subsetCharacters = collectSubsetCharacters({ pageCharacters });
const jobs = getFontJobs(paths);
if (options.dryRun) {
return {
paths,
jobs,
characterCount: subsetCharacters.length,
written: [],
};
}
const missingInputs = jobs
.map((job) => job.inputPath)
.filter((inputPath) => !fs.existsSync(inputPath));
if (missingInputs.length > 0) {
throw new Error(
[
'Missing source font file(s):',
...missingInputs.map((inputPath) => ` - ${inputPath}`),
'Pass --source-font-dir or set AXHUB_BEGINNER_GUIDE_FONT_SOURCE_DIR.',
].join('\n'),
);
}
const { default: subsetFont } = await import('subset-font');
fs.mkdirSync(paths.prototypeRoot, { recursive: true });
const written = [];
for (const job of jobs) {
const inputBuffer = fs.readFileSync(job.inputPath);
const outputBuffer = await subsetFont(inputBuffer, subsetCharacters.join(''), {
targetFormat: 'woff2',
preserveNameIds: [1, 2, 4, 6],
});
fs.writeFileSync(job.outputPath, outputBuffer);
written.push({
...job,
inputSize: inputBuffer.byteLength,
outputSize: outputBuffer.byteLength,
});
}
return {
paths,
jobs,
characterCount: subsetCharacters.length,
written,
};
}
async function runCli() {
const args = parseSubsetArgs(process.argv);
if (args.help) {
printHelp();
return;
}
const result = await subsetBeginnerGuideFonts(args);
console.log(`Beginner guide subset characters: ${result.characterCount}`);
for (const job of result.jobs) {
if (args.dryRun) {
console.log(`dry-run ${job.weight}: ${job.inputPath} -> ${job.outputPath}`);
continue;
}
const written = result.written.find((item) => item.weight === job.weight);
console.log(
`${job.weight}: ${formatSize(written.inputSize)} -> ${formatSize(written.outputSize)} ${written.outputPath}`,
);
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
runCli().catch((error) => {
console.error(error.message || error);
process.exit(1);
});
}