@xingwangzhe/stalux 1.17.0 → 1.18.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -90,6 +90,7 @@
90
90
  "astro-seo": "^1.1.0",
91
91
  "astro-typewriter": "^1.2.0",
92
92
  "badge-maker": "^6.0.0",
93
+ "js-yaml": "^5.2.3",
93
94
  "pagefind": "^1.5.2",
94
95
  "photoswipe": "^5.4.4",
95
96
  "satteri": "^0.9.5",
@@ -27,6 +27,10 @@ import { resolve, basename, extname, join, dirname } from "node:path";
27
27
  import { fileURLToPath } from "node:url";
28
28
 
29
29
  import type { AstroIntegrationLogger } from "astro";
30
+ // 与 Astro 内容集合同款的 YAML 解析器(astro 依赖 js-yaml 解析 frontmatter,
31
+ // 见 node_modules/astro/dist/content/loaders/file.js)。用它解析 frontmatter
32
+ // 得到的结果与 getCollection 的 data 一致:引号剥离、类型还原、结构完整。
33
+ import { load as parseYaml } from "js-yaml";
30
34
  import subsetFont from "subset-font";
31
35
 
32
36
  // ---------------------------------------------------------------------------
@@ -100,6 +104,43 @@ interface ContentFile {
100
104
  abbrlink?: string;
101
105
  }
102
106
 
107
+ /**
108
+ * 用 js-yaml(内容集合同款解析器)解析 frontmatter。
109
+ *
110
+ * 结果与 getCollection("posts") 的 data 一致:
111
+ * - abbrlink:number 转 string、引号剥离(YAML 解析天然处理)
112
+ * - tags/categories:单行字符串 "Bing" 按 schema preprocess 转 ["Bing"]
113
+ */
114
+ function parsePostFrontmatter(content: string): {
115
+ abbrlink?: string;
116
+ tags: string[];
117
+ categories: string[];
118
+ } {
119
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
120
+ if (!m) return { abbrlink: undefined, tags: [], categories: [] };
121
+
122
+ let data: Record<string, unknown>;
123
+ try {
124
+ data = (parseYaml(m[1]) as Record<string, unknown>) ?? {};
125
+ } catch {
126
+ return { abbrlink: undefined, tags: [], categories: [] };
127
+ }
128
+
129
+ // 复刻 posts schema 的 preprocess:单行字符串转数组
130
+ const toList = (v: unknown): string[] => {
131
+ if (Array.isArray(v)) return v.map((x) => String(x));
132
+ if (typeof v === "string") return v.trim() ? [v.trim()] : [];
133
+ return [];
134
+ };
135
+
136
+ const abbr = data.abbrlink;
137
+ return {
138
+ abbrlink: abbr === undefined || abbr === null ? undefined : String(abbr),
139
+ tags: toList(data.tags),
140
+ categories: toList(data.categories),
141
+ };
142
+ }
143
+
103
144
  function scanContent(projectRoot: string): ContentFile[] {
104
145
  const files: ContentFile[] = [];
105
146
  const contentRoot = resolve(projectRoot, CONTENT_ROOT);
@@ -117,11 +158,11 @@ function scanContent(projectRoot: string): ContentFile[] {
117
158
  if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
118
159
  const content = readFileSync(join(postsDir, f), "utf-8");
119
160
  const isTemplate = f.startsWith("_");
161
+ const fm = parsePostFrontmatter(content);
120
162
 
121
163
  // Per-post routes: skip template (_) files
122
164
  if (!isTemplate) {
123
- const abbrlinkMatch = content.match(/^abbrlink:\s*(.+)$/m);
124
- const abbrlink = abbrlinkMatch?.[1]?.trim() ?? basename(f, extname(f));
165
+ const abbrlink = fm.abbrlink ?? basename(f, extname(f));
125
166
  files.push({
126
167
  route: `posts/${abbrlink}`,
127
168
  filePath: join(postsDir, f),
@@ -131,11 +172,13 @@ function scanContent(projectRoot: string): ContentFile[] {
131
172
 
132
173
  // Virtual routes (archives/tags/categories): include ALL posts including templates
133
174
  // because content collections load them and they render on those pages
134
- const abbrlinkMatch = content.match(/^abbrlink:\s*(.+)$/m);
135
- const abbrlink = abbrlinkMatch?.[1]?.trim() ?? basename(f, extname(f));
136
- const tags = extractYamlList(content, "tags");
137
- const categories = extractYamlList(content, "categories");
138
- allPostContents.push({ abbrlink, tags, categories, content });
175
+ const abbrlink = fm.abbrlink ?? basename(f, extname(f));
176
+ allPostContents.push({
177
+ abbrlink,
178
+ tags: fm.tags,
179
+ categories: fm.categories,
180
+ content,
181
+ });
139
182
  }
140
183
  }
141
184
 
@@ -251,23 +294,6 @@ function scanContent(projectRoot: string): ContentFile[] {
251
294
  return files;
252
295
  }
253
296
 
254
- /** Extract a YAML list value from frontmatter by key */
255
- function extractYamlList(content: string, key: string): string[] {
256
- const lines: string[] = [];
257
- const regex = new RegExp(`^${key}:$`, "m");
258
- const match = content.match(regex);
259
- if (!match) return [];
260
- const startIdx = match.index! + match[0].length;
261
- const afterKey = content.slice(startIdx);
262
- const listMatch = afterKey.match(/^\n((?:\s+-\s+.+\n?)*)/);
263
- if (!listMatch) return [];
264
- for (const line of listMatch[1].split("\n")) {
265
- const item = line.match(/^\s+-\s+(.+)/)?.[1];
266
- if (item) lines.push(item.trim());
267
- }
268
- return lines;
269
- }
270
-
271
297
  // Cache for virtual route char sets
272
298
  const wordCharsCache = new Map<string, string>();
273
299
  const virtualRouteCache = new Map<string, string>();