@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.
- package/README.md +311 -2
- package/package.json +68 -3
- package/src/components/AlbumCard.astro +73 -0
- package/src/components/Analytics.astro +22 -0
- package/src/components/BaseLayout.astro +188 -0
- package/src/components/Calendar.astro +379 -0
- package/src/components/CategoryBar.astro +74 -0
- package/src/components/Comment.astro +48 -0
- package/src/components/EncryptedContent.astro +42 -0
- package/src/components/Footer.astro +31 -0
- package/src/components/Header.astro +153 -0
- package/src/components/License.astro +29 -0
- package/src/components/Live2DWidget.astro +262 -0
- package/src/components/MusicPlayer.astro +940 -0
- package/src/components/OutdatedCard.astro +53 -0
- package/src/components/Pagination.astro +94 -0
- package/src/components/PhotoCard.astro +22 -0
- package/src/components/PostCard.astro +89 -0
- package/src/components/PostNav.astro +40 -0
- package/src/components/RecommendedPost.astro +166 -0
- package/src/components/SakuraEffect.astro +152 -0
- package/src/components/Sidebar.astro +334 -0
- package/src/components/Toc.astro +27 -0
- package/src/components/analytics/GoogleAnalytics.astro +22 -0
- package/src/components/analytics/La51Analytics.astro +52 -0
- package/src/components/analytics/MicrosoftClarity.astro +18 -0
- package/src/components/analytics/UmamiAnalytics.astro +86 -0
- package/src/components/comments/Artalk.astro +51 -0
- package/src/components/comments/Disqus.astro +43 -0
- package/src/components/comments/Giscus.astro +50 -0
- package/src/components/comments/Twikoo.astro +65 -0
- package/src/components/comments/Waline.astro +36 -0
- package/src/i18n/en.ts +164 -0
- package/src/i18n/index.ts +18 -0
- package/src/i18n/resolve.ts +49 -0
- package/src/i18n/zh-CN.ts +184 -0
- package/src/icons.ts +71 -0
- package/src/index.ts +406 -0
- package/src/plugins/plantuml-encoder.ts +95 -0
- package/src/plugins/rehype-diagram.ts +218 -0
- package/src/plugins/rehype-email.ts +75 -0
- package/src/plugins/rehype-github-card.ts +114 -0
- package/src/plugins/rehype.ts +284 -0
- package/src/plugins/remark-diagram.ts +78 -0
- package/src/plugins/remark-directives.ts +100 -0
- package/src/plugins/remark-excerpt.ts +29 -0
- package/src/plugins/remark-image-grid.ts +97 -0
- package/src/plugins/remark-reading-time.ts +38 -0
- package/src/plugins/scripts/diagram-panzoom.js +388 -0
- package/src/plugins/scripts/plantuml-theme-switch.js +90 -0
- package/src/routes/404.astro +14 -0
- package/src/routes/_hydrate.ts +34 -0
- package/src/routes/about.astro +25 -0
- package/src/routes/api/allPostMeta.json.ts +26 -0
- package/src/routes/archive.astro +81 -0
- package/src/routes/categories/[category].astro +37 -0
- package/src/routes/categories/index.astro +29 -0
- package/src/routes/friends.astro +83 -0
- package/src/routes/gallery/[album].astro +97 -0
- package/src/routes/gallery/index.astro +100 -0
- package/src/routes/guestbook.astro +37 -0
- package/src/routes/index.astro +47 -0
- package/src/routes/og/[...slug].ts +281 -0
- package/src/routes/page/[page].astro +53 -0
- package/src/routes/posts/[...slug].astro +337 -0
- package/src/routes/robots.txt.ts +18 -0
- package/src/routes/rss.xml.ts +37 -0
- package/src/routes/series/[name].astro +63 -0
- package/src/routes/series/index.astro +64 -0
- package/src/routes/sponsor.astro +84 -0
- package/src/routes/tags/[tag].astro +40 -0
- package/src/routes/tags/index.astro +29 -0
- package/src/schema.ts +1212 -0
- package/src/scripts/app.ts +1041 -0
- package/src/scripts/share-poster.ts +468 -0
- package/src/styles/global.css +4830 -0
- package/src/utils/crypto.ts +46 -0
- package/src/utils/gallery.ts +76 -0
- package/src/utils/lqip.ts +18 -0
- package/src/utils/related.ts +101 -0
- package/src/utils.ts +181 -0
- package/src/vite.ts +105 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remark 图表标记插件(M3b-b4,移植自 orchid-blog)
|
|
3
|
+
*
|
|
4
|
+
* - remarkMermaid:```mermaid 代码块 → 自定义节点 type='mermaid',
|
|
5
|
+
* data.hName/hProperties 落为 <div class="mermaid-container" data-mermaid-code="…">,
|
|
6
|
+
* hChildren 保留源码文本(构建期渲染失败时可降级展示),value 置空避免重复输出
|
|
7
|
+
* - remarkPlantuml:```plantuml 代码块 → <div class="plantuml-wrapper">,
|
|
8
|
+
* 双主题 URL(injectTheme → pako deflate → PlantUML base64url)预编码进 data 属性,
|
|
9
|
+
* 构建期零请求,主题切换由客户端换 src 完成
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { buildUrl, encodePlantUML, injectTheme } from "./plantuml-encoder.js";
|
|
13
|
+
|
|
14
|
+
type Transformer = (tree: any, file: any) => void;
|
|
15
|
+
|
|
16
|
+
/** 深度遍历辅助(与 rehype.ts 内 visit 同风格),fn 返回 'skip' 跳过子树 */
|
|
17
|
+
function visit(node: any, fn: (n: any) => void | "skip") {
|
|
18
|
+
const children = node.children;
|
|
19
|
+
if (!Array.isArray(children)) return;
|
|
20
|
+
for (const child of children) {
|
|
21
|
+
if (fn(child) !== "skip") visit(child, fn);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function remarkMermaid(): Transformer {
|
|
26
|
+
return (tree) => {
|
|
27
|
+
visit(tree, (node) => {
|
|
28
|
+
if (node.type !== "code") return;
|
|
29
|
+
if (String(node.lang ?? "").toLowerCase() !== "mermaid") return;
|
|
30
|
+
const code = String(node.value ?? "");
|
|
31
|
+
node.type = "mermaid";
|
|
32
|
+
node.data = { ...node.data };
|
|
33
|
+
node.data.hName = "div";
|
|
34
|
+
node.data.hProperties = {
|
|
35
|
+
className: ["mermaid-container"],
|
|
36
|
+
"data-mermaid-code": code,
|
|
37
|
+
};
|
|
38
|
+
node.data.hChildren = [{ type: "text", value: code }];
|
|
39
|
+
node.value = undefined;
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PlantumlOptions {
|
|
45
|
+
/** PlantUML 渲染服务地址,如 https://www.plantuml.com/plantuml */
|
|
46
|
+
server: string;
|
|
47
|
+
/** 浅色主题名(空 = 不注入 !theme,尊重图内已有声明) */
|
|
48
|
+
lightTheme?: string;
|
|
49
|
+
/** 深色主题名(空 = 复用浅色 URL) */
|
|
50
|
+
darkTheme?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function remarkPlantuml(options: PlantumlOptions): Transformer {
|
|
54
|
+
return (tree) => {
|
|
55
|
+
visit(tree, (node) => {
|
|
56
|
+
if (node.type !== "code") return;
|
|
57
|
+
if (String(node.lang ?? "").toLowerCase() !== "plantuml") return;
|
|
58
|
+
const code = String(node.value ?? "");
|
|
59
|
+
const lightSource = injectTheme(code, options.lightTheme ?? "");
|
|
60
|
+
const darkSource = injectTheme(code, options.darkTheme ?? "");
|
|
61
|
+
const lightUrl = buildUrl(options.server, encodePlantUML(lightSource));
|
|
62
|
+
const darkUrl =
|
|
63
|
+
darkSource === lightSource
|
|
64
|
+
? lightUrl
|
|
65
|
+
: buildUrl(options.server, encodePlantUML(darkSource));
|
|
66
|
+
node.type = "plantuml";
|
|
67
|
+
node.data = { ...node.data };
|
|
68
|
+
node.data.hName = "div";
|
|
69
|
+
node.data.hProperties = {
|
|
70
|
+
className: ["plantuml-wrapper"],
|
|
71
|
+
"data-light-src": lightUrl,
|
|
72
|
+
"data-dark-src": darkUrl,
|
|
73
|
+
"data-plantuml-alt": code.slice(0, 200),
|
|
74
|
+
};
|
|
75
|
+
node.value = undefined;
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remark-directive 指令分发(A2/A3,移植自参考模板 remark-directive-rehype)
|
|
3
|
+
*
|
|
4
|
+
* 官方 remark-directive 将 :::container / ::leaf / :text 指令解析为三类
|
|
5
|
+
* directive 节点,本插件统一改写:
|
|
6
|
+
* - containerDirective + 26 种 admonition 类型 → blockquote,并在首段注入
|
|
7
|
+
* "[!TYPE] " 标记,复用既有 rehypeCallouts 渲染(自定义 label 融入标题)
|
|
8
|
+
* - 其余指令 → hastscript 转成同名自定义标签(如 ::github{repo=…} →
|
|
9
|
+
* <github repo="…">),交由对应 rehype 插件替换(rehypeGithubCard)
|
|
10
|
+
*/
|
|
11
|
+
import { h } from "hastscript";
|
|
12
|
+
|
|
13
|
+
/** 参考模板的 26 种 admonition 类型(GitHub 6 种 + Obsidian 扩充别名) */
|
|
14
|
+
const ADMONITION_TYPES = new Set([
|
|
15
|
+
"note",
|
|
16
|
+
"tip",
|
|
17
|
+
"important",
|
|
18
|
+
"warning",
|
|
19
|
+
"caution",
|
|
20
|
+
"abstract",
|
|
21
|
+
"summary",
|
|
22
|
+
"tldr",
|
|
23
|
+
"info",
|
|
24
|
+
"todo",
|
|
25
|
+
"success",
|
|
26
|
+
"check",
|
|
27
|
+
"done",
|
|
28
|
+
"question",
|
|
29
|
+
"help",
|
|
30
|
+
"faq",
|
|
31
|
+
"attention",
|
|
32
|
+
"failure",
|
|
33
|
+
"missing",
|
|
34
|
+
"fail",
|
|
35
|
+
"danger",
|
|
36
|
+
"error",
|
|
37
|
+
"bug",
|
|
38
|
+
"example",
|
|
39
|
+
"quote",
|
|
40
|
+
"cite",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export function remarkDirectives(): any {
|
|
44
|
+
const transform = (node: any) => {
|
|
45
|
+
if (
|
|
46
|
+
node.type !== "containerDirective" &&
|
|
47
|
+
node.type !== "leafDirective" &&
|
|
48
|
+
node.type !== "textDirective"
|
|
49
|
+
)
|
|
50
|
+
return;
|
|
51
|
+
const name = String(node.name ?? "").toLowerCase();
|
|
52
|
+
|
|
53
|
+
if (node.type === "containerDirective" && ADMONITION_TYPES.has(name)) {
|
|
54
|
+
const type = name.toUpperCase();
|
|
55
|
+
const firstChild = node.children[0];
|
|
56
|
+
if (firstChild?.data?.directiveLabel) {
|
|
57
|
+
// 自定义标题:标记注入 label 首文本,由 rehypeCallouts 提为标题
|
|
58
|
+
if (
|
|
59
|
+
firstChild.children.length > 0 &&
|
|
60
|
+
firstChild.children[0].type === "text"
|
|
61
|
+
) {
|
|
62
|
+
firstChild.children[0].value = `[!${type}] ${firstChild.children[0].value}`;
|
|
63
|
+
} else {
|
|
64
|
+
firstChild.children.unshift({
|
|
65
|
+
type: "text",
|
|
66
|
+
value: `[!${type}] `,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// 无标题:插入仅含标记的新段落
|
|
71
|
+
node.children.unshift({
|
|
72
|
+
type: "paragraph",
|
|
73
|
+
children: [{ type: "text", value: `[!${type}]` }],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// 关键:转 blockquote 且清除 hProperties,防止变成自定义标签
|
|
77
|
+
node.type = "blockquote";
|
|
78
|
+
node.data = node.data ?? {};
|
|
79
|
+
node.data.hName = "blockquote";
|
|
80
|
+
delete node.data.hProperties;
|
|
81
|
+
} else {
|
|
82
|
+
// 其他指令:转同名自定义标签(attributes 原样带入),样式缺省不可见,
|
|
83
|
+
// 由对应 rehype 插件负责替换
|
|
84
|
+
node.data = node.data ?? {};
|
|
85
|
+
const hast = h(name, node.attributes ?? {});
|
|
86
|
+
node.data.hName = hast.tagName;
|
|
87
|
+
node.data.hProperties = hast.properties;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const walk = (node: any) => {
|
|
92
|
+
if (!Array.isArray(node.children)) return;
|
|
93
|
+
for (const child of node.children) {
|
|
94
|
+
transform(child);
|
|
95
|
+
walk(child);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (tree: any) => walk(tree);
|
|
100
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remark 插件:自动摘要(写入 frontmatter.excerpt)
|
|
3
|
+
*
|
|
4
|
+
* 取正文第一个 paragraph 的纯文本(与 orchid-blog 的 remark-excerpt 行为一致)。
|
|
5
|
+
* description 字段仍是手动维护的展示摘要;excerpt 仅作列表兜底。
|
|
6
|
+
*/
|
|
7
|
+
export function remarkExcerpt() {
|
|
8
|
+
return (tree: any, { data }: any) => {
|
|
9
|
+
let excerpt = "";
|
|
10
|
+
for (const node of tree.children || []) {
|
|
11
|
+
if (node.type !== "paragraph") continue;
|
|
12
|
+
excerpt = mdastToString(node).trim();
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
data.astro.frontmatter.excerpt = excerpt;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 近似 mdast-util-to-string:递归取 text / inlineCode 的 value,忽略 html 与 image */
|
|
20
|
+
function mdastToString(node: any): string {
|
|
21
|
+
if (!node) return "";
|
|
22
|
+
if (node.type === "text" || node.type === "inlineCode")
|
|
23
|
+
return node.value || "";
|
|
24
|
+
if (node.type === "html" || node.type === "image" || node.type === "code")
|
|
25
|
+
return "";
|
|
26
|
+
let out = "";
|
|
27
|
+
for (const child of node.children || []) out += mdastToString(child);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [grid]…[/grid] 图片网格(A1,移植自参考模板 remark-image-grid)
|
|
3
|
+
*
|
|
4
|
+
* 支持两种形态:
|
|
5
|
+
* 1. 同段:[grid]   [/grid](标签与图片同段)
|
|
6
|
+
* 2. 跨段:[grid] 独行、图片段落若干、[/grid] 独行;未闭合时原样输出不转换
|
|
7
|
+
*
|
|
8
|
+
* 列数 = 网格内图片数(上限 4,无图缺省 2)。输出 div.image-grid[data-cols="N"],
|
|
9
|
+
* 布局由主题 CSS 生成——不用参考模板的 tailwind 动态类(md:grid-cols-N 这类
|
|
10
|
+
* 拼接类名在 node_modules symlink 源码中不可靠扫描,见 §4.10)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const countImages = (node: any): number => {
|
|
14
|
+
let count = node.type === "image" ? 1 : 0;
|
|
15
|
+
if (Array.isArray(node.children)) {
|
|
16
|
+
for (const child of node.children) count += countImages(child);
|
|
17
|
+
}
|
|
18
|
+
return count;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const gridNode = (children: any[]) => {
|
|
22
|
+
const cols = Math.min(4, countImages({ children }) || 2);
|
|
23
|
+
return {
|
|
24
|
+
type: "paragraph",
|
|
25
|
+
data: {
|
|
26
|
+
hName: "div",
|
|
27
|
+
hProperties: {
|
|
28
|
+
className: ["image-grid"],
|
|
29
|
+
"data-cols": String(cols),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
children,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function remarkImageGrid(): any {
|
|
37
|
+
return (tree: any) => {
|
|
38
|
+
if (tree.type !== "root") return;
|
|
39
|
+
const newChildren: any[] = [];
|
|
40
|
+
let inGrid = false;
|
|
41
|
+
let gridChildren: any[] = [];
|
|
42
|
+
|
|
43
|
+
for (const node of tree.children) {
|
|
44
|
+
if (node.type === "paragraph" && node.children?.length > 0) {
|
|
45
|
+
const first = node.children[0];
|
|
46
|
+
const last = node.children[node.children.length - 1];
|
|
47
|
+
const hasStart =
|
|
48
|
+
first.type === "text" && first.value.trim().startsWith("[grid]");
|
|
49
|
+
const hasEnd =
|
|
50
|
+
last.type === "text" && last.value.trim().endsWith("[/grid]");
|
|
51
|
+
|
|
52
|
+
// 形态 1:[grid]…[/grid] 同段
|
|
53
|
+
if (hasStart && hasEnd && !inGrid) {
|
|
54
|
+
first.value = first.value.replace(/^\s*\[grid\]\s*/, "");
|
|
55
|
+
last.value = last.value.replace(/\s*\[\/grid\]\s*$/, "");
|
|
56
|
+
newChildren.push(
|
|
57
|
+
gridNode(
|
|
58
|
+
node.children.filter(
|
|
59
|
+
(n: any) => n.type !== "text" || n.value.trim() !== "",
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 形态 2:跨段开始([grid] 独行则丢弃该段落)
|
|
67
|
+
if (!inGrid && hasStart) {
|
|
68
|
+
inGrid = true;
|
|
69
|
+
first.value = first.value.replace(/^\s*\[grid\]\s*/, "");
|
|
70
|
+
if (!(node.children.length === 1 && first.value.trim() === "")) {
|
|
71
|
+
gridChildren.push(node);
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 形态 2:跨段结束,汇总图片数定列数
|
|
77
|
+
if (inGrid && hasEnd) {
|
|
78
|
+
inGrid = false;
|
|
79
|
+
last.value = last.value.replace(/\s*\[\/grid\]\s*$/, "");
|
|
80
|
+
if (!(node.children.length === 1 && last.value.trim() === "")) {
|
|
81
|
+
gridChildren.push(node);
|
|
82
|
+
}
|
|
83
|
+
newChildren.push(gridNode(gridChildren));
|
|
84
|
+
gridChildren = [];
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (inGrid) gridChildren.push(node);
|
|
90
|
+
else newChildren.push(node);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 未闭合:原样输出已收集内容
|
|
94
|
+
if (inGrid) newChildren.push(...gridChildren);
|
|
95
|
+
tree.children = newChildren;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remark 插件:阅读时长与字数统计(写入 frontmatter.minutes / frontmatter.words)
|
|
3
|
+
*
|
|
4
|
+
* 口径(自实现,零依赖,对中文博客更准确):
|
|
5
|
+
* - words = CJK 字符数 + 非 CJK 单词数
|
|
6
|
+
* - minutes = max(1, round(words / 275)),275 wpm 为 reading-time 默认速度
|
|
7
|
+
* 值在渲染期由 remark 链写入,不经过 content collection schema 校验,
|
|
8
|
+
* 组件通过 `const { remarkPluginFrontmatter } = await render(post)` 读取。
|
|
9
|
+
*/
|
|
10
|
+
export function remarkReadingTime() {
|
|
11
|
+
return (tree: any, { data }: any) => {
|
|
12
|
+
const text = mdastToString(tree);
|
|
13
|
+
const words = countWords(text);
|
|
14
|
+
data.astro.frontmatter.words = words;
|
|
15
|
+
data.astro.frontmatter.minutes = Math.max(1, Math.round(words / 275));
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 近似 mdast-util-to-string:递归取 text / inlineCode 的 value,忽略 html 与 image */
|
|
20
|
+
function mdastToString(node: any): string {
|
|
21
|
+
if (!node) return "";
|
|
22
|
+
if (node.type === "text" || node.type === "inlineCode")
|
|
23
|
+
return node.value || "";
|
|
24
|
+
if (node.type === "html" || node.type === "image" || node.type === "code")
|
|
25
|
+
return "";
|
|
26
|
+
let out = "";
|
|
27
|
+
for (const child of node.children || []) out += mdastToString(child);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CJK = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g;
|
|
32
|
+
|
|
33
|
+
function countWords(text: string): number {
|
|
34
|
+
const cjk = (text.match(CJK) || []).length;
|
|
35
|
+
const rest = text.replace(CJK, " ");
|
|
36
|
+
const latin = rest.split(/\s+/).filter(Boolean).length;
|
|
37
|
+
return cjk + latin;
|
|
38
|
+
}
|