Initial commit: OneOS prototype workspace baseline.
Include weekly report and other prototype sources with project tooling config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
268
scripts/subset-beginner-guide-fonts.mjs
Normal file
268
scripts/subset-beginner-guide-fonts.mjs
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user