@xingwangzhe/stalux 1.4.2 → 1.4.3
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 +1 -1
- package/src/internal/font-subset-dev.ts +78 -67
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Intercepts /fonts/subset-*.css requests, generates the WOFF2 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.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
@@ -17,43 +17,66 @@ const FONT_INPUT = "src/assets/fonts/LXGWWenKai-Regular.ttf";
|
|
|
17
17
|
const CONTENT_ROOT = "stalux";
|
|
18
18
|
const FONT_OUT_DIR = "public/fonts";
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Get characters for a given route subset.
|
|
22
|
+
* Returns null if route is unknown.
|
|
23
|
+
*/
|
|
24
|
+
function getRouteChars(projectRoot: string, subsetId: string): string | null {
|
|
25
|
+
const contentRoot = resolve(projectRoot, CONTENT_ROOT);
|
|
26
|
+
|
|
27
|
+
// ── Posts ──────────────────────────────────────────────
|
|
28
|
+
if (subsetId.startsWith("posts-")) {
|
|
29
|
+
const abbrlink = subsetId.slice(6);
|
|
30
|
+
const postsDir = join(contentRoot, "posts");
|
|
31
|
+
if (!existsSync(postsDir)) return null;
|
|
32
|
+
for (const f of readdirSync(postsDir)) {
|
|
33
|
+
if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
|
|
34
|
+
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
35
|
+
if (content.includes(`abbrlink: ${abbrlink}`)) return content;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── About ──────────────────────────────────────────────
|
|
23
42
|
if (subsetId === "about") {
|
|
24
|
-
const aboutDir =
|
|
43
|
+
const aboutDir = join(contentRoot, "about");
|
|
25
44
|
if (!existsSync(aboutDir)) return null;
|
|
26
45
|
for (const f of readdirSync(aboutDir)) {
|
|
27
|
-
if (f.endsWith(".md") || f.endsWith(".mdx")) return join(aboutDir, f);
|
|
46
|
+
if (f.endsWith(".md") || f.endsWith(".mdx")) return readFileSync(join(aboutDir, f), "utf-8");
|
|
28
47
|
}
|
|
29
48
|
return null;
|
|
30
49
|
}
|
|
50
|
+
|
|
51
|
+
// ── Words ──────────────────────────────────────────────
|
|
31
52
|
if (subsetId === "words") {
|
|
32
|
-
const wordsDir =
|
|
53
|
+
const wordsDir = join(contentRoot, "words");
|
|
33
54
|
if (!existsSync(wordsDir)) return null;
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
const all: string[] = [];
|
|
56
|
+
for (const f of readdirSync(wordsDir)) {
|
|
57
|
+
if (f.endsWith(".md") && !f.startsWith("_")) {
|
|
58
|
+
all.push(readFileSync(join(wordsDir, f), "utf-8"));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return all.join("\n") || null;
|
|
36
62
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
|
|
64
|
+
// ── Other HTML routes (home, archives, tags, categories, links, 404) ──
|
|
65
|
+
// These pages use i18n text + config YAML text, which are already
|
|
66
|
+
// covered by the common subset. Return config text for extra safety.
|
|
67
|
+
if (["home", "archives", "tags", "categories", "links", "common"].includes(subsetId)) {
|
|
68
|
+
const configDir = join(contentRoot, "config");
|
|
69
|
+
if (!existsSync(configDir)) return null;
|
|
70
|
+
const all: string[] = [];
|
|
71
|
+
for (const f of readdirSync(configDir)) {
|
|
72
|
+
if (f.endsWith(".yml") || f.endsWith(".yaml")) {
|
|
73
|
+
all.push(readFileSync(join(configDir, f), "utf-8"));
|
|
45
74
|
}
|
|
46
75
|
}
|
|
47
|
-
return null;
|
|
76
|
+
return all.join("\n") || null;
|
|
48
77
|
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
78
|
|
|
52
|
-
|
|
53
|
-
function extractChars(text: string): string {
|
|
54
|
-
const set = new Set<string>();
|
|
55
|
-
for (const ch of text) set.add(ch);
|
|
56
|
-
return [...set].sort().join("");
|
|
79
|
+
return null;
|
|
57
80
|
}
|
|
58
81
|
|
|
59
82
|
/**
|
|
@@ -65,42 +88,25 @@ async function generateSubsetOnDemand(
|
|
|
65
88
|
subsetId: string,
|
|
66
89
|
logger: AstroIntegrationLogger,
|
|
67
90
|
): Promise<string | null> {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!contentPath) return null;
|
|
71
|
-
|
|
72
|
-
// 2. Read content + extract chars
|
|
73
|
-
let content: string;
|
|
74
|
-
if (contentPath.endsWith("words")) {
|
|
75
|
-
// aggregate all words
|
|
76
|
-
const allWords: string[] = [];
|
|
77
|
-
for (const f of readdirSync(contentPath)) {
|
|
78
|
-
if (f.endsWith(".md") && !f.startsWith("_")) {
|
|
79
|
-
allWords.push(readFileSync(join(contentPath, f), "utf-8"));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
content = allWords.join("\n");
|
|
83
|
-
} else {
|
|
84
|
-
content = readFileSync(contentPath, "utf-8");
|
|
85
|
-
}
|
|
91
|
+
const chars = getRouteChars(projectRoot, subsetId);
|
|
92
|
+
if (!chars) return null;
|
|
86
93
|
|
|
87
|
-
|
|
94
|
+
// Extract unique characters
|
|
95
|
+
const charSet = new Set<string>();
|
|
96
|
+
for (const ch of chars) charSet.add(ch);
|
|
97
|
+
if (charSet.size === 0) return null;
|
|
88
98
|
|
|
89
|
-
//
|
|
99
|
+
// Deduplicate against common subset: read common.css to find its referenced WOFF2
|
|
90
100
|
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
// the original source. But we can just generate the full subset.
|
|
101
|
-
// To minimize size, we note that common subset already covers most.
|
|
102
|
-
|
|
103
|
-
// 4. Locate font file
|
|
101
|
+
const commonCssPath = join(outDir, "common.css");
|
|
102
|
+
const commonCharSet = new Set<string>();
|
|
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
|
+
// Locate font file
|
|
104
110
|
const fontPaths = [
|
|
105
111
|
resolve(projectRoot, FONT_INPUT),
|
|
106
112
|
resolve(projectRoot, "node_modules", "@xingwangzhe", "stalux", FONT_INPUT),
|
|
@@ -115,18 +121,24 @@ async function generateSubsetOnDemand(
|
|
|
115
121
|
const fontBuffer = readFileSync(fontPath);
|
|
116
122
|
mkdirSync(outDir, { recursive: true });
|
|
117
123
|
|
|
118
|
-
//
|
|
119
|
-
const
|
|
124
|
+
// Generate subset WOFF2
|
|
125
|
+
const charsStr = [...charSet].sort().join("");
|
|
126
|
+
const hash = [...charsStr].slice(0, 12).join("");
|
|
120
127
|
const filename = `subset-${subsetId}-${hash}.woff2`;
|
|
121
128
|
const outPath = join(outDir, filename);
|
|
122
129
|
|
|
123
130
|
if (!existsSync(outPath)) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
try {
|
|
132
|
+
const data = await subsetFontFn(fontBuffer, charsStr, { targetFormat: "woff2" });
|
|
133
|
+
writeFileSync(outPath, data);
|
|
134
|
+
logger.info(
|
|
135
|
+
` on-demand subset: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
|
|
136
|
+
);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
127
140
|
}
|
|
128
141
|
|
|
129
|
-
// 6. Return CSS content
|
|
130
142
|
return `@font-face {
|
|
131
143
|
font-family: "LXGW WenKai Subset";
|
|
132
144
|
src: url("/fonts/${filename}") format("woff2");
|
|
@@ -145,7 +157,7 @@ export function createDevFontMiddleware(projectRoot: string, logger: AstroIntegr
|
|
|
145
157
|
|
|
146
158
|
const subsetId = match[1];
|
|
147
159
|
|
|
148
|
-
// Check if file already exists
|
|
160
|
+
// Check if file already exists (cached from previous on-demand generation)
|
|
149
161
|
const cssPath = resolve(projectRoot, FONT_OUT_DIR, `subset-${subsetId}.css`);
|
|
150
162
|
if (existsSync(cssPath)) {
|
|
151
163
|
res.statusCode = 200;
|
|
@@ -158,8 +170,7 @@ export function createDevFontMiddleware(projectRoot: string, logger: AstroIntegr
|
|
|
158
170
|
logger.info(`Font subset: on-demand generating for "${subsetId}"...`);
|
|
159
171
|
const css = await generateSubsetOnDemand(projectRoot, subsetId, logger);
|
|
160
172
|
if (css) {
|
|
161
|
-
//
|
|
162
|
-
writeFileSync(cssPath, css);
|
|
173
|
+
writeFileSync(cssPath, css); // cache it
|
|
163
174
|
res.statusCode = 200;
|
|
164
175
|
res.setHeader("Content-Type", "text/css");
|
|
165
176
|
res.end(css);
|