@xingwangzhe/stalux 1.5.4 → 1.6.1
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 +143 -20
- package/src/internal/font-subset.ts +137 -39
- package/src/pages/archives.astro +1 -1
- package/src/pages/categories/[category].astro +1 -1
- package/src/pages/categories/index.astro +3 -1
- package/src/pages/tags/[tag].astro +1 -1
- package/src/pages/tags/index.astro +3 -1
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.1",
|
|
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";
|
|
@@ -63,10 +63,37 @@ function getRouteChars(projectRoot: string, subsetId: string): string | null {
|
|
|
63
63
|
return all.join("\n") || null;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
// ──
|
|
66
|
+
// ── Archives ────────────────────────────────────────────
|
|
67
|
+
if (subsetId === "archives") {
|
|
68
|
+
return getAllPostContent(contentRoot);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Tags index ──────────────────────────────────────────
|
|
72
|
+
if (subsetId === "tags") {
|
|
73
|
+
return getAllTagsAndPostContent(contentRoot);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Tags detail ─────────────────────────────────────────
|
|
77
|
+
if (subsetId.startsWith("tags-")) {
|
|
78
|
+
const tagName = decodeURIComponent(subsetId.slice(5));
|
|
79
|
+
return getTagContent(contentRoot, tagName);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Categories index ────────────────────────────────────
|
|
83
|
+
if (subsetId === "categories") {
|
|
84
|
+
return getAllCategoriesAndPostContent(contentRoot);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Categories detail ───────────────────────────────────
|
|
88
|
+
if (subsetId.startsWith("categories-")) {
|
|
89
|
+
const catName = decodeURIComponent(subsetId.slice(11));
|
|
90
|
+
return getCategoryContent(contentRoot, catName);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Other HTML routes (home, links, 404) ──
|
|
67
94
|
// These pages use i18n text + config YAML text, which are already
|
|
68
95
|
// covered by the common subset. Return config text for extra safety.
|
|
69
|
-
if (["home", "
|
|
96
|
+
if (["home", "links", "common"].includes(subsetId)) {
|
|
70
97
|
const configDir = join(contentRoot, "config");
|
|
71
98
|
if (!existsSync(configDir)) return null;
|
|
72
99
|
const all: string[] = [];
|
|
@@ -81,6 +108,99 @@ function getRouteChars(projectRoot: string, subsetId: string): string | null {
|
|
|
81
108
|
return null;
|
|
82
109
|
}
|
|
83
110
|
|
|
111
|
+
/** Get all post content concatenated (including template/_ files) */
|
|
112
|
+
function getAllPostContent(contentRoot: string): string | null {
|
|
113
|
+
const postsDir = join(contentRoot, "posts");
|
|
114
|
+
if (!existsSync(postsDir)) return null;
|
|
115
|
+
const all: string[] = [];
|
|
116
|
+
for (const f of readdirSync(postsDir)) {
|
|
117
|
+
if (f.endsWith(".md") || f.endsWith(".mdx")) {
|
|
118
|
+
all.push(readFileSync(join(postsDir, f), "utf-8"));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return all.join("\n") || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Get all tags + all post content */
|
|
125
|
+
function getAllTagsAndPostContent(contentRoot: string): string | null {
|
|
126
|
+
const postContent = getAllPostContent(contentRoot);
|
|
127
|
+
if (!postContent) return null;
|
|
128
|
+
const tags = extractAllYamlValues(postContent, "tags");
|
|
129
|
+
return postContent + " " + tags.join(" ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Get all categories + all post content */
|
|
133
|
+
function getAllCategoriesAndPostContent(contentRoot: string): string | null {
|
|
134
|
+
const postContent = getAllPostContent(contentRoot);
|
|
135
|
+
if (!postContent) return null;
|
|
136
|
+
const categories = extractAllYamlValues(postContent, "categories");
|
|
137
|
+
return postContent + " " + categories.join(" ");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Get content for a specific tag: tag name + posts with that tag */
|
|
141
|
+
function getTagContent(contentRoot: string, tagName: string): string | null {
|
|
142
|
+
const postsDir = join(contentRoot, "posts");
|
|
143
|
+
if (!existsSync(postsDir)) return null;
|
|
144
|
+
const all: string[] = [tagName];
|
|
145
|
+
for (const f of readdirSync(postsDir)) {
|
|
146
|
+
if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
|
|
147
|
+
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
148
|
+
const tags = extractYamlListFromContent(content, "tags");
|
|
149
|
+
if (tags.includes(tagName)) {
|
|
150
|
+
all.push(content);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return all.join("\n") || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Get content for a specific category: category name + posts in that category */
|
|
158
|
+
function getCategoryContent(contentRoot: string, catName: string): string | null {
|
|
159
|
+
const postsDir = join(contentRoot, "posts");
|
|
160
|
+
if (!existsSync(postsDir)) return null;
|
|
161
|
+
const all: string[] = [catName];
|
|
162
|
+
for (const f of readdirSync(postsDir)) {
|
|
163
|
+
if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
|
|
164
|
+
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
165
|
+
const categories = extractYamlListFromContent(content, "categories");
|
|
166
|
+
if (categories.includes(catName)) {
|
|
167
|
+
all.push(content);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return all.join("\n") || null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Extract a YAML list value from frontmatter by key */
|
|
175
|
+
function extractYamlListFromContent(content: string, key: string): string[] {
|
|
176
|
+
const lines: string[] = [];
|
|
177
|
+
const regex = new RegExp(`^${key}:$`, "m");
|
|
178
|
+
const match = content.match(regex);
|
|
179
|
+
if (!match) return [];
|
|
180
|
+
const startIdx = match.index! + match[0].length;
|
|
181
|
+
const afterKey = content.slice(startIdx);
|
|
182
|
+
const listMatch = afterKey.match(/^\n((?:\s+-\s+.+\n?)*)/);
|
|
183
|
+
if (!listMatch) return [];
|
|
184
|
+
for (const line of listMatch[1].split("\n")) {
|
|
185
|
+
const item = line.match(/^\s+-\s+(.+)/)?.[1];
|
|
186
|
+
if (item) lines.push(item.trim());
|
|
187
|
+
}
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Extract all YAML list values from all posts by key */
|
|
192
|
+
function extractAllYamlValues(postContent: string, key: string): string[] {
|
|
193
|
+
const values = new Set<string>();
|
|
194
|
+
// Split by frontmatter boundaries (---)
|
|
195
|
+
const parts = postContent.split(/^---$/m);
|
|
196
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
197
|
+
const fm = parts[i];
|
|
198
|
+
const list = extractYamlListFromContent("---\n" + fm + "\n---\n", key);
|
|
199
|
+
for (const v of list) values.add(v);
|
|
200
|
+
}
|
|
201
|
+
return [...values];
|
|
202
|
+
}
|
|
203
|
+
|
|
84
204
|
/**
|
|
85
205
|
* Generate and serve a font subset on demand.
|
|
86
206
|
* Returns the CSS content to be served.
|
|
@@ -98,14 +218,6 @@ async function generateSubsetOnDemand(
|
|
|
98
218
|
for (const ch of chars) charSet.add(ch);
|
|
99
219
|
if (charSet.size === 0) return null;
|
|
100
220
|
|
|
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
221
|
// Locate font file
|
|
110
222
|
const fontPaths = [
|
|
111
223
|
resolve(projectRoot, FONT_INPUT),
|
|
@@ -122,23 +234,23 @@ async function generateSubsetOnDemand(
|
|
|
122
234
|
if (!fontPath) return null;
|
|
123
235
|
|
|
124
236
|
const fontBuffer = readFileSync(fontPath);
|
|
237
|
+
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
125
238
|
mkdirSync(outDir, { recursive: true });
|
|
126
239
|
|
|
127
|
-
// Generate subset TTF using
|
|
240
|
+
// Generate subset TTF using subset-font (WASM)
|
|
128
241
|
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("");
|
|
242
|
+
const hash = hashStr(charsStr);
|
|
134
243
|
const filename = `subset-${subsetId}-${hash}.ttf`;
|
|
135
244
|
const outPath = join(outDir, filename);
|
|
136
245
|
|
|
137
246
|
if (!existsSync(outPath)) {
|
|
138
247
|
try {
|
|
139
|
-
|
|
248
|
+
const result = await subsetFont(fontBuffer, charsStr, {
|
|
249
|
+
targetFormat: "sfnt",
|
|
250
|
+
});
|
|
251
|
+
writeFileSync(outPath, result);
|
|
140
252
|
logger.info(
|
|
141
|
-
` on-demand subset: ${(
|
|
253
|
+
` on-demand subset: ${(result.length / 1024).toFixed(1)} KB → ${filename}`,
|
|
142
254
|
);
|
|
143
255
|
} catch {
|
|
144
256
|
return null;
|
|
@@ -152,6 +264,17 @@ async function generateSubsetOnDemand(
|
|
|
152
264
|
}`;
|
|
153
265
|
}
|
|
154
266
|
|
|
267
|
+
/** Simple hash function for strings */
|
|
268
|
+
function hashStr(s: string): string {
|
|
269
|
+
let hash = 0;
|
|
270
|
+
for (let i = 0; i < s.length && i < 100; i++) {
|
|
271
|
+
const ch = s.charCodeAt(i);
|
|
272
|
+
hash = (hash << 5) - hash + ch;
|
|
273
|
+
hash |= 0;
|
|
274
|
+
}
|
|
275
|
+
return Math.abs(hash).toString(36).slice(0, 8);
|
|
276
|
+
}
|
|
277
|
+
|
|
155
278
|
/**
|
|
156
279
|
* Create Vite middleware for on-demand font subsetting in dev mode.
|
|
157
280
|
*/
|
|
@@ -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
|
|
@@ -97,19 +103,81 @@ function scanContent(projectRoot: string): ContentFile[] {
|
|
|
97
103
|
|
|
98
104
|
// Posts
|
|
99
105
|
const postsDir = join(contentRoot, "posts");
|
|
106
|
+
const allPostContents: {
|
|
107
|
+
abbrlink: string;
|
|
108
|
+
tags: string[];
|
|
109
|
+
categories: string[];
|
|
110
|
+
content: string;
|
|
111
|
+
}[] = [];
|
|
100
112
|
if (existsSync(postsDir)) {
|
|
101
113
|
for (const f of readdirSync(postsDir)) {
|
|
102
114
|
if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
|
|
103
|
-
if (f.startsWith("_")) continue; // skip template files
|
|
104
115
|
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
105
|
-
|
|
116
|
+
const isTemplate = f.startsWith("_");
|
|
117
|
+
|
|
118
|
+
// Per-post routes: skip template (_) files
|
|
119
|
+
if (!isTemplate) {
|
|
120
|
+
const abbrlinkMatch = content.match(/^abbrlink:\s*(.+)$/m);
|
|
121
|
+
const abbrlink = abbrlinkMatch?.[1]?.trim() ?? basename(f, extname(f));
|
|
122
|
+
files.push({
|
|
123
|
+
route: `posts/${abbrlink}`,
|
|
124
|
+
filePath: join(postsDir, f),
|
|
125
|
+
abbrlink,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Virtual routes (archives/tags/categories): include ALL posts including templates
|
|
130
|
+
// because content collections load them and they render on those pages
|
|
106
131
|
const abbrlinkMatch = content.match(/^abbrlink:\s*(.+)$/m);
|
|
107
132
|
const abbrlink = abbrlinkMatch?.[1]?.trim() ?? basename(f, extname(f));
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
133
|
+
const tags = extractYamlList(content, "tags");
|
|
134
|
+
const categories = extractYamlList(content, "categories");
|
|
135
|
+
allPostContents.push({ abbrlink, tags, categories, content });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Build virtual routes for archives, tags, categories (aggregated pages)
|
|
140
|
+
// These pages render content from ALL posts
|
|
141
|
+
if (allPostContents.length > 0) {
|
|
142
|
+
const allContent = allPostContents.map((p) => p.content).join("\n");
|
|
143
|
+
|
|
144
|
+
// Archives: all posts' content (titles, dates, etc.)
|
|
145
|
+
virtualRouteCache.set("archives", extractChars(allContent));
|
|
146
|
+
|
|
147
|
+
// Tags index: all tags + all posts' content
|
|
148
|
+
const allTags = [...new Set(allPostContents.flatMap((p) => p.tags))].join(" ");
|
|
149
|
+
virtualRouteCache.set("tags", extractChars(allContent + allTags));
|
|
150
|
+
|
|
151
|
+
// Categories index: all categories + all posts' content
|
|
152
|
+
const allCategories = [...new Set(allPostContents.flatMap((p) => p.categories))].join(" ");
|
|
153
|
+
virtualRouteCache.set("categories", extractChars(allContent + allCategories));
|
|
154
|
+
|
|
155
|
+
// Per-tag routes: tag name + posts with that tag
|
|
156
|
+
const tagPosts = new Map<string, string[]>();
|
|
157
|
+
for (const p of allPostContents) {
|
|
158
|
+
for (const tag of p.tags) {
|
|
159
|
+
const list = tagPosts.get(tag) ?? [];
|
|
160
|
+
list.push(p.content);
|
|
161
|
+
tagPosts.set(tag, list);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const [tag, contents] of tagPosts) {
|
|
165
|
+
const tagContent = tag + " " + contents.join("\n");
|
|
166
|
+
virtualRouteCache.set(`tags-${tag}`, extractChars(tagContent));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Per-category routes: category name + posts in that category
|
|
170
|
+
const catPosts = new Map<string, string[]>();
|
|
171
|
+
for (const p of allPostContents) {
|
|
172
|
+
for (const cat of p.categories) {
|
|
173
|
+
const list = catPosts.get(cat) ?? [];
|
|
174
|
+
list.push(p.content);
|
|
175
|
+
catPosts.set(cat, list);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const [cat, contents] of catPosts) {
|
|
179
|
+
const catContent = cat + " " + contents.join("\n");
|
|
180
|
+
virtualRouteCache.set(`categories-${cat}`, extractChars(catContent));
|
|
113
181
|
}
|
|
114
182
|
}
|
|
115
183
|
|
|
@@ -150,11 +218,37 @@ function scanContent(projectRoot: string): ContentFile[] {
|
|
|
150
218
|
}
|
|
151
219
|
}
|
|
152
220
|
|
|
221
|
+
// Add virtual routes (archives, tags, categories) as content files
|
|
222
|
+
for (const [route] of virtualRouteCache) {
|
|
223
|
+
files.push({
|
|
224
|
+
route,
|
|
225
|
+
filePath: resolve(projectRoot, CONTENT_ROOT, `_virtual/${route}.md`),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
153
229
|
return files;
|
|
154
230
|
}
|
|
155
231
|
|
|
232
|
+
/** Extract a YAML list value from frontmatter by key */
|
|
233
|
+
function extractYamlList(content: string, key: string): string[] {
|
|
234
|
+
const lines: string[] = [];
|
|
235
|
+
const regex = new RegExp(`^${key}:$`, "m");
|
|
236
|
+
const match = content.match(regex);
|
|
237
|
+
if (!match) return [];
|
|
238
|
+
const startIdx = match.index! + match[0].length;
|
|
239
|
+
const afterKey = content.slice(startIdx);
|
|
240
|
+
const listMatch = afterKey.match(/^\n((?:\s+-\s+.+\n?)*)/);
|
|
241
|
+
if (!listMatch) return [];
|
|
242
|
+
for (const line of listMatch[1].split("\n")) {
|
|
243
|
+
const item = line.match(/^\s+-\s+(.+)/)?.[1];
|
|
244
|
+
if (item) lines.push(item.trim());
|
|
245
|
+
}
|
|
246
|
+
return lines;
|
|
247
|
+
}
|
|
248
|
+
|
|
156
249
|
// Cache for virtual route char sets
|
|
157
250
|
const wordCharsCache = new Map<string, string>();
|
|
251
|
+
const virtualRouteCache = new Map<string, string>();
|
|
158
252
|
|
|
159
253
|
// ---------------------------------------------------------------------------
|
|
160
254
|
// Common subset: extract chars from UI source files
|
|
@@ -277,20 +371,16 @@ export async function runFontSubsetting(
|
|
|
277
371
|
}
|
|
278
372
|
const fontBuffer = readFileSync(fontPath);
|
|
279
373
|
|
|
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
374
|
// Clean old generated font files to avoid stale cached references
|
|
289
375
|
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
290
376
|
if (existsSync(outDir)) {
|
|
291
377
|
for (const f of readdirSync(outDir)) {
|
|
292
378
|
if (f.startsWith("common-") || f.startsWith("subset-")) {
|
|
293
|
-
try {
|
|
379
|
+
try {
|
|
380
|
+
unlinkSync(join(outDir, f));
|
|
381
|
+
} catch {
|
|
382
|
+
/* skip */
|
|
383
|
+
}
|
|
294
384
|
}
|
|
295
385
|
}
|
|
296
386
|
}
|
|
@@ -309,6 +399,8 @@ export async function runFontSubsetting(
|
|
|
309
399
|
let content: string;
|
|
310
400
|
if (file.route === "words" && wordCharsCache.has("words")) {
|
|
311
401
|
content = wordCharsCache.get("words")!;
|
|
402
|
+
} else if (virtualRouteCache.has(file.route)) {
|
|
403
|
+
content = virtualRouteCache.get(file.route)!;
|
|
312
404
|
} else {
|
|
313
405
|
content = readFileSync(file.filePath, "utf-8");
|
|
314
406
|
}
|
|
@@ -320,25 +412,32 @@ export async function runFontSubsetting(
|
|
|
320
412
|
});
|
|
321
413
|
}
|
|
322
414
|
|
|
323
|
-
// 5. Deduplicate and generate subsets using
|
|
415
|
+
// 5. Deduplicate and generate subsets using subset-font (WASM)
|
|
324
416
|
mkdirSync(outDir, { recursive: true });
|
|
325
417
|
|
|
326
|
-
// Helper: subset using
|
|
327
|
-
function doSubset(chars: string): Uint8Array {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
418
|
+
// Helper: subset using subset-font (async WASM)
|
|
419
|
+
async function doSubset(chars: string): Promise<Uint8Array> {
|
|
420
|
+
if (!chars) return new Uint8Array(0);
|
|
421
|
+
try {
|
|
422
|
+
const result = await subsetFont(fontBuffer, chars, {
|
|
423
|
+
targetFormat: "sfnt", // "sfnt" = TTF output
|
|
424
|
+
});
|
|
425
|
+
return result;
|
|
426
|
+
} catch {
|
|
427
|
+
return new Uint8Array(0);
|
|
428
|
+
}
|
|
332
429
|
}
|
|
333
430
|
|
|
334
431
|
// Generate common subset
|
|
335
432
|
const commonFilename = `common-${commonHash}.ttf`;
|
|
336
433
|
const commonOutPath = join(outDir, commonFilename);
|
|
337
434
|
if (!existsSync(commonOutPath)) {
|
|
338
|
-
const data = doSubset(commonChars);
|
|
435
|
+
const data = await doSubset(commonChars);
|
|
339
436
|
if (data.length > 0) {
|
|
340
437
|
writeFileSync(commonOutPath, data);
|
|
341
|
-
logger.info(
|
|
438
|
+
logger.info(
|
|
439
|
+
` common subset: ${(data.length / 1024).toFixed(1)} KB → ${commonFilename}`,
|
|
440
|
+
);
|
|
342
441
|
}
|
|
343
442
|
}
|
|
344
443
|
|
|
@@ -370,13 +469,12 @@ export async function runFontSubsetting(
|
|
|
370
469
|
.filter((ch) => !commonChars.includes(ch))
|
|
371
470
|
.join("");
|
|
372
471
|
if (uniqueChars.length > 0) {
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
writeFileSync(outPath, result.fontBytes);
|
|
472
|
+
const data = await doSubset(uniqueChars);
|
|
473
|
+
if (data.length > 0) {
|
|
474
|
+
writeFileSync(outPath, data);
|
|
377
475
|
subsetCount++;
|
|
378
476
|
logger.info(
|
|
379
|
-
` subset ${subsetCount}: ${(
|
|
477
|
+
` subset ${subsetCount}: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
|
|
380
478
|
);
|
|
381
479
|
}
|
|
382
480
|
}
|
package/src/pages/archives.astro
CHANGED
|
@@ -12,7 +12,7 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
|
|
|
12
12
|
const posts = await getCollection("posts", ({ data }) => !data.draft);
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
<Stalux title={t("archives.title") + " | " + siteConfig.title} description={t("archives.description", { title: siteConfig.title })}>
|
|
15
|
+
<Stalux title={t("archives.title") + " | " + siteConfig.title} description={t("archives.description", { title: siteConfig.title })} fontSubsetId={posts.length > 0 ? "archives" : undefined}>
|
|
16
16
|
<div style="height: 3em;"></div>
|
|
17
17
|
<Navs />
|
|
18
18
|
<h1 class={styles.h1}>{t("archives.heading")}</h1>
|
|
@@ -27,7 +27,7 @@ const props = Astro.props as Props;
|
|
|
27
27
|
<Stalux
|
|
28
28
|
title={t("taxonomy.category") + ": " + props.category + " | " + siteConfig.title}
|
|
29
29
|
description={t("taxonomy.browseCategory", { title: siteConfig.title, category: props.category, count: props.count })}
|
|
30
|
-
|
|
30
|
+
fontSubsetId={props.posts.length > 0 ? `categories-${encodeURIComponent(props.category)}` : undefined}
|
|
31
31
|
>
|
|
32
32
|
<div style="height: 3em;"></div>
|
|
33
33
|
<Navs />
|
|
@@ -9,12 +9,14 @@ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, get
|
|
|
9
9
|
const configEntries2 = await getCollection("config");
|
|
10
10
|
const siteConfig = getSiteData(configEntries2);
|
|
11
11
|
const { t } = createTranslator(siteConfig.lang || "zh-CN");
|
|
12
|
+
const posts = await getCollection("posts", ({ data }) => !data.draft);
|
|
13
|
+
const hasContent = posts.length > 0;
|
|
12
14
|
---
|
|
13
15
|
|
|
14
16
|
<Stalux
|
|
15
17
|
title={t("categories.title") + " | " + siteConfig.title}
|
|
16
18
|
description={t("categories.description", { title: siteConfig.title })}
|
|
17
|
-
|
|
19
|
+
fontSubsetId={hasContent ? "categories" : undefined}
|
|
18
20
|
>
|
|
19
21
|
<div style="height: 3em;"></div>
|
|
20
22
|
<Navs />
|
|
@@ -29,7 +29,7 @@ const props = Astro.props as Props;
|
|
|
29
29
|
title={t("taxonomy.tag") + ": " + props.tag + " | " + siteConfig.title}
|
|
30
30
|
description={t("taxonomy.browseTag", { title: siteConfig.title, tag: props.tag, count: props.count })}
|
|
31
31
|
noindex={props.count < 3}
|
|
32
|
-
|
|
32
|
+
fontSubsetId={props.posts.length > 0 ? `tags-${encodeURIComponent(props.tag)}` : undefined}
|
|
33
33
|
>
|
|
34
34
|
<div style="height: 3em;"></div>
|
|
35
35
|
<Navs />
|
|
@@ -9,12 +9,14 @@ import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, get
|
|
|
9
9
|
const configEntries = await getCollection("config");
|
|
10
10
|
const siteConfig = getSiteData(configEntries);
|
|
11
11
|
const { t } = createTranslator(siteConfig.lang || "zh-CN");
|
|
12
|
+
const posts = await getCollection("posts", ({ data }) => !data.draft);
|
|
13
|
+
const hasContent = posts.length > 0;
|
|
12
14
|
---
|
|
13
15
|
|
|
14
16
|
<Stalux
|
|
15
17
|
title={t("tags.title") + " | " + siteConfig.title}
|
|
16
18
|
description={t("tags.description", { title: siteConfig.title })}
|
|
17
|
-
|
|
19
|
+
fontSubsetId={hasContent ? "tags" : undefined}
|
|
18
20
|
>
|
|
19
21
|
<div style="height: 3em;"></div>
|
|
20
22
|
<Navs />
|