@xingwangzhe/stalux 1.23.3 → 1.24.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/README.md +20 -17
- package/package.json +1 -1
- package/src/components/stalux/posts/postContent.astro +53 -19
- package/src/config.ts +15 -0
- package/src/expressive-code.ts +1 -1
- package/src/index.ts +59 -9
package/README.md
CHANGED
|
@@ -23,33 +23,35 @@ bun add @xingwangzhe/stalux # Install theme (all dependencies included)
|
|
|
23
23
|
bunx stalux init # Generate stalux/ content directory
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Then configure `astro.config.mjs`.
|
|
26
|
+
Then configure `astro.config.mjs`. All plugins are bundled into the Stalux integration by default — **no manual configuration is needed**:
|
|
27
|
+
|
|
28
|
+
- **Markdown**: Mermaid (MDAST detection + HAST/SVG rendering), math formulas (Temml → MathML), word count / feature flags, and PhotoSwipe image lightbox are injected into the default `satteri()` processor automatically (math / frontmatter / gfm / smart punctuation are enabled by default).
|
|
29
|
+
- **Sitemap**: `@astrojs/sitemap` is bundled (`.md` source endpoints are filtered out by default).
|
|
30
|
+
- **Expressive Code**: bundled with line numbers enabled by default.
|
|
27
31
|
|
|
28
32
|
```ts
|
|
29
33
|
import { defineConfig } from "astro/config";
|
|
30
|
-
import { satteri } from "@astrojs/markdown-satteri";
|
|
31
|
-
import sitemap from "@astrojs/sitemap";
|
|
32
|
-
import expressiveCode from "astro-expressive-code";
|
|
33
34
|
import stalux from "@xingwangzhe/stalux";
|
|
34
|
-
import { photoswipe } from "@xingwangzhe/satteri-photoswipe";
|
|
35
35
|
|
|
36
36
|
export default defineConfig({
|
|
37
37
|
output: "static",
|
|
38
38
|
site: "https://example.com",
|
|
39
|
-
integrations: [
|
|
40
|
-
stalux({ contentDir: "stalux" }),
|
|
41
|
-
sitemap(),
|
|
42
|
-
expressiveCode({ themes: ["dark-plus", "github-light"] }),
|
|
43
|
-
],
|
|
44
|
-
markdown: {
|
|
45
|
-
processor: satteri({
|
|
46
|
-
features: { math: true, smartPunctuation: true, gfm: true, frontmatter: true },
|
|
47
|
-
hastPlugins: [photoswipe()],
|
|
48
|
-
}),
|
|
49
|
-
},
|
|
39
|
+
integrations: [stalux({ contentDir: "stalux" })],
|
|
50
40
|
});
|
|
51
41
|
```
|
|
52
42
|
|
|
43
|
+
To customize the bundled integrations, pass options (or disable them with `false`):
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
integrations: [
|
|
47
|
+
stalux({
|
|
48
|
+
contentDir: "stalux",
|
|
49
|
+
sitemap: { filter: (page) => page.startsWith("https://example.com/posts/") },
|
|
50
|
+
expressiveCode: { themes: ["dark-plus", "github-light"] },
|
|
51
|
+
}),
|
|
52
|
+
];
|
|
53
|
+
```
|
|
54
|
+
|
|
53
55
|
Create `src/content.config.ts`:
|
|
54
56
|
|
|
55
57
|
```ts
|
|
@@ -78,9 +80,10 @@ bun run dev
|
|
|
78
80
|
- 🔤 **Unicode-range font slicing** — 25 MB font → ~22 woff2 chunks, browser downloads only what matches the page
|
|
79
81
|
- 🔍 **Full-text search** via Pagefind (auto-indexed on build)
|
|
80
82
|
- 📡 **RSS & Atom feeds**
|
|
83
|
+
- 🗺️ **Sitemap** (bundled; `.md` source endpoints auto-filtered)
|
|
81
84
|
- 🖼️ **PhotoSwipe** image lightbox
|
|
82
85
|
- 📊 **Mermaid** diagrams and flowcharts
|
|
83
|
-
- 📐 **Math formula rendering** (
|
|
86
|
+
- 📐 **Math formula rendering** (Temml → MathML)
|
|
84
87
|
- 💬 **Waline** comment system
|
|
85
88
|
- 🤖 **LLM discovery files** (llms.txt / llms-full.txt)
|
|
86
89
|
- 🤝 **WebMCP tools** for AI agents (W3C draft, pure front-end)
|
package/package.json
CHANGED
|
@@ -140,7 +140,14 @@ const mdSourceUrl = exportMd
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
// 状态放 window:View Transitions 软导航会重执行模块脚本,
|
|
144
|
+
// 用全局标记避免 document 级监听器重复注册导致灯箱重复初始化。
|
|
145
|
+
const state = (window.__staluxPhotoswipe ||= {
|
|
146
|
+
lightbox: null,
|
|
147
|
+
initPromise: null,
|
|
148
|
+
lightboxInitialized: false,
|
|
149
|
+
handlerInstalled: false,
|
|
150
|
+
});
|
|
144
151
|
|
|
145
152
|
async function initLightbox() {
|
|
146
153
|
const [{ default: PhotoSwipeLightbox }, { default: PhotoSwipe }, { default: photoSwipeCss }] = await Promise.all([
|
|
@@ -159,45 +166,72 @@ const mdSourceUrl = exportMd
|
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
// 销毁旧实例(View Transition 导航后,旧 DOM 已移除,需安全销毁)
|
|
162
|
-
if (lightbox) {
|
|
169
|
+
if (state.lightbox) {
|
|
163
170
|
try {
|
|
164
|
-
lightbox.destroy();
|
|
171
|
+
state.lightbox.destroy();
|
|
165
172
|
} catch {
|
|
166
173
|
/* ignore */
|
|
167
174
|
}
|
|
168
|
-
lightbox = null;
|
|
175
|
+
state.lightbox = null;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
// 不阻塞文章首屏;已加载图片会立即写入尺寸,其余图片在后台补齐。
|
|
172
179
|
void enrichPhotoSwipeData();
|
|
173
180
|
|
|
174
|
-
lightbox = new PhotoSwipeLightbox({
|
|
181
|
+
state.lightbox = new PhotoSwipeLightbox({
|
|
175
182
|
gallery: "[data-pagefind-body]",
|
|
176
183
|
children: GALLERY_SELECTOR,
|
|
177
184
|
pswpModule: PhotoSwipe,
|
|
178
185
|
initialZoomLevel: "fill",
|
|
179
186
|
});
|
|
180
|
-
lightbox.init();
|
|
187
|
+
state.lightbox.init();
|
|
181
188
|
}
|
|
182
189
|
|
|
183
|
-
let initPromise = null;
|
|
184
190
|
const ensureLightbox = () => {
|
|
185
|
-
if (!initPromise) initPromise = initLightbox();
|
|
186
|
-
return initPromise;
|
|
191
|
+
if (!state.initPromise) state.initPromise = initLightbox();
|
|
192
|
+
return state.initPromise;
|
|
187
193
|
};
|
|
188
194
|
|
|
189
|
-
|
|
190
|
-
// 文章首屏不再为未使用的灯箱支付 JS/CSS 成本。
|
|
191
|
-
document.addEventListener("click", (event) => {
|
|
195
|
+
function handleGalleryClick(event) {
|
|
192
196
|
const target = event.target;
|
|
193
|
-
if (target instanceof Element && target.closest(GALLERY_SELECTOR))
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
if (!(target instanceof Element && target.closest(GALLERY_SELECTOR))) return;
|
|
198
|
+
|
|
199
|
+
// 灯箱就绪后,后续点击交给 PhotoSwipeLightbox 自身的
|
|
200
|
+
// bubble 阶段处理(它负责 preventDefault + loadAndOpen)。
|
|
201
|
+
if (state.lightboxInitialized) return;
|
|
202
|
+
|
|
203
|
+
// 首次点击:立即阻止浏览器跳转到原始图片(否则会离开文章页),
|
|
204
|
+
// 异步初始化完成后直接打开被点击的图片。
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
const anchor = target.closest(GALLERY_SELECTOR);
|
|
207
|
+
const clickedIndex = [...document.querySelectorAll(GALLERY_SELECTOR)].indexOf(anchor);
|
|
208
|
+
const initialPoint = { x: event.clientX, y: event.clientY };
|
|
209
|
+
|
|
210
|
+
void ensureLightbox()
|
|
211
|
+
.then(() => {
|
|
212
|
+
state.lightboxInitialized = true;
|
|
213
|
+
if (state.lightbox) {
|
|
214
|
+
state.lightbox.loadAndOpen(clickedIndex, undefined, initialPoint);
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
.catch((error) => {
|
|
218
|
+
console.error("[stalux photoswipe] 初始化失败", error);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
197
221
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
222
|
+
if (!state.handlerInstalled) {
|
|
223
|
+
state.handlerInstalled = true;
|
|
224
|
+
|
|
225
|
+
// 仅在用户第一次点击图片时加载 PhotoSwipe、其 CSS 和图片尺寸数据,
|
|
226
|
+
// 文章首屏不再为未使用的灯箱支付 JS/CSS 成本。
|
|
227
|
+
document.addEventListener("click", handleGalleryClick, { capture: true });
|
|
228
|
+
|
|
229
|
+
// 软导航后旧 DOM 已移除:下次点击重新初始化(initLightbox 会销毁旧实例)。
|
|
230
|
+
document.addEventListener("astro:page-load", () => {
|
|
231
|
+
state.initPromise = null;
|
|
232
|
+
state.lightboxInitialized = false;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
201
235
|
</script>
|
|
202
236
|
)
|
|
203
237
|
}
|
package/src/config.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* 用户在 astro.config.mjs 中传入这些选项来自定义主题行为。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { SitemapOptions } from "@astrojs/sitemap";
|
|
8
|
+
|
|
9
|
+
import type { ExpressiveCodeOptions } from "./expressive-code";
|
|
7
10
|
import type { StaluxComponentKey } from "./internal/override";
|
|
8
11
|
|
|
9
12
|
/** 用户可覆盖的组件映射 */
|
|
@@ -55,4 +58,16 @@ export interface StaluxOptions {
|
|
|
55
58
|
|
|
56
59
|
/** 站点 URL(默认从 astro.config 读取) */
|
|
57
60
|
site?: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 是否自动打包 @astrojs/sitemap(默认 true)。
|
|
64
|
+
* 传 false 关闭;传对象自定义选项(filter 会与默认的 .md 源码端点过滤叠加)。
|
|
65
|
+
*/
|
|
66
|
+
sitemap?: boolean | SitemapOptions;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 是否自动打包 Expressive Code(默认 true,带代码块行号插件)。
|
|
70
|
+
* 传 false 关闭;传对象自定义主题/样式等选项。
|
|
71
|
+
*/
|
|
72
|
+
expressiveCode?: boolean | ExpressiveCodeOptions;
|
|
58
73
|
}
|
package/src/expressive-code.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
|
|
18
18
|
import expressiveCodeBase from "astro-expressive-code";
|
|
19
19
|
|
|
20
|
-
type ExpressiveCodeOptions = NonNullable<Parameters<typeof expressiveCodeBase>[0]>;
|
|
20
|
+
export type ExpressiveCodeOptions = NonNullable<Parameters<typeof expressiveCodeBase>[0]>;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* 创建带默认行号插件的 Expressive Code 集成。
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,9 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
|
|
15
|
+
import sitemap from "@astrojs/sitemap";
|
|
15
16
|
import { mermaidMdast, mermaidHast } from "@xingwangzhe/satteri-mermaid";
|
|
17
|
+
import { photoswipe } from "@xingwangzhe/satteri-photoswipe";
|
|
16
18
|
import type { AstroIntegration } from "astro";
|
|
17
19
|
import { fontProviders } from "astro/config";
|
|
18
20
|
// pagefind 是 ESM-only 包,需要在模块顶层导入
|
|
@@ -20,6 +22,7 @@ import { fontProviders } from "astro/config";
|
|
|
20
22
|
import { createIndex as pagefindCreateIndex } from "pagefind";
|
|
21
23
|
|
|
22
24
|
import type { StaluxOptions } from "./config";
|
|
25
|
+
import { expressiveCode } from "./expressive-code";
|
|
23
26
|
import { staluxComponentsAlias } from "./internal/components-plugin";
|
|
24
27
|
import { runFontSlicing, type FontSlice } from "./internal/font-slices";
|
|
25
28
|
import { featureFlagsHast, featureFlagsMdast } from "./plugins/feature-flags";
|
|
@@ -213,11 +216,13 @@ function getViteAliases(srcDir: string) {
|
|
|
213
216
|
* });
|
|
214
217
|
* ```
|
|
215
218
|
*/
|
|
216
|
-
export function stalux(options: StaluxOptions = {}): AstroIntegration {
|
|
219
|
+
export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
217
220
|
const opt: StaluxOptions = {
|
|
218
221
|
contentDir: options.contentDir ?? "stalux",
|
|
219
222
|
pagefind: options.pagefind ?? true,
|
|
220
223
|
devToolbar: options.devToolbar ?? true,
|
|
224
|
+
sitemap: options.sitemap ?? true,
|
|
225
|
+
expressiveCode: options.expressiveCode ?? true,
|
|
221
226
|
...options,
|
|
222
227
|
};
|
|
223
228
|
|
|
@@ -225,7 +230,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration {
|
|
|
225
230
|
// 安装在 node_modules 中时,页面的文件路由不可用,需要通过 injectRoute 注入
|
|
226
231
|
const isPluginMode = import.meta.url.includes("node_modules");
|
|
227
232
|
|
|
228
|
-
|
|
233
|
+
const coreIntegration: AstroIntegration = {
|
|
229
234
|
name: "stalux",
|
|
230
235
|
hooks: {
|
|
231
236
|
"astro:config:setup": async ({
|
|
@@ -297,16 +302,33 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration {
|
|
|
297
302
|
|
|
298
303
|
logger.info(`Stalux initialized (contentDir: ${opt.contentDir})`);
|
|
299
304
|
|
|
300
|
-
// 5. 注入 satteri 插件(Mermaid
|
|
301
|
-
//
|
|
305
|
+
// 5. 注入 satteri 插件(Mermaid/字数统计/特性标记/数学公式/PhotoSwipe),按插件 name 去重
|
|
306
|
+
// 两种模式都由集成补齐默认插件。Astro 7 的 markdown.processor 默认为 satteri(),
|
|
307
|
+
// 消费方即使完全不配置 processor,这里也会对默认 processor 的 options 做幂等合并:
|
|
308
|
+
// - features:默认开启 math / frontmatter / gfm / smartPunctuation(用户显式 false 则尊重)
|
|
309
|
+
// - mdastPlugins:mermaidMdast + temml(数学公式)+ featureFlagsMdast(字数统计)
|
|
310
|
+
// - hastPlugins:photoswipe(图片灯箱)+ mermaidHast + featureFlagsHast
|
|
302
311
|
// 去重是必须的:重复注册 Mermaid 或 featureFlagsMdast 会导致渲染异常或字数统计翻倍。
|
|
303
312
|
try {
|
|
304
313
|
const processor = (config.markdown?.processor ?? undefined) as
|
|
305
|
-
| {
|
|
314
|
+
| {
|
|
315
|
+
name?: string;
|
|
316
|
+
options?: {
|
|
317
|
+
mdastPlugins?: any[];
|
|
318
|
+
hastPlugins?: any[];
|
|
319
|
+
features?: Record<string, unknown>;
|
|
320
|
+
};
|
|
321
|
+
}
|
|
306
322
|
| undefined;
|
|
307
323
|
if (processor?.name === "satteri") {
|
|
308
|
-
const
|
|
309
|
-
const
|
|
324
|
+
const options = processor.options ?? {};
|
|
325
|
+
const mdastPlugins = (options.mdastPlugins ??= []);
|
|
326
|
+
const hastPlugins = (options.hastPlugins ??= []);
|
|
327
|
+
const features = (options.features ??= {});
|
|
328
|
+
// 默认开启 math / frontmatter / gfm / smartPunctuation
|
|
329
|
+
for (const key of ["math", "frontmatter", "gfm", "smartPunctuation"]) {
|
|
330
|
+
if (features[key] !== false) features[key] = true;
|
|
331
|
+
}
|
|
310
332
|
const seen = new Set<string>();
|
|
311
333
|
for (const p of [...mdastPlugins, ...hastPlugins]) {
|
|
312
334
|
if (p?.name) seen.add(p.name);
|
|
@@ -329,13 +351,14 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration {
|
|
|
329
351
|
themeOverrides: { clusterBorder: "#cccccc" },
|
|
330
352
|
}),
|
|
331
353
|
);
|
|
354
|
+
pushUnique(hastPlugins, photoswipe());
|
|
332
355
|
pushUnique(hastPlugins, featureFlagsHast);
|
|
333
356
|
logger.debug(
|
|
334
|
-
"Stalux: injected satteri plugins (mermaid/temml/feature-flags)",
|
|
357
|
+
"Stalux: injected satteri plugins (mermaid/temml/photoswipe/feature-flags)",
|
|
335
358
|
);
|
|
336
359
|
} else {
|
|
337
360
|
logger.warn(
|
|
338
|
-
"Stalux: markdown.processor 不是 satteri
|
|
361
|
+
"Stalux: markdown.processor 不是 satteri,无法注入字数统计/数学公式/PhotoSwipe 插件。" +
|
|
339
362
|
"请在 astro.config 中配置 `processor: satteri({...})`。",
|
|
340
363
|
);
|
|
341
364
|
}
|
|
@@ -447,6 +470,33 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration {
|
|
|
447
470
|
},
|
|
448
471
|
},
|
|
449
472
|
};
|
|
473
|
+
|
|
474
|
+
// 打包内置集成:Astro 的 integrations 配置支持嵌套数组并自动展平(schema 里 val.flat(Infinity)),
|
|
475
|
+
// 因此这里返回 [coreIntegration, sitemap, expressiveCode],让消费方一行 `integrations: [stalux()]` 即可完成配置。
|
|
476
|
+
const bundled: AstroIntegration[] = [coreIntegration];
|
|
477
|
+
|
|
478
|
+
if (opt.sitemap !== false) {
|
|
479
|
+
const userSitemap = opt.sitemap === true ? undefined : opt.sitemap;
|
|
480
|
+
// 默认过滤:不把 Markdown 源码端点(/posts/*.md)写入 sitemap;用户自定义 filter 与之叠加
|
|
481
|
+
const defaultFilter = (page: string) => !page.endsWith(".md");
|
|
482
|
+
const userFilter = userSitemap?.filter;
|
|
483
|
+
const filter = userFilter
|
|
484
|
+
? (page: string) => defaultFilter(page) && userFilter(page)
|
|
485
|
+
: defaultFilter;
|
|
486
|
+
bundled.push(
|
|
487
|
+
sitemap({
|
|
488
|
+
...userSitemap,
|
|
489
|
+
filter,
|
|
490
|
+
}),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (opt.expressiveCode !== false) {
|
|
495
|
+
const ecOptions = opt.expressiveCode === true ? undefined : opt.expressiveCode;
|
|
496
|
+
bundled.push(expressiveCode(ecOptions));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return bundled;
|
|
450
500
|
}
|
|
451
501
|
|
|
452
502
|
export default stalux;
|