@xingwangzhe/stalux 1.6.0 → 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 +1 -1
- package/src/internal/font-subset-dev.ts +122 -2
- package/src/internal/font-subset.ts +97 -7
- 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
|
@@ -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.
|
|
@@ -103,19 +103,81 @@ function scanContent(projectRoot: string): ContentFile[] {
|
|
|
103
103
|
|
|
104
104
|
// Posts
|
|
105
105
|
const postsDir = join(contentRoot, "posts");
|
|
106
|
+
const allPostContents: {
|
|
107
|
+
abbrlink: string;
|
|
108
|
+
tags: string[];
|
|
109
|
+
categories: string[];
|
|
110
|
+
content: string;
|
|
111
|
+
}[] = [];
|
|
106
112
|
if (existsSync(postsDir)) {
|
|
107
113
|
for (const f of readdirSync(postsDir)) {
|
|
108
114
|
if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
|
|
109
|
-
if (f.startsWith("_")) continue; // skip template files
|
|
110
115
|
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
111
|
-
|
|
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
|
|
112
131
|
const abbrlinkMatch = content.match(/^abbrlink:\s*(.+)$/m);
|
|
113
132
|
const abbrlink = abbrlinkMatch?.[1]?.trim() ?? basename(f, extname(f));
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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));
|
|
119
181
|
}
|
|
120
182
|
}
|
|
121
183
|
|
|
@@ -156,11 +218,37 @@ function scanContent(projectRoot: string): ContentFile[] {
|
|
|
156
218
|
}
|
|
157
219
|
}
|
|
158
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
|
+
|
|
159
229
|
return files;
|
|
160
230
|
}
|
|
161
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
|
+
|
|
162
249
|
// Cache for virtual route char sets
|
|
163
250
|
const wordCharsCache = new Map<string, string>();
|
|
251
|
+
const virtualRouteCache = new Map<string, string>();
|
|
164
252
|
|
|
165
253
|
// ---------------------------------------------------------------------------
|
|
166
254
|
// Common subset: extract chars from UI source files
|
|
@@ -311,6 +399,8 @@ export async function runFontSubsetting(
|
|
|
311
399
|
let content: string;
|
|
312
400
|
if (file.route === "words" && wordCharsCache.has("words")) {
|
|
313
401
|
content = wordCharsCache.get("words")!;
|
|
402
|
+
} else if (virtualRouteCache.has(file.route)) {
|
|
403
|
+
content = virtualRouteCache.get(file.route)!;
|
|
314
404
|
} else {
|
|
315
405
|
content = readFileSync(file.filePath, "utf-8");
|
|
316
406
|
}
|
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 />
|