@windcoder/astro-theme-orchid 0.0.0-stage → 0.1.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.
Files changed (82) hide show
  1. package/README.md +311 -2
  2. package/package.json +68 -3
  3. package/src/components/AlbumCard.astro +73 -0
  4. package/src/components/Analytics.astro +22 -0
  5. package/src/components/BaseLayout.astro +188 -0
  6. package/src/components/Calendar.astro +379 -0
  7. package/src/components/CategoryBar.astro +74 -0
  8. package/src/components/Comment.astro +48 -0
  9. package/src/components/EncryptedContent.astro +42 -0
  10. package/src/components/Footer.astro +31 -0
  11. package/src/components/Header.astro +153 -0
  12. package/src/components/License.astro +29 -0
  13. package/src/components/Live2DWidget.astro +262 -0
  14. package/src/components/MusicPlayer.astro +940 -0
  15. package/src/components/OutdatedCard.astro +53 -0
  16. package/src/components/Pagination.astro +94 -0
  17. package/src/components/PhotoCard.astro +22 -0
  18. package/src/components/PostCard.astro +89 -0
  19. package/src/components/PostNav.astro +40 -0
  20. package/src/components/RecommendedPost.astro +166 -0
  21. package/src/components/SakuraEffect.astro +152 -0
  22. package/src/components/Sidebar.astro +334 -0
  23. package/src/components/Toc.astro +27 -0
  24. package/src/components/analytics/GoogleAnalytics.astro +22 -0
  25. package/src/components/analytics/La51Analytics.astro +52 -0
  26. package/src/components/analytics/MicrosoftClarity.astro +18 -0
  27. package/src/components/analytics/UmamiAnalytics.astro +86 -0
  28. package/src/components/comments/Artalk.astro +51 -0
  29. package/src/components/comments/Disqus.astro +43 -0
  30. package/src/components/comments/Giscus.astro +50 -0
  31. package/src/components/comments/Twikoo.astro +65 -0
  32. package/src/components/comments/Waline.astro +36 -0
  33. package/src/i18n/en.ts +164 -0
  34. package/src/i18n/index.ts +18 -0
  35. package/src/i18n/resolve.ts +49 -0
  36. package/src/i18n/zh-CN.ts +184 -0
  37. package/src/icons.ts +71 -0
  38. package/src/index.ts +406 -0
  39. package/src/plugins/plantuml-encoder.ts +95 -0
  40. package/src/plugins/rehype-diagram.ts +218 -0
  41. package/src/plugins/rehype-email.ts +75 -0
  42. package/src/plugins/rehype-github-card.ts +114 -0
  43. package/src/plugins/rehype.ts +284 -0
  44. package/src/plugins/remark-diagram.ts +78 -0
  45. package/src/plugins/remark-directives.ts +100 -0
  46. package/src/plugins/remark-excerpt.ts +29 -0
  47. package/src/plugins/remark-image-grid.ts +97 -0
  48. package/src/plugins/remark-reading-time.ts +38 -0
  49. package/src/plugins/scripts/diagram-panzoom.js +388 -0
  50. package/src/plugins/scripts/plantuml-theme-switch.js +90 -0
  51. package/src/routes/404.astro +14 -0
  52. package/src/routes/_hydrate.ts +34 -0
  53. package/src/routes/about.astro +25 -0
  54. package/src/routes/api/allPostMeta.json.ts +26 -0
  55. package/src/routes/archive.astro +81 -0
  56. package/src/routes/categories/[category].astro +37 -0
  57. package/src/routes/categories/index.astro +29 -0
  58. package/src/routes/friends.astro +83 -0
  59. package/src/routes/gallery/[album].astro +97 -0
  60. package/src/routes/gallery/index.astro +100 -0
  61. package/src/routes/guestbook.astro +37 -0
  62. package/src/routes/index.astro +47 -0
  63. package/src/routes/og/[...slug].ts +281 -0
  64. package/src/routes/page/[page].astro +53 -0
  65. package/src/routes/posts/[...slug].astro +337 -0
  66. package/src/routes/robots.txt.ts +18 -0
  67. package/src/routes/rss.xml.ts +37 -0
  68. package/src/routes/series/[name].astro +63 -0
  69. package/src/routes/series/index.astro +64 -0
  70. package/src/routes/sponsor.astro +84 -0
  71. package/src/routes/tags/[tag].astro +40 -0
  72. package/src/routes/tags/index.astro +29 -0
  73. package/src/schema.ts +1212 -0
  74. package/src/scripts/app.ts +1041 -0
  75. package/src/scripts/share-poster.ts +468 -0
  76. package/src/styles/global.css +4830 -0
  77. package/src/utils/crypto.ts +46 -0
  78. package/src/utils/gallery.ts +76 -0
  79. package/src/utils/lqip.ts +18 -0
  80. package/src/utils/related.ts +101 -0
  81. package/src/utils.ts +181 -0
  82. package/src/vite.ts +105 -0
