@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.5.3",
4
- "description": "A powerful, modern Astro blog theme \u2014 use as template or install as plugin",
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
- "taetype": "0.1.3",
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 WOFF2 subset
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 { register_font_raw, get_glyph_ids, subset_font_full } from "taetype";
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 taetype
120
+ // Generate subset TTF using subset-font (WASM)
128
121
  const charsStr = [...charSet].sort().join("");
129
- try { register_font_raw("stalux", fontBuffer); } catch { /* already registered */ }
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
- writeFileSync(outPath, subsetResult.fontBytes);
128
+ const result = await subsetFont(fontBuffer, charsStr, {
129
+ targetFormat: "sfnt",
130
+ });
131
+ writeFileSync(outPath, result);
140
132
  logger.info(
141
- ` on-demand subset: ${(subsetResult.fontBytes.length / 1024).toFixed(1)} KB → ${filename}`,
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 WOFF2 subset per route. Also generates a "common"
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 — UI text subset (ASCII, nav, i18n)
11
- * post-{abbrlink}.ttf — per-post subset
12
- * about.ttf — about page subset
13
- * words.ttf — combined words subset
14
- * manifest.json — route → subset file mapping
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 { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from "node:fs";
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 { register_font_raw, get_glyph_ids, subset_font_full } from "taetype";
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 { unlinkSync(join(outDir, f)); } catch { /* skip */ }
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 taetype (Rust native, fast)
325
+ // 5. Deduplicate and generate subsets using subset-font (WASM)
324
326
  mkdirSync(outDir, { recursive: true });
325
327
 
326
- // Helper: subset using taetype
327
- function doSubset(chars: string): Uint8Array {
328
- const glyphs = get_glyph_ids(chars, "stalux", "normal", 400);
329
- if (glyphs.length === 0) return new Uint8Array(0);
330
- const result = subset_font_full("stalux", "normal", 400, 0, glyphs);
331
- return result.fontBytes;
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(` common subset: ${(data.length / 1024).toFixed(1)} KB → ${commonFilename}`);
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
- const cssGenerated = new Set<string>();
361
-
362
- if (!skipPerRoute) {
363
- for (const [hash, routes] of groups) {
364
- const filename = `subset-${hash}.ttf`;
365
- const outPath = join(outDir, filename);
366
- if (!existsSync(outPath)) {
367
- const route = routeSets.find((r) => r.hash === hash);
368
- const chars = route?.chars ?? "";
369
- const uniqueChars = chars
370
- .split("")
371
- .filter((ch) => !commonChars.includes(ch))
372
- .join("");
373
- if (uniqueChars.length > 0) {
374
- const glyphs = get_glyph_ids(uniqueChars, "stalux", "normal", 400);
375
- if (glyphs.length > 0) {
376
- const result = subset_font_full("stalux", "normal", 400, 0, glyphs);
377
- writeFileSync(outPath, result.fontBytes);
378
- subsetCount++;
379
- logger.info(
380
- ` subset ${subsetCount}: ${(result.fontBytes.length / 1024).toFixed(1)} KB → ${filename}`,
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
- // Generate per-route CSS (one per unique hash, references all routes with this hash)
387
- if (!cssGenerated.has(hash)) {
388
- cssGenerated.add(hash);
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/${filename}") format("truetype");
400
+ src: url("/fonts/${refTtf}") format("truetype");
394
401
  font-display: swap;
395
402
  }
396
403
  `;
397
- // Use first route's name as the CSS filename (they share the same subset)
398
- const cssName = routes.length === 1 ? route.replace("/", "-") : `group-${hash}`;
399
- const cssFilename = `subset-${cssName}.css`;
400
- writeFileSync(join(outDir, cssFilename), cssContent);
401
- routeMap[route] = cssFilename;
402
-
403
- if (route.startsWith("posts/")) {
404
- const abbrlink = route.replace("posts/", "");
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
- // 6. Generate manifest
425
- const manifest: FontManifest = {
426
- common: commonFilename,
427
- routes: routeMap,
428
- posts: postMap,
429
- };
430
- writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
431
-
432
- logger.info(
433
- `Font subsetting done: ${subsetCount} route subsets + common (${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB → variable)`,
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
  }