@xingwangzhe/stalux 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/index.ts +1 -1
- package/src/internal/font-subset-dev.ts +21 -18
- package/src/internal/font-subset.ts +81 -88
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A powerful, modern Astro blog theme
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "A powerful, modern Astro blog theme — use as template or install as plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
7
7
|
"astro-integration",
|
|
@@ -83,15 +83,15 @@
|
|
|
83
83
|
"@xingwangzhe/satteri-photoswipe": "^0.2.0",
|
|
84
84
|
"@xingwangzhe/tags-cloud": "^1.2.4",
|
|
85
85
|
"animejs": "^4.5.0",
|
|
86
|
-
"astro": "^7.1.3",
|
|
87
86
|
"astro-expressive-code": "^0.44.1",
|
|
88
87
|
"astro-seo": "^1.1.0",
|
|
89
88
|
"astro-typewriter": "^1.2.0",
|
|
90
89
|
"badge-maker": "^6.0.0",
|
|
91
90
|
"pagefind": "^1.5.2",
|
|
92
91
|
"photoswipe": "^5.4.4",
|
|
92
|
+
"satteri": "^0.9.5",
|
|
93
93
|
"simple-icons": "^16.27.1",
|
|
94
|
-
"
|
|
94
|
+
"subset-font": "^2.5.0",
|
|
95
95
|
"temml": "^0.13.3"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* 基于 Astro 7.1.3 Integration API(astro:config:setup / injectRoute / injectScript)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { resolve } from "node:path";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
15
15
|
|
|
16
16
|
import type { AstroIntegration } from "astro";
|
|
17
17
|
// pagefind 是 ESM-only 包,需要在模块顶层导入
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dev mode on-demand font subsetting middleware.
|
|
3
3
|
*
|
|
4
|
-
* Intercepts /fonts/subset-*.css requests, generates the
|
|
4
|
+
* Intercepts /fonts/subset-*.css requests, generates the TTF subset
|
|
5
5
|
* for the requested route on-the-fly, and serves it.
|
|
6
6
|
*
|
|
7
7
|
* Handles ALL HTML routes: posts, about, words, home, archives, tags, etc.
|
|
@@ -12,7 +12,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
import type { AstroIntegrationLogger } from "astro";
|
|
15
|
-
import
|
|
15
|
+
import subsetFont from "subset-font";
|
|
16
16
|
|
|
17
17
|
const FONT_INPUT = "src/assets/fonts/LXGWWenKai-Regular.ttf";
|
|
18
18
|
const CONTENT_ROOT = "stalux";
|
|
@@ -98,14 +98,6 @@ async function generateSubsetOnDemand(
|
|
|
98
98
|
for (const ch of chars) charSet.add(ch);
|
|
99
99
|
if (charSet.size === 0) return null;
|
|
100
100
|
|
|
101
|
-
// Deduplicate against common subset
|
|
102
|
-
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
103
|
-
|
|
104
|
-
// We can't precisely know which chars common covers without re-extracting.
|
|
105
|
-
// But we can read the i18n + config source for a rough estimate.
|
|
106
|
-
// For now, generate the full subset - the WOFF2 will be small either way.
|
|
107
|
-
// (Most chars are already in common, so the delta is tiny.)
|
|
108
|
-
|
|
109
101
|
// Locate font file
|
|
110
102
|
const fontPaths = [
|
|
111
103
|
resolve(projectRoot, FONT_INPUT),
|
|
@@ -122,23 +114,23 @@ async function generateSubsetOnDemand(
|
|
|
122
114
|
if (!fontPath) return null;
|
|
123
115
|
|
|
124
116
|
const fontBuffer = readFileSync(fontPath);
|
|
117
|
+
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
125
118
|
mkdirSync(outDir, { recursive: true });
|
|
126
119
|
|
|
127
|
-
// Generate subset TTF using
|
|
120
|
+
// Generate subset TTF using subset-font (WASM)
|
|
128
121
|
const charsStr = [...charSet].sort().join("");
|
|
129
|
-
|
|
130
|
-
const glyphs = get_glyph_ids(charsStr, "stalux", "normal", 400);
|
|
131
|
-
if (glyphs.length === 0) return null;
|
|
132
|
-
const subsetResult = subset_font_full("stalux", "normal", 400, 0, glyphs);
|
|
133
|
-
const hash = [...charsStr].slice(0, 12).join("");
|
|
122
|
+
const hash = hashStr(charsStr);
|
|
134
123
|
const filename = `subset-${subsetId}-${hash}.ttf`;
|
|
135
124
|
const outPath = join(outDir, filename);
|
|
136
125
|
|
|
137
126
|
if (!existsSync(outPath)) {
|
|
138
127
|
try {
|
|
139
|
-
|
|
128
|
+
const result = await subsetFont(fontBuffer, charsStr, {
|
|
129
|
+
targetFormat: "sfnt",
|
|
130
|
+
});
|
|
131
|
+
writeFileSync(outPath, result);
|
|
140
132
|
logger.info(
|
|
141
|
-
` on-demand subset: ${(
|
|
133
|
+
` on-demand subset: ${(result.length / 1024).toFixed(1)} KB → ${filename}`,
|
|
142
134
|
);
|
|
143
135
|
} catch {
|
|
144
136
|
return null;
|
|
@@ -152,6 +144,17 @@ async function generateSubsetOnDemand(
|
|
|
152
144
|
}`;
|
|
153
145
|
}
|
|
154
146
|
|
|
147
|
+
/** Simple hash function for strings */
|
|
148
|
+
function hashStr(s: string): string {
|
|
149
|
+
let hash = 0;
|
|
150
|
+
for (let i = 0; i < s.length && i < 100; i++) {
|
|
151
|
+
const ch = s.charCodeAt(i);
|
|
152
|
+
hash = (hash << 5) - hash + ch;
|
|
153
|
+
hash |= 0;
|
|
154
|
+
}
|
|
155
|
+
return Math.abs(hash).toString(36).slice(0, 8);
|
|
156
|
+
}
|
|
157
|
+
|
|
155
158
|
/**
|
|
156
159
|
* Create Vite middleware for on-demand font subsetting in dev mode.
|
|
157
160
|
*/
|
|
@@ -2,26 +2,32 @@
|
|
|
2
2
|
* Per-route font subsetting engine.
|
|
3
3
|
*
|
|
4
4
|
* Scans each content file individually, extracts its unique character set,
|
|
5
|
-
* and generates a minimal
|
|
5
|
+
* and generates a minimal TTF subset per route. Also generates a "common"
|
|
6
6
|
* subset for UI text (navs, i18n, layout).
|
|
7
7
|
*
|
|
8
8
|
* Output layout (under public/fonts/):
|
|
9
9
|
* fonts/
|
|
10
|
-
* common.ttf
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* manifest.json
|
|
15
|
-
* subset.css — global @font-face declarations
|
|
10
|
+
* common-{hash}.ttf — UI text subset (ASCII, nav, i18n)
|
|
11
|
+
* subset-{hash}.ttf — per-route subset (shared by routes with same chars)
|
|
12
|
+
* common.css — @font-face for common subset
|
|
13
|
+
* subset-{route}.css — per-route @font-face
|
|
14
|
+
* manifest.json — route → subset file mapping
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
17
|
import { createHash } from "node:crypto";
|
|
19
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
unlinkSync,
|
|
25
|
+
} from "node:fs";
|
|
20
26
|
import { resolve, basename, extname, join, dirname } from "node:path";
|
|
21
27
|
import { fileURLToPath } from "node:url";
|
|
22
28
|
|
|
23
29
|
import type { AstroIntegrationLogger } from "astro";
|
|
24
|
-
import
|
|
30
|
+
import subsetFont from "subset-font";
|
|
25
31
|
|
|
26
32
|
// ---------------------------------------------------------------------------
|
|
27
33
|
// Configuration
|
|
@@ -277,20 +283,16 @@ export async function runFontSubsetting(
|
|
|
277
283
|
}
|
|
278
284
|
const fontBuffer = readFileSync(fontPath);
|
|
279
285
|
|
|
280
|
-
// Register font with taetype for fast Rust subsetting
|
|
281
|
-
try {
|
|
282
|
-
register_font_raw("stalux", fontBuffer);
|
|
283
|
-
} catch (e) {
|
|
284
|
-
logger.warn(`Font registration failed: ${e}, skipping subsetting`);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
286
|
// Clean old generated font files to avoid stale cached references
|
|
289
287
|
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
290
288
|
if (existsSync(outDir)) {
|
|
291
289
|
for (const f of readdirSync(outDir)) {
|
|
292
290
|
if (f.startsWith("common-") || f.startsWith("subset-")) {
|
|
293
|
-
try {
|
|
291
|
+
try {
|
|
292
|
+
unlinkSync(join(outDir, f));
|
|
293
|
+
} catch {
|
|
294
|
+
/* skip */
|
|
295
|
+
}
|
|
294
296
|
}
|
|
295
297
|
}
|
|
296
298
|
}
|
|
@@ -320,25 +322,32 @@ export async function runFontSubsetting(
|
|
|
320
322
|
});
|
|
321
323
|
}
|
|
322
324
|
|
|
323
|
-
// 5. Deduplicate and generate subsets using
|
|
325
|
+
// 5. Deduplicate and generate subsets using subset-font (WASM)
|
|
324
326
|
mkdirSync(outDir, { recursive: true });
|
|
325
327
|
|
|
326
|
-
// Helper: subset using
|
|
327
|
-
function doSubset(chars: string): Uint8Array {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
328
|
+
// Helper: subset using subset-font (async WASM)
|
|
329
|
+
async function doSubset(chars: string): Promise<Uint8Array> {
|
|
330
|
+
if (!chars) return new Uint8Array(0);
|
|
331
|
+
try {
|
|
332
|
+
const result = await subsetFont(fontBuffer, chars, {
|
|
333
|
+
targetFormat: "sfnt", // "sfnt" = TTF output
|
|
334
|
+
});
|
|
335
|
+
return result;
|
|
336
|
+
} catch {
|
|
337
|
+
return new Uint8Array(0);
|
|
338
|
+
}
|
|
332
339
|
}
|
|
333
340
|
|
|
334
341
|
// Generate common subset
|
|
335
342
|
const commonFilename = `common-${commonHash}.ttf`;
|
|
336
343
|
const commonOutPath = join(outDir, commonFilename);
|
|
337
344
|
if (!existsSync(commonOutPath)) {
|
|
338
|
-
const data = doSubset(commonChars);
|
|
345
|
+
const data = await doSubset(commonChars);
|
|
339
346
|
if (data.length > 0) {
|
|
340
347
|
writeFileSync(commonOutPath, data);
|
|
341
|
-
logger.info(
|
|
348
|
+
logger.info(
|
|
349
|
+
` common subset: ${(data.length / 1024).toFixed(1)} KB → ${commonFilename}`,
|
|
350
|
+
);
|
|
342
351
|
}
|
|
343
352
|
}
|
|
344
353
|
|
|
@@ -357,79 +366,63 @@ export async function runFontSubsetting(
|
|
|
357
366
|
const postMap: Record<string, string> = {};
|
|
358
367
|
|
|
359
368
|
let subsetCount = 0;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
);
|
|
369
|
+
|
|
370
|
+
if (!skipPerRoute) {
|
|
371
|
+
for (const [hash, routes] of groups) {
|
|
372
|
+
const filename = `subset-${hash}.ttf`;
|
|
373
|
+
const outPath = join(outDir, filename);
|
|
374
|
+
if (!existsSync(outPath)) {
|
|
375
|
+
const route = routeSets.find((r) => r.hash === hash);
|
|
376
|
+
const chars = route?.chars ?? "";
|
|
377
|
+
const uniqueChars = chars
|
|
378
|
+
.split("")
|
|
379
|
+
.filter((ch) => !commonChars.includes(ch))
|
|
380
|
+
.join("");
|
|
381
|
+
if (uniqueChars.length > 0) {
|
|
382
|
+
const data = await doSubset(uniqueChars);
|
|
383
|
+
if (data.length > 0) {
|
|
384
|
+
writeFileSync(outPath, data);
|
|
385
|
+
subsetCount++;
|
|
386
|
+
logger.info(
|
|
387
|
+
` subset ${subsetCount}: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
382
390
|
}
|
|
383
391
|
}
|
|
384
|
-
}
|
|
385
392
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
// Generate per-route CSS (each route gets its own CSS file)
|
|
394
|
+
// If no unique chars, reference the common TTF as fallback
|
|
395
|
+
const refTtf = existsSync(outPath) ? filename : commonFilename;
|
|
389
396
|
for (const route of routes) {
|
|
390
397
|
const cssContent = `/* Auto-generated font subset for route: ${route} */
|
|
391
398
|
@font-face {
|
|
392
399
|
font-family: "LXGW WenKai Subset";
|
|
393
|
-
src: url("/fonts/${
|
|
400
|
+
src: url("/fonts/${refTtf}") format("truetype");
|
|
394
401
|
font-display: swap;
|
|
395
402
|
}
|
|
396
403
|
`;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
postMap[abbrlink] = cssFilename;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
} else {
|
|
409
|
-
// CSS already generated, just map
|
|
410
|
-
for (const route of routes) {
|
|
411
|
-
const cssFilename =
|
|
412
|
-
routes.length === 1
|
|
413
|
-
? `subset-${route.replace("/", "-")}.css`
|
|
414
|
-
: `subset-group-${hash}.css`;
|
|
415
|
-
routeMap[route] = cssFilename;
|
|
416
|
-
if (route.startsWith("posts/")) {
|
|
417
|
-
postMap[route.replace("posts/", "")] = cssFilename;
|
|
418
|
-
}
|
|
404
|
+
const cssName = route.replace("/", "-");
|
|
405
|
+
const cssFilename = `subset-${cssName}.css`;
|
|
406
|
+
writeFileSync(join(outDir, cssFilename), cssContent);
|
|
407
|
+
routeMap[route] = cssFilename;
|
|
408
|
+
|
|
409
|
+
if (route.startsWith("posts/")) {
|
|
410
|
+
const abbrlink = route.replace("posts/", "");
|
|
411
|
+
postMap[abbrlink] = cssFilename;
|
|
419
412
|
}
|
|
420
413
|
}
|
|
421
414
|
}
|
|
422
|
-
}
|
|
423
415
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
416
|
+
// 6. Generate manifest
|
|
417
|
+
const manifest: FontManifest = {
|
|
418
|
+
common: commonFilename,
|
|
419
|
+
routes: routeMap,
|
|
420
|
+
posts: postMap,
|
|
421
|
+
};
|
|
422
|
+
writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
423
|
+
|
|
424
|
+
logger.info(
|
|
425
|
+
`Font subsetting done: ${subsetCount} route subsets + common (${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB → variable)`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
435
428
|
}
|