@xingwangzhe/stalux 1.17.0 → 1.19.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 +2 -1
- package/src/internal/font-subset.ts +50 -24
- package/src/layouts/Stalux.astro +4 -9
- package/src/scripts/background.ts +17 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.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
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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>();
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -36,9 +36,9 @@ const fontSubsetUrl = props.fontSubsetId
|
|
|
36
36
|
|
|
37
37
|
// 背景 SVG 以静态 URL 硬编码在 background.ts(纯客户端),
|
|
38
38
|
// 集成在 config:setup 时把 SVG 复制到用户项目 public/background/。
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
39
|
+
// SSR 不渲染背景图,默认露出 .bg-layer 的灰墙底色(--stalux-bg-color),
|
|
40
|
+
// 客户端运行时随机选图;不经过 import.meta.glob 资源管线,
|
|
41
|
+
// 保证增量构建依赖图 hash 稳定。
|
|
42
42
|
const llmPromoteRaw = promoteConfig?.llm_promote ?? "";
|
|
43
43
|
const llmPromote = llmPromoteRaw
|
|
44
44
|
?.replace(/\{author\}/g, authorConfig.name)
|
|
@@ -97,12 +97,7 @@ const llmPromote = llmPromoteRaw
|
|
|
97
97
|
data-stalux-bg-index="0"
|
|
98
98
|
>
|
|
99
99
|
{llmPromote && <div style="display:none">{llmPromote}</div>}
|
|
100
|
-
<div
|
|
101
|
-
class="bg-layer bg-a"
|
|
102
|
-
style={`background-image: url('${initialBg}'); opacity: 1;`}
|
|
103
|
-
aria-hidden="true"
|
|
104
|
-
>
|
|
105
|
-
</div>
|
|
100
|
+
<div class="bg-layer bg-a" style="opacity: 1;" aria-hidden="true"></div>
|
|
106
101
|
<div class="bg-layer bg-b" aria-hidden="true"></div>
|
|
107
102
|
<main class="stalux-main">
|
|
108
103
|
<slot />
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* 管线(import.meta.glob 会给 SVG 打上每次构建随机的 __ASTRO_ASSET_IMAGE__
|
|
7
7
|
* 占位符,破坏增量构建依赖图 hash),也不做任何构建期扫描。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 背景行为:
|
|
10
|
+
* - SSR 只渲染灰墙底色(--stalux-bg-color),不渲染任何背景图,构建产物确定。
|
|
11
|
+
* - 首次加载与 VT 软导航每次都会随机选一张背景:astro:page-load 在
|
|
12
|
+
* window load(首访)与 swap 完成后、过渡动画期间(软导航)触发,
|
|
13
|
+
* 此时设置背景,过渡动画中旧图淡出、新图淡入,自然 crossfade 无跳变。
|
|
10
14
|
*/
|
|
11
15
|
declare global {
|
|
12
16
|
interface Window {
|
|
@@ -34,11 +38,11 @@ function setLayerOpacity(layer: "a" | "b", opacity: number): void {
|
|
|
34
38
|
if (el) el.style.opacity = String(opacity);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
function
|
|
41
|
+
function applyBackground(): void {
|
|
38
42
|
// 客户端运行时随机选图,构建产物保持确定性
|
|
39
43
|
const index = Math.floor(Math.random() * backgroundImages.length);
|
|
40
44
|
const layer: "a" | "b" = "a";
|
|
41
|
-
if (backgroundImages.length > 0) {
|
|
45
|
+
if (backgroundImages.length > 0 && backgroundImages[index]) {
|
|
42
46
|
const el = getLayerEl(layer);
|
|
43
47
|
if (el) el.style.backgroundImage = `url('${backgroundImages[index]}')`;
|
|
44
48
|
}
|
|
@@ -48,16 +52,13 @@ function initBackground(): void {
|
|
|
48
52
|
document.body.dataset.staluxBgLayer = layer;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
initBackground();
|
|
62
|
-
}
|
|
63
|
-
});
|
|
55
|
+
// 首次加载与 VT 软导航都触发 astro:page-load:
|
|
56
|
+
// - 首次加载:window load 后触发(router.js addEventListener("load", onPageLoad))。
|
|
57
|
+
// - VT 软导航:swap 完成后、过渡动画期间触发(router.js onPageLoad()),
|
|
58
|
+
// 此时设置随机背景,过渡中旧图淡出、新图淡入,无跳变闪变。
|
|
59
|
+
// 注意:软导航不会重新执行打包的 module 脚本(监听器仍是首访注册的),
|
|
60
|
+
// 所以这里不能做“只执行一次”的去重,每次 page-load 都必须随机;
|
|
61
|
+
// 若软导航后新脚本被重新执行,重复监听导致随机两次也无妨(仍是随机图)。
|
|
62
|
+
// body 的 data-stalux-bg-* 是 SSR 固定占位(a/0,为了构建确定性),
|
|
63
|
+
// 不能再用它判断“服务端已渲染正确背景”,否则随机逻辑永不执行。
|
|
64
|
+
document.addEventListener("astro:page-load", applyBackground);
|