package/src/index.ts ADDED
@@ -0,0 +1,406 @@
1
+ // unified 处理器:Astro 7 默认 Sätteri 原生管线不执行 JS remark/rehype 插件,
2
+ // 必须显式切回 unified 处理器,插件数组直接传入 unified({...})。
3
+ // 铁律:数组元素一律是 attacher(插件工厂函数本身),带参数的用 [plugin, options]
4
+ // 元组;绝不能 push 已调用的 transformer(如 remarkMermaid()),否则 unified freeze
5
+ // 时会把它当 attacher 二次调用(tree=undefined)导致渲染崩溃,详见下方装配区注释
6
+ import { unified } from "@astrojs/markdown-remark";
7
+ // RSS / Sitemap(§4.63,Astro 生态标准方案):不再手写 XML 生成路由,
8
+ // sitemap 以 integration 形式挂载(由 defineTheme 一并返回),rss 仍走端点路由
9
+ import sitemap from "@astrojs/sitemap";
10
+ import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
11
+ import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
12
+ import tailwindcss from "@tailwindcss/vite";
13
+ import type { AstroIntegration } from "astro";
14
+ // Astro Fonts API(§4.66)官方 provider 工厂:google/fontsource/local/bunny/
15
+ // fontshare/npm 六种,全部支持无参调用(local 的文件声明走 family options.variants)
16
+ import { fontProviders } from "astro/config";
17
+ // astro-icon(§4.65):Iconify 标准图标生态的官方 Astro integration,
18
+ // 供 Sidebar 资料卡等组件以 <Icon name="集:名"> 构建期内联渲染图标
19
+ import icon from "astro-icon";
20
+ // mhchem 化学宏:import 时刻把 \ce{...} 等宏注册进 katex 实例。
21
+ // pnpm-workspace.yaml overrides 已把 rehype-katex 等统一到同一 katex 版本,
22
+ // 共享单实例,故 mhchem 宏对 rehype-katex 的渲染直接生效
23
+ import "katex/dist/contrib/mhchem.mjs";
24
+ // EC 社区插件:超长代码块自动折叠(fold.enabled 开启时挂载)
25
+ import pluginCollapsible from "expressive-code-collapsible";
26
+ import { pluginLanguageBadge } from "expressive-code-language-badge";
27
+ // Expressive Code(§4.61,Astro 生态标准方案):rehype 内核层接入。
28
+ // 注意不能用 astro-expressive-code integration:其 updateConfig append 的
29
+ // rehype 插件会被本主题 unified processor 的插件数组覆盖(processor 优先级
30
+ // 高于 config 层),必须直接 push 进主题自己的插件数组(与 katex/mermaid 同模式)
31
+ import rehypeExpressiveCode from "rehype-expressive-code";
32
+ import rehypeKatex from "rehype-katex";
33
+ // 官方解析器:把 :::container / ::leaf / :text 语法解析为 directive 节点,
34
+ // 供 remarkDirectives 后处理(缺它则指令按字面文本渲染,卡片/容器全部失效)
35
+ import remarkDirective from "remark-directive";
36
+ import remarkMath from "remark-math";
37
+ import { resolveLabel } from "./i18n/resolve.js";
38
+ import {
39
+ rehypeAnchor,
40
+ rehypeCallouts,
41
+ rehypeExternalLinks,
42
+ rehypeFigure,
43
+ rehypeFirstImage,
44
+ } from "./plugins/rehype.js";
45
+ import {
46
+ rehypeDiagramPanZoom,
47
+ rehypeMermaid,
48
+ rehypePlantuml,
49
+ } from "./plugins/rehype-diagram.js";
50
+ import { rehypeEmailProtection } from "./plugins/rehype-email.js";
51
+ import { rehypeGithubCard } from "./plugins/rehype-github-card.js";
52
+ import { remarkMermaid, remarkPlantuml } from "./plugins/remark-diagram.js";
53
+ import { remarkDirectives } from "./plugins/remark-directives.js";
54
+ import { remarkExcerpt } from "./plugins/remark-excerpt.js";
55
+ import { remarkImageGrid } from "./plugins/remark-image-grid.js";
56
+ import { remarkReadingTime } from "./plugins/remark-reading-time.js";
57
+ import { type ThemeConfig, themeConfigSchema } from "./schema.js";
58
+ import {
59
+ overridesPlugin,
60
+ themeConfigPlugin,
61
+ themeLqipsPlugin,
62
+ } from "./vite.js";
63
+
64
+ export const PKG_NAME = "@windcoder/astro-theme-orchid";
65
+
66
+ /**
67
+ * 主题入口:以 Astro Integration 形式接入站点
68
+ *
69
+ * 返回 integration 数组(Astro 对 integrations 配置里的嵌套数组自动展平,
70
+ * 站点侧仍写 integrations: [defineTheme(cfg)] 即可):
71
+ * - 核心 integration:injectRoute 注入主题路由(首页 / 文章页),entrypoint 指向
72
+ * 包内文件,站点无需复制任何页面;updateConfig 注入 Tailwind v4 Vite 插件 +
73
+ * 虚拟模块(config)+ overrides 解析;injectTypes(astro:config:done)为虚拟模块提供类型声明
74
+ * - @astrojs/sitemap:官方 sitemap(构建末尾产出 sitemap-index.xml + 分片),
75
+ * 替代原手写 /sitemap.xml 路由
76
+ *
77
+ * 升级主题 = 升级本包版本号;站点定制走 src/overrides/ 同名覆盖,不会被升级覆盖。
78
+ */
79
+ export function defineTheme(userConfig: ThemeConfig): AstroIntegration[] {
80
+ // 启动即校验配置,配置错误在 dev/build 一开始就报出来
81
+ const config = themeConfigSchema.parse(userConfig);
82
+
83
+ const core: AstroIntegration = {
84
+ name: PKG_NAME,
85
+ hooks: {
86
+ "astro:config:setup": ({
87
+ config: astroConfig,
88
+ injectRoute,
89
+ updateConfig,
90
+ }) => {
91
+ // remark:阅读时长/字数 + 自动摘要(渲染期写入 remarkPluginFrontmatter)
92
+ // LaTeX(b4):remark-math 识别公式 → rehype-katex 构建期渲染(无客户端运行时)
93
+ // Mermaid/PlantUML(b4):remark 标记代码块 → rehype 构建期产出(静态 SVG / 外链 img)
94
+ // 邮箱防爬(b4):mailto 构建期编码 + 点击时内联解码
95
+ // rehype 顺序:内容变换在前 → katex → 图表 → pan-zoom 兜底 → 邮箱防爬 →
96
+ // Expressive Code 代码块渲染最后(math/mermaid/plantuml pre 已被前面的
97
+ // 插件整体替换,EC 只接管剩余普通代码块)
98
+ // Astro 7 接线方式:
99
+ // - 默认 Sätteri 处理器不执行 JS 插件,须显式 processor: unified({...})
100
+ // (integration 更新 processor 后 content store 不自动清除,
101
+ // 重启 dev 前删 site/.astro/ 可排除陈旧缓存干扰)
102
+ // - unified 的铁律:插件数组里的元素一律是 attacher(插件工厂函数本身),
103
+ // 带参数的用 [plugin, options] 元组(loadPlugins 展开 → parser.use(plugin, opts))。
104
+ // 千万不能 push remarkMermaid() 这种"已调用的 transformer"——unified freeze 时
105
+ // 会把数组元素当 attacher 再调用一次(第一参数 tree=undefined),
106
+ // 报 "Cannot read properties of undefined (reading 'children')",且与文档内容无关
107
+ // 代码高亮(§4.61):Expressive Code 接管(Astro 生态标准方案),
108
+ // markdown.syntaxHighlight 必须置 false——Astro 原生 shiki 高亮先于用户
109
+ // rehype 插件执行,不关掉会出现 shiki + EC 双重高亮冲突
110
+ const remarkPlugins: any[] = [
111
+ remarkReadingTime,
112
+ remarkExcerpt,
113
+ // [grid]…[/grid] 图片网格(§4.10):聚合图片 → div.image-grid[data-cols=N]
114
+ remarkImageGrid,
115
+ // directive 处理链(§4.10,三段缺一不可):
116
+ // ① remarkDirective 官方解析器::::note / ::github 语法 → directive 节点
117
+ // ② remarkDirectives 后处理:26 种 admonition 容器 → blockquote +
118
+ // [!TYPE] 标记(复用 rehypeCallouts 渲染);其余指令 → 同名自定义标签
119
+ // ③ rehypeGithubCard:github 标签 → 仓库卡片
120
+ remarkDirective,
121
+ remarkDirectives,
122
+ ];
123
+ const rehypePlugins: any[] = [
124
+ // GitHub 仓库卡片(§4.10):github 标签 → 卡片(自带 target,须在外链处理前)
125
+ rehypeGithubCard,
126
+ rehypeExternalLinks,
127
+ rehypeCallouts,
128
+ rehypeFigure,
129
+ rehypeFirstImage,
130
+ rehypeAnchor,
131
+ ];
132
+ if (config.latex.enabled) {
133
+ remarkPlugins.push(remarkMath);
134
+ // 不传 { katex } 选项:rehype-katex 7.x 不支持实例覆盖(模块级 import),
135
+ // settings 会原样展开进 renderToString,传了也是无效选项
136
+ rehypePlugins.push(rehypeKatex);
137
+ }
138
+ if (config.diagram.mermaid.enabled) {
139
+ remarkPlugins.push(remarkMermaid);
140
+ rehypePlugins.push([
141
+ rehypeMermaid,
142
+ {
143
+ lightTheme: config.diagram.mermaid.lightTheme,
144
+ darkTheme: config.diagram.mermaid.darkTheme,
145
+ },
146
+ ]);
147
+ }
148
+ if (config.diagram.plantuml.enabled) {
149
+ remarkPlugins.push([
150
+ remarkPlantuml,
151
+ {
152
+ server: config.diagram.plantuml.server,
153
+ lightTheme: config.diagram.plantuml.lightTheme,
154
+ darkTheme: config.diagram.plantuml.darkTheme,
155
+ },
156
+ ]);
157
+ rehypePlugins.push(rehypePlantuml);
158
+ }
159
+ if (config.diagram.mermaid.enabled || config.diagram.plantuml.enabled) {
160
+ // 兜底:任一图表形态存在时确保 pan-zoom 脚本注入(每 tree 一次)
161
+ rehypePlugins.push(rehypeDiagramPanZoom);
162
+ }
163
+ if (config.emailProtection.enabled) {
164
+ rehypePlugins.push([
165
+ rehypeEmailProtection,
166
+ { method: config.emailProtection.method },
167
+ ]);
168
+ }
169
+ // Expressive Code(§4.61):必须最后 push——math/mermaid/plantuml pre 已被
170
+ // 前面的插件整体替换,EC 只接管剩余普通代码块。
171
+ // 双主题:EC 约定 themes [dark, light] 顺序;useDarkModeMediaQuery 关掉系统
172
+ // 媒体查询,改用 themeCssSelector 映射站点深浅类(html.dark,与 app.ts 切换同源)。
173
+ // 插件组合对齐参考站:collapsible-sections(meta collapse={} 标记折叠)恒挂、
174
+ // line-numbers 恒挂(defaultProps.showLineNumbers 控制显隐)、社区 collapsible
175
+ // 自动折叠按 fold.enabled、语言徽章按 languageBadge。
176
+ // 复制按钮文案(copyButtonTooltip 等)走 EC 内置 texts,仅 en/de 内置翻译、
177
+ // 无公开 zh 注册入口,保持 EC 默认(对齐参考站做法)。
178
+ rehypePlugins.push([
179
+ rehypeExpressiveCode,
180
+ {
181
+ themes: [config.codeTheme.dark, config.codeTheme.light],
182
+ useDarkModeMediaQuery: false,
183
+ themeCssSelector: (theme: { type: string }) =>
184
+ theme.type === "dark" ? "html.dark" : "html:not(.dark)",
185
+ defaultLocale: config.lang,
186
+ plugins: [
187
+ ...(config.codeTheme.languageBadge
188
+ ? [pluginLanguageBadge()]
189
+ : []),
190
+ pluginCollapsibleSections(),
191
+ pluginLineNumbers(),
192
+ ...(config.codeTheme.fold.enabled
193
+ ? [
194
+ pluginCollapsible({
195
+ lineThreshold: config.codeTheme.fold.lineThreshold,
196
+ previewLines: config.codeTheme.fold.previewLines,
197
+ defaultCollapsed: config.codeTheme.fold.defaultCollapsed,
198
+ // 折叠按钮与读屏公告文案跟随站点语言
199
+ expandButtonText: resolveLabel(
200
+ config.lang,
201
+ "codeFoldExpand",
202
+ ),
203
+ collapseButtonText: resolveLabel(
204
+ config.lang,
205
+ "codeFoldCollapse",
206
+ ),
207
+ expandedAnnouncement: resolveLabel(
208
+ config.lang,
209
+ "codeFoldExpanded",
210
+ ),
211
+ collapsedAnnouncement: resolveLabel(
212
+ config.lang,
213
+ "codeFoldCollapsed",
214
+ ),
215
+ }),
216
+ ]
217
+ : []),
218
+ ],
219
+ defaultProps: {
220
+ showLineNumbers: config.codeTheme.lineNumbers,
221
+ wrap: config.codeTheme.wrap,
222
+ // 终端会话块不显示行号(对齐参考站)
223
+ overridesByLang: { shellsession: { showLineNumbers: false } },
224
+ },
225
+ // 站点级视觉对齐参考站:字体变量用本主题的 --font-mono
226
+ styleOverrides: {
227
+ borderRadius: "0.75rem",
228
+ codeFontSize: "0.875rem",
229
+ codeFontFamily:
230
+ "var(--font-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
231
+ codeLineHeight: "1.5rem",
232
+ textMarkers: { delHue: 0, insHue: 180, markHue: 250 },
233
+ languageBadge: {
234
+ fontSize: "0.75rem",
235
+ fontWeight: "bold",
236
+ borderRadius: "0.25rem",
237
+ opacity: "1",
238
+ borderWidth: "0px",
239
+ borderColor: "transparent",
240
+ },
241
+ },
242
+ frames: { showCopyToClipboardButton: true },
243
+ },
244
+ ]);
245
+ updateConfig({
246
+ // 主题路由全部按尾斜杠组织(如 /posts/xxx/),统一强制,dev 与 prod 行为一致
247
+ trailingSlash: "always",
248
+ vite: {
249
+ plugins: [tailwindcss()],
250
+ // 构建优化说明(§4.63):参考站的 vite.build 块(minify:"esbuild"、
251
+ // esbuildOptions、cssMinify:"esbuild" 等)是 vite 5 时代写法——Astro 7
252
+ // 所用 vite 8(rolldown)里 esbuild 仅为可选 peer(默认不装),
253
+ // esbuildOptions 已是死配置、cssMinify:"esbuild" 会直接构建失败;
254
+ // 而 JS 压缩(oxc)、CSS 压缩(lightningcss)、cssCodeSplit(true)、
255
+ // assetsInlineLimit(4096) 在 vite 8 均已是默认值,无需重复设置
256
+ },
257
+ markdown: {
258
+ processor: unified({ remarkPlugins, rehypePlugins }),
259
+ // 关掉 Astro 原生 shiki:高亮由 EC 独占(原生 shiki 先于用户
260
+ // rehype 插件执行)
261
+ syntaxHighlight: false,
262
+ },
263
+ });
264
+ // 响应式图像(§4.64):顶层 image.layout 经 content-layer 传给
265
+ // processor.createRenderer({ image }),unified processor 以
266
+ // { ...shared } 全量透传进 createMarkdownProcessor(图像插件
267
+ // remarkCollectImages/rehypeImages 由其内部无条件装配),最终运行期
268
+ // 图像服务按 layout 为 md 本地图片产出 srcset/sizes。
269
+ // constrained 与参考站一致:限宽缩放 + 默认断点多尺寸变体;
270
+ // 仅在站点未显式配置 image.layout 时注入,不覆盖用户选择
271
+ if (!astroConfig.image.layout) {
272
+ updateConfig({ image: { layout: "constrained" } });
273
+ }
274
+ // 字体(§4.66,Astro Fonts API 标准方案):schema 声明 → astro 顶层
275
+ // fonts 配置。站点自有 fonts 在前、主题配置追加在后;preload/role 是
276
+ // 主题层字段(分别由 BaseLayout 的 <Font> 与角色映射 CSS 消费),
277
+ // 显式挑字段映射即天然剥离;provider 字符串经官方工厂转 provider 对象。
278
+ // 未配置时完全不注入,零开销
279
+ if (config.fonts.length > 0) {
280
+ const themeFonts = config.fonts.map((f) => {
281
+ const font: Record<string, unknown> = {
282
+ name: f.name,
283
+ cssVariable: f.cssVariable,
284
+ weights: f.weights,
285
+ styles: f.styles,
286
+ subsets: f.subsets,
287
+ fallbacks: f.fallbacks,
288
+ provider: fontProviders[f.provider](),
289
+ };
290
+ if (f.display) font.display = f.display;
291
+ if (f.options?.variants) {
292
+ font.options = { variants: f.options.variants };
293
+ }
294
+ return font;
295
+ });
296
+ updateConfig({
297
+ fonts: [...(astroConfig.fonts ?? []), ...themeFonts],
298
+ });
299
+ }
300
+
301
+ const routes: [pattern: string, file: string][] = [
302
+ ["/", "index.astro"],
303
+ ["/page/[page]", "page/[page].astro"],
304
+ ["/archive", "archive.astro"],
305
+ ["/tags", "tags/index.astro"],
306
+ ["/tags/[tag]", "tags/[tag].astro"],
307
+ ["/categories", "categories/index.astro"],
308
+ ["/categories/[category]", "categories/[category].astro"],
309
+ ["/posts/[...slug]", "posts/[...slug].astro"],
310
+ // sitemap(§4.63):改由官方 @astrojs/sitemap integration 产出
311
+ // sitemap-index.xml,原手写 /sitemap.xml 路由移除
312
+ ["/rss.xml", "rss.xml.ts"],
313
+ ["/robots.txt", "robots.txt.ts"],
314
+ ["/404", "404.astro"],
315
+ ];
316
+ // 独立页按开关注入(关闭则完全不生成对应路由,而非运行期 404 跳转)
317
+ if (config.pages.about) routes.push(["/about", "about.astro"]);
318
+ if (config.pages.friends) routes.push(["/friends", "friends.astro"]);
319
+ if (config.pages.guestbook)
320
+ routes.push(["/guestbook", "guestbook.astro"]);
321
+ if (config.pages.sponsor) routes.push(["/sponsor", "sponsor.astro"]);
322
+ if (config.pages.gallery) {
323
+ routes.push(["/gallery", "gallery/index.astro"]);
324
+ routes.push(["/gallery/[album]", "gallery/[album].astro"]);
325
+ }
326
+ // 专题(§4.31):frontmatter series 聚合页与专题详情页
327
+ if (config.pages.series) {
328
+ routes.push(["/series", "series/index.astro"]);
329
+ routes.push(["/series/[name]", "series/[name].astro"]);
330
+ }
331
+ // 相关/随机文章推荐(P1-12):随机栏数据源
332
+ if (config.relatedPosts.enabled) {
333
+ routes.push(["/api/allPostMeta.json", "api/allPostMeta.json.ts"]);
334
+ }
335
+ // OG 分享图(P1-11):每篇文章一张分享图,默认关
336
+ if (config.og.enabled) {
337
+ routes.push(["/og/[...slug]", "og/[...slug].ts"]);
338
+ }
339
+ for (const [pattern, file] of routes) {
340
+ injectRoute({
341
+ pattern,
342
+ entrypoint: `${PKG_NAME}/routes/${file}`,
343
+ prerender: true,
344
+ });
345
+ }
346
+
347
+ updateConfig({
348
+ // themeLqipsPlugin 必须在 overridesPlugin 前(后者兜底所有 virtual:theme/*,
349
+ // 会抢走 virtual:theme/lqips 的解析,详见 vite.ts 注释)
350
+ vite: {
351
+ plugins: [
352
+ themeConfigPlugin(config),
353
+ themeLqipsPlugin(),
354
+ overridesPlugin(),
355
+ ],
356
+ },
357
+ });
358
+ },
359
+ // astro 7 中 injectTypes 只在 astro:config:done 上提供
360
+ "astro:config:done": ({ injectTypes }) => {
361
+ injectTypes({
362
+ filename: `${PKG_NAME}.d.ts`,
363
+ content: [
364
+ `declare module 'virtual:theme/config' {`,
365
+ ` const config: import('${PKG_NAME}/schema').ResolvedThemeConfig;`,
366
+ ` export default config;`,
367
+ `}`,
368
+ `declare module 'virtual:theme/lqips' {`,
369
+ ` const lqips: Record<string, string>;`,
370
+ ` export default lqips;`,
371
+ `}`,
372
+ `declare module 'virtual:theme/*' {`,
373
+ ` const Component: any;`,
374
+ ` export default Component;`,
375
+ `}`,
376
+ ].join("\n"),
377
+ });
378
+ },
379
+ },
380
+ };
381
+
382
+ // sitemap(§4.63):官方 integration 在 astro:build:done 统一扫描全部预渲染页面
383
+ // 产出 sitemap-index.xml + 分片。filter 排除非内容端点:
384
+ // - /og/:文章分享图(二进制图片,不该进 sitemap)
385
+ // - /api/:随机文章数据端点(JSON)
386
+ // 关闭的独立页路由根本不注入,无需在此过滤
387
+ const integrations: AstroIntegration[] = [
388
+ core,
389
+ sitemap({
390
+ filter: (page) => {
391
+ const { pathname } = new URL(page);
392
+ return !pathname.startsWith("/og/") && !pathname.startsWith("/api/");
393
+ },
394
+ }),
395
+ // astro-icon(§4.65):不配 include,保持开放——静态站图标数据由 vite 插件
396
+ // 按需从已装 @iconify-json/* 读取、实际只内联用到的 SVG,无产物膨胀;
397
+ // 站点补装任意 @iconify-json/* 集合包即可在 icon 字段直接填 "集:名" 扩展
398
+ icon(),
399
+ ];
400
+ return integrations;
401
+ }
402
+
403
+ export default defineTheme;
404
+
405
+ // 供站点侧配置文件做类型标注与检查(含 provider 等字面量联合)
406
+ export type { ThemeConfig } from "./schema";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * PlantUML 服务器编码工具(M3b-b4,移植自 orchid-blog)
3
+ *
4
+ * PlantUML 文本编码三步:UTF-8 字节 → raw DEFLATE(无 zlib 头)→
5
+ * PlantUML 自定义 base64 字母表(`0-9A-Za-z-_`,无 `=` 填充)。
6
+ * 结果可直接拼到服务器 `/svg/`、`/png/` 路径之后。
7
+ * @see https://plantuml.com/text-encoding
8
+ */
9
+ import * as pako from "pako";
10
+
11
+ const PLANTUML_ALPHABET =
12
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
13
+
14
+ function encode6bit(value: number) {
15
+ return PLANTUML_ALPHABET.charAt(value & 0x3f);
16
+ }
17
+
18
+ function append3bytes(b1: number, b2: number, b3: number) {
19
+ const c1 = b1 >> 2;
20
+ const c2 = ((b1 & 0x3) << 4) | (b2 >> 4);
21
+ const c3 = ((b2 & 0xf) << 2) | (b3 >> 6);
22
+ const c4 = b3 & 0x3f;
23
+ return encode6bit(c1) + encode6bit(c2) + encode6bit(c3) + encode6bit(c4);
24
+ }
25
+
26
+ function encode64(bytes: Uint8Array) {
27
+ let result = "";
28
+ const length = bytes.length;
29
+ for (let i = 0; i < length; i += 3) {
30
+ if (i + 2 === length) {
31
+ result += append3bytes(bytes[i], bytes[i + 1], 0);
32
+ } else if (i + 1 === length) {
33
+ result += append3bytes(bytes[i], 0, 0);
34
+ } else {
35
+ result += append3bytes(bytes[i], bytes[i + 1], bytes[i + 2]);
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+
41
+ /** PlantUML 专用编码:UTF-8 → deflateRaw(level 9) → 自定义 base64 */
42
+ export function encodePlantUML(source: string) {
43
+ if (typeof source !== "string") {
44
+ throw new TypeError(
45
+ `encodePlantUML expects a string, got ${typeof source}`,
46
+ );
47
+ }
48
+ const utf8Bytes = new TextEncoder().encode(source);
49
+ const deflated = pako.deflateRaw(utf8Bytes, { level: 9 });
50
+ return encode64(deflated);
51
+ }
52
+
53
+ /** 源码已含 `!theme` 或 `skinparam backgroundColor` 时尊重作者显式设置,不再注入 */
54
+ function hasExplicitTheme(source: string) {
55
+ return (
56
+ /^\s*!theme\s+\S+/m.test(source) ||
57
+ /^\s*skinparam\s+backgroundColor\b/im.test(source)
58
+ );
59
+ }
60
+
61
+ /**
62
+ * 在源码头部注入 `!theme <name>`:
63
+ * - 主题名为空 → 原样返回
64
+ * - 已有显式主题/背景色 → 原样返回
65
+ * - 首行是 `@startuml` → 该行之后插入
66
+ * - 无 `@startuml` → 源码最前面插入
67
+ */
68
+ export function injectTheme(source: string, themeName: string) {
69
+ if (typeof source !== "string") {
70
+ throw new TypeError(
71
+ `injectTheme expects a string source, got ${typeof source}`,
72
+ );
73
+ }
74
+ if (!themeName?.trim()) return source;
75
+ if (hasExplicitTheme(source)) return source;
76
+ const themeDirective = `!theme ${themeName.trim()}`;
77
+ const startumlMatch = source.match(/^[^\S\r\n]*@startuml[^\r\n]*\r?\n?/);
78
+ if (startumlMatch) {
79
+ const insertAt = (startumlMatch.index ?? 0) + startumlMatch[0].length;
80
+ return `${source.slice(0, insertAt)}${themeDirective}\n${source.slice(insertAt)}`;
81
+ }
82
+ return `${themeDirective}\n${source}`;
83
+ }
84
+
85
+ /** 拼接 `<server>/svg/<encoded>`,自动去 server 尾部斜杠 */
86
+ export function buildUrl(server: string, encoded: string) {
87
+ if (typeof server !== "string" || !server.trim()) {
88
+ throw new TypeError("buildUrl expects a non-empty server string");
89
+ }
90
+ if (typeof encoded !== "string" || !encoded.trim()) {
91
+ throw new TypeError("buildUrl expects a non-empty encoded string");
92
+ }
93
+ const normalizedServer = server.replace(/\/+$/, "");
94
+ return `${normalizedServer}/svg/${encoded}`;
95
+ }