@xingwangzhe/stalux 1.22.7 → 1.23.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 +1 -2
- package/src/components/stalux/layout/head.astro +5 -3
- package/src/components/stalux/layout/listPage.astro +2 -3
- package/src/index.ts +69 -42
- package/src/internal/font-slices.ts +207 -0
- package/src/layouts/Stalux.astro +0 -41
- package/src/pages/about.astro +0 -1
- package/src/pages/archives.astro +1 -1
- package/src/pages/categories/[category].astro +0 -1
- package/src/pages/categories/index.astro +0 -1
- package/src/pages/index.astro +1 -1
- package/src/pages/links.astro +5 -6
- package/src/pages/posts/[post].astro +0 -1
- package/src/pages/tags/[tag].astro +0 -1
- package/src/pages/tags/index.astro +0 -1
- package/src/pages/words.astro +1 -1
- package/src/styles/base/init.css +1 -1
- package/src/internal/font-subset-dev.ts +0 -325
- package/src/internal/font-subset.ts +0 -619
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dev mode on-demand font subsetting middleware.
|
|
3
|
-
*
|
|
4
|
-
* Intercepts /fonts/subset-*.css requests, generates the TTF subset
|
|
5
|
-
* for the requested route on-the-fly, and serves it.
|
|
6
|
-
*
|
|
7
|
-
* Handles ALL HTML routes: posts, about, words, home, archives, tags, etc.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { resolve, join, dirname } from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
|
|
14
|
-
import type { AstroIntegrationLogger } from "astro";
|
|
15
|
-
import subsetFont from "subset-font";
|
|
16
|
-
|
|
17
|
-
const FONT_INPUT = "src/assets/fonts/LXGWWenKai-Regular.ttf";
|
|
18
|
-
const CONTENT_ROOT = "stalux";
|
|
19
|
-
const FONT_OUT_DIR = "public/fonts";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Get characters for a given route subset.
|
|
23
|
-
* Returns null if route is unknown.
|
|
24
|
-
*/
|
|
25
|
-
function getRouteChars(projectRoot: string, subsetId: string): string | null {
|
|
26
|
-
const contentRoot = resolve(projectRoot, CONTENT_ROOT);
|
|
27
|
-
|
|
28
|
-
// ── Posts ──────────────────────────────────────────────
|
|
29
|
-
if (subsetId.startsWith("posts-")) {
|
|
30
|
-
const abbrlink = subsetId.slice(6);
|
|
31
|
-
const postsDir = join(contentRoot, "posts");
|
|
32
|
-
if (!existsSync(postsDir)) return null;
|
|
33
|
-
for (const f of readdirSync(postsDir)) {
|
|
34
|
-
if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
|
|
35
|
-
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
36
|
-
if (content.includes(`abbrlink: ${abbrlink}`)) return content;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ── About ──────────────────────────────────────────────
|
|
43
|
-
if (subsetId === "about") {
|
|
44
|
-
const aboutDir = join(contentRoot, "about");
|
|
45
|
-
if (!existsSync(aboutDir)) return null;
|
|
46
|
-
for (const f of readdirSync(aboutDir)) {
|
|
47
|
-
if (f.endsWith(".md") || f.endsWith(".mdx"))
|
|
48
|
-
return readFileSync(join(aboutDir, f), "utf-8");
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── Words ──────────────────────────────────────────────
|
|
54
|
-
if (subsetId === "words") {
|
|
55
|
-
const wordsDir = join(contentRoot, "words");
|
|
56
|
-
if (!existsSync(wordsDir)) return null;
|
|
57
|
-
const all: string[] = [];
|
|
58
|
-
for (const f of readdirSync(wordsDir)) {
|
|
59
|
-
if (f.endsWith(".md") && !f.startsWith("_")) {
|
|
60
|
-
all.push(readFileSync(join(wordsDir, f), "utf-8"));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return all.join("\n") || null;
|
|
64
|
-
}
|
|
65
|
-
|
|
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
|
-
// ── Home page ───────────────────────────────────────────
|
|
94
|
-
if (subsetId === "home") {
|
|
95
|
-
return getAllConfigContent(contentRoot);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ── Links page ──────────────────────────────────────────
|
|
99
|
-
if (subsetId === "links") {
|
|
100
|
-
return getAllConfigContent(contentRoot);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Other HTML routes (common) ──
|
|
104
|
-
// The common subset includes UI text from source + config YAML
|
|
105
|
-
if (subsetId === "common") {
|
|
106
|
-
return getAllConfigContent(contentRoot);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Get all config YAML content */
|
|
113
|
-
function getAllConfigContent(contentRoot: string): string | null {
|
|
114
|
-
const configDir = join(contentRoot, "config");
|
|
115
|
-
if (!existsSync(configDir)) return null;
|
|
116
|
-
const all: string[] = [];
|
|
117
|
-
for (const f of readdirSync(configDir)) {
|
|
118
|
-
if (f.endsWith(".yml") || f.endsWith(".yaml")) {
|
|
119
|
-
all.push(readFileSync(join(configDir, f), "utf-8"));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return all.join("\n") || null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Get all post content concatenated (including template/_ files) */
|
|
126
|
-
function getAllPostContent(contentRoot: string): string | null {
|
|
127
|
-
const postsDir = join(contentRoot, "posts");
|
|
128
|
-
if (!existsSync(postsDir)) return null;
|
|
129
|
-
const all: string[] = [];
|
|
130
|
-
for (const f of readdirSync(postsDir)) {
|
|
131
|
-
if (f.endsWith(".md") || f.endsWith(".mdx")) {
|
|
132
|
-
all.push(readFileSync(join(postsDir, f), "utf-8"));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return all.join("\n") || null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Get all tags + all post content */
|
|
139
|
-
function getAllTagsAndPostContent(contentRoot: string): string | null {
|
|
140
|
-
const postContent = getAllPostContent(contentRoot);
|
|
141
|
-
if (!postContent) return null;
|
|
142
|
-
const tags = extractAllYamlValues(postContent, "tags");
|
|
143
|
-
return postContent + " " + tags.join(" ");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Get all categories + all post content */
|
|
147
|
-
function getAllCategoriesAndPostContent(contentRoot: string): string | null {
|
|
148
|
-
const postContent = getAllPostContent(contentRoot);
|
|
149
|
-
if (!postContent) return null;
|
|
150
|
-
const categories = extractAllYamlValues(postContent, "categories");
|
|
151
|
-
return postContent + " " + categories.join(" ");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Get content for a specific tag: tag name + posts with that tag */
|
|
155
|
-
function getTagContent(contentRoot: string, tagName: string): string | null {
|
|
156
|
-
const postsDir = join(contentRoot, "posts");
|
|
157
|
-
if (!existsSync(postsDir)) return null;
|
|
158
|
-
const all: string[] = [tagName];
|
|
159
|
-
for (const f of readdirSync(postsDir)) {
|
|
160
|
-
if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
|
|
161
|
-
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
162
|
-
const tags = extractYamlListFromContent(content, "tags");
|
|
163
|
-
if (tags.includes(tagName)) {
|
|
164
|
-
all.push(content);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return all.join("\n") || null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Get content for a specific category: category name + posts in that category */
|
|
172
|
-
function getCategoryContent(contentRoot: string, catName: string): string | null {
|
|
173
|
-
const postsDir = join(contentRoot, "posts");
|
|
174
|
-
if (!existsSync(postsDir)) return null;
|
|
175
|
-
const all: string[] = [catName];
|
|
176
|
-
for (const f of readdirSync(postsDir)) {
|
|
177
|
-
if ((f.endsWith(".md") || f.endsWith(".mdx")) && !f.startsWith("_")) {
|
|
178
|
-
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
179
|
-
const categories = extractYamlListFromContent(content, "categories");
|
|
180
|
-
if (categories.includes(catName)) {
|
|
181
|
-
all.push(content);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return all.join("\n") || null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Extract a YAML list value from frontmatter by key */
|
|
189
|
-
function extractYamlListFromContent(content: string, key: string): string[] {
|
|
190
|
-
const lines: string[] = [];
|
|
191
|
-
const regex = new RegExp(`^${key}:$`, "m");
|
|
192
|
-
const match = content.match(regex);
|
|
193
|
-
if (!match) return [];
|
|
194
|
-
const startIdx = match.index! + match[0].length;
|
|
195
|
-
const afterKey = content.slice(startIdx);
|
|
196
|
-
const listMatch = afterKey.match(/^\n((?:\s+-\s+.+\n?)*)/);
|
|
197
|
-
if (!listMatch) return [];
|
|
198
|
-
for (const line of listMatch[1].split("\n")) {
|
|
199
|
-
const item = line.match(/^\s+-\s+(.+)/)?.[1];
|
|
200
|
-
if (item) lines.push(item.trim());
|
|
201
|
-
}
|
|
202
|
-
return lines;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Extract all YAML list values from all posts by key */
|
|
206
|
-
function extractAllYamlValues(postContent: string, key: string): string[] {
|
|
207
|
-
const values = new Set<string>();
|
|
208
|
-
// Split by frontmatter boundaries (---)
|
|
209
|
-
const parts = postContent.split(/^---$/m);
|
|
210
|
-
for (let i = 1; i < parts.length; i += 2) {
|
|
211
|
-
const fm = parts[i];
|
|
212
|
-
const list = extractYamlListFromContent("---\n" + fm + "\n---\n", key);
|
|
213
|
-
for (const v of list) values.add(v);
|
|
214
|
-
}
|
|
215
|
-
return [...values];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Generate and serve a font subset on demand.
|
|
220
|
-
* Returns the CSS content to be served.
|
|
221
|
-
*/
|
|
222
|
-
async function generateSubsetOnDemand(
|
|
223
|
-
projectRoot: string,
|
|
224
|
-
subsetId: string,
|
|
225
|
-
logger: AstroIntegrationLogger,
|
|
226
|
-
): Promise<string | null> {
|
|
227
|
-
const chars = getRouteChars(projectRoot, subsetId);
|
|
228
|
-
if (!chars) return null;
|
|
229
|
-
|
|
230
|
-
// Extract unique characters
|
|
231
|
-
const charSet = new Set<string>();
|
|
232
|
-
for (const ch of chars) charSet.add(ch);
|
|
233
|
-
if (charSet.size === 0) return null;
|
|
234
|
-
|
|
235
|
-
// Locate font file
|
|
236
|
-
const fontPaths = [
|
|
237
|
-
resolve(projectRoot, FONT_INPUT),
|
|
238
|
-
resolve(projectRoot, "node_modules", "@xingwangzhe", "stalux", FONT_INPUT),
|
|
239
|
-
resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", FONT_INPUT),
|
|
240
|
-
];
|
|
241
|
-
let fontPath: string | undefined;
|
|
242
|
-
for (const p of fontPaths) {
|
|
243
|
-
if (existsSync(p)) {
|
|
244
|
-
fontPath = p;
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (!fontPath) return null;
|
|
249
|
-
|
|
250
|
-
const fontBuffer = readFileSync(fontPath);
|
|
251
|
-
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
252
|
-
mkdirSync(outDir, { recursive: true });
|
|
253
|
-
|
|
254
|
-
// Generate subset TTF using subset-font (WASM)
|
|
255
|
-
const charsStr = [...charSet].sort().join("");
|
|
256
|
-
const hash = hashStr(charsStr);
|
|
257
|
-
const filename = `subset-${subsetId}-${hash}.ttf`;
|
|
258
|
-
const outPath = join(outDir, filename);
|
|
259
|
-
|
|
260
|
-
if (!existsSync(outPath)) {
|
|
261
|
-
try {
|
|
262
|
-
const result = await subsetFont(fontBuffer, charsStr, {
|
|
263
|
-
targetFormat: "sfnt",
|
|
264
|
-
});
|
|
265
|
-
writeFileSync(outPath, result);
|
|
266
|
-
logger.info(
|
|
267
|
-
` on-demand subset: ${(result.length / 1024).toFixed(1)} KB → ${filename}`,
|
|
268
|
-
);
|
|
269
|
-
} catch {
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return `@font-face {
|
|
275
|
-
font-family: "LXGW WenKai Subset";
|
|
276
|
-
src: url("/fonts/${filename}") format("truetype");
|
|
277
|
-
font-display: swap;
|
|
278
|
-
}`;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/** Simple hash function for strings */
|
|
282
|
-
function hashStr(s: string): string {
|
|
283
|
-
let hash = 0;
|
|
284
|
-
for (let i = 0; i < s.length && i < 100; i++) {
|
|
285
|
-
const ch = s.charCodeAt(i);
|
|
286
|
-
hash = (hash << 5) - hash + ch;
|
|
287
|
-
hash |= 0;
|
|
288
|
-
}
|
|
289
|
-
return Math.abs(hash).toString(36).slice(0, 8);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Create Vite middleware for on-demand font subsetting in dev mode.
|
|
294
|
-
*/
|
|
295
|
-
export function createDevFontMiddleware(projectRoot: string, logger: AstroIntegrationLogger) {
|
|
296
|
-
return async (req: any, res: any, next: any) => {
|
|
297
|
-
const url = req.url || "";
|
|
298
|
-
const pathname = url.split("?")[0].split("#")[0];
|
|
299
|
-
const match = pathname.match(/^\/fonts\/subset-(.+)\.css$/);
|
|
300
|
-
if (!match) return next();
|
|
301
|
-
|
|
302
|
-
const subsetId = match[1];
|
|
303
|
-
|
|
304
|
-
// Check if file already exists (cached from previous on-demand generation)
|
|
305
|
-
const cssPath = resolve(projectRoot, FONT_OUT_DIR, `subset-${subsetId}.css`);
|
|
306
|
-
if (existsSync(cssPath)) {
|
|
307
|
-
res.statusCode = 200;
|
|
308
|
-
res.setHeader("Content-Type", "text/css");
|
|
309
|
-
res.end(readFileSync(cssPath, "utf-8"));
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Generate on-demand
|
|
314
|
-
logger.info(`Font subset: on-demand generating for "${subsetId}"...`);
|
|
315
|
-
const css = await generateSubsetOnDemand(projectRoot, subsetId, logger);
|
|
316
|
-
if (css) {
|
|
317
|
-
writeFileSync(cssPath, css); // cache it
|
|
318
|
-
res.statusCode = 200;
|
|
319
|
-
res.setHeader("Content-Type", "text/css");
|
|
320
|
-
res.end(css);
|
|
321
|
-
} else {
|
|
322
|
-
next();
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
}
|