@windcoder/astro-theme-orchid 0.0.0-stage → 0.2.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 +118 -2
- package/package.json +70 -5
- 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 +411 -0
- package/src/plugins/plantuml-encoder.ts +95 -0
- package/src/plugins/rehype-diagram.ts +218 -0
- package/src/plugins/rehype-ec-vt-rerun.ts +80 -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 +350 -0
- package/src/routes/robots.txt.ts +18 -0
- package/src/routes/rss.xml.ts +37 -0
- package/src/routes/series/[name].astro +66 -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 +1218 -0
- package/src/scripts/app.ts +1041 -0
- package/src/scripts/share-poster.ts +468 -0
- package/src/styles/global.css +4864 -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 +233 -0
- package/src/vite.ts +105 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rehype 图表插件(M3b-b4,移植自 orchid-blog)
|
|
3
|
+
*
|
|
4
|
+
* - rehypeMermaid:div.mermaid-container → 构建期 beautiful-mermaid 渲染
|
|
5
|
+
* light/dark 双静态 SVG(div.mermaid-svg-light/-dark 包裹,CSS 按主题显隐),
|
|
6
|
+
* 失败降级 .mermaid-error + pre.mermaid-fallback-code 源码展示,零客户端运行时
|
|
7
|
+
* - rehypePlantuml:div.plantuml-wrapper → img.plantuml-image(data-light-src/-dark-src),
|
|
8
|
+
* 客户端 plantuml-theme-switch.js 按主题切换 src、加载失败展示重试
|
|
9
|
+
* - rehypeDiagramPanZoom:为含图表的文章注入 diagram-panzoom.js(控制栏/拖拽/全屏)
|
|
10
|
+
*
|
|
11
|
+
* 偏差(相对 orchid):
|
|
12
|
+
* - 客户端脚本改 readFileSync 模块顶层读入,不依赖 Vite ?raw 管线
|
|
13
|
+
* - panzoom 脚本仅对实际含图表的 tree 注入(orchid 无条件注入)
|
|
14
|
+
* - plantuml wrapper 不再生成随机 id(保证构建产物可复现)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { renderMermaidSVG, THEMES } from "beautiful-mermaid";
|
|
19
|
+
|
|
20
|
+
type Transformer = (tree: any, file: any) => void;
|
|
21
|
+
|
|
22
|
+
/** 深度遍历辅助(与 rehype.ts 内 visit 同风格),fn 返回 'skip' 跳过子树 */
|
|
23
|
+
function visit(node: any, fn: (n: any) => void | "skip") {
|
|
24
|
+
const children = node.children;
|
|
25
|
+
if (!Array.isArray(children)) return;
|
|
26
|
+
for (const child of children) {
|
|
27
|
+
if (fn(child) !== "skip") visit(child, fn);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const panzoomScript = readFileSync(
|
|
32
|
+
new URL("./scripts/diagram-panzoom.js", import.meta.url),
|
|
33
|
+
"utf8",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const plantumlSwitchScript = readFileSync(
|
|
37
|
+
new URL("./scripts/plantuml-theme-switch.js", import.meta.url),
|
|
38
|
+
"utf8",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/** 本 rehype tree 已注入过 panzoom 脚本(每篇文档最多一次) */
|
|
42
|
+
const panzoomInjected = new WeakSet<object>();
|
|
43
|
+
|
|
44
|
+
function hasClass(node: any, name: string): boolean {
|
|
45
|
+
const cls = node.properties?.className;
|
|
46
|
+
return Array.isArray(cls) && cls.includes(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function injectScript(tree: any, code: string) {
|
|
50
|
+
tree.children.push({
|
|
51
|
+
type: "element",
|
|
52
|
+
tagName: "script",
|
|
53
|
+
properties: {},
|
|
54
|
+
children: [{ type: "text", value: code }],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function injectPanzoom(tree: any) {
|
|
59
|
+
if (panzoomInjected.has(tree)) return;
|
|
60
|
+
panzoomInjected.add(tree);
|
|
61
|
+
injectScript(tree, panzoomScript);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface MermaidOptions {
|
|
65
|
+
lightTheme: string;
|
|
66
|
+
darkTheme: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function rehypeMermaid(options: MermaidOptions): Transformer {
|
|
70
|
+
return (tree) => {
|
|
71
|
+
let found = false;
|
|
72
|
+
visit(tree, (node) => {
|
|
73
|
+
if (node.type !== "element" || node.tagName !== "div") return;
|
|
74
|
+
if (!hasClass(node, "mermaid-container")) return;
|
|
75
|
+
const code = String(node.properties?.["data-mermaid-code"] ?? "");
|
|
76
|
+
const wrapper: any = {
|
|
77
|
+
type: "element",
|
|
78
|
+
tagName: "div",
|
|
79
|
+
properties: { className: ["diagram-wrapper", "mermaid-wrapper"] },
|
|
80
|
+
children: [],
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
const lightSvg = renderMermaidSVG(code, {
|
|
84
|
+
...THEMES[options.lightTheme],
|
|
85
|
+
padding: 20,
|
|
86
|
+
});
|
|
87
|
+
const darkSvg = renderMermaidSVG(code, {
|
|
88
|
+
...THEMES[options.darkTheme],
|
|
89
|
+
padding: 20,
|
|
90
|
+
});
|
|
91
|
+
wrapper.children.push(
|
|
92
|
+
{
|
|
93
|
+
type: "element",
|
|
94
|
+
tagName: "div",
|
|
95
|
+
properties: { className: ["mermaid-svg-light"] },
|
|
96
|
+
children: [{ type: "raw", value: lightSvg }],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: "element",
|
|
100
|
+
tagName: "div",
|
|
101
|
+
properties: { className: ["mermaid-svg-dark"] },
|
|
102
|
+
children: [{ type: "raw", value: darkSvg }],
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
node.tagName = "div";
|
|
106
|
+
node.properties = {
|
|
107
|
+
className: ["diagram-container", "mermaid-diagram-container"],
|
|
108
|
+
};
|
|
109
|
+
node.children = [wrapper];
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
112
|
+
const preview = code.length > 200 ? `${code.slice(0, 200)}…` : code;
|
|
113
|
+
console.error(`[theme] Mermaid 渲染失败(${message}):\n${preview}`);
|
|
114
|
+
node.properties = {
|
|
115
|
+
className: [
|
|
116
|
+
"diagram-container",
|
|
117
|
+
"mermaid-diagram-container",
|
|
118
|
+
"mermaid-error",
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
node.children = [
|
|
122
|
+
{
|
|
123
|
+
type: "element",
|
|
124
|
+
tagName: "p",
|
|
125
|
+
properties: {},
|
|
126
|
+
children: [
|
|
127
|
+
{ type: "text", value: "Mermaid 图表渲染失败,已展示源码:" },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: "element",
|
|
132
|
+
tagName: "pre",
|
|
133
|
+
properties: { className: ["mermaid-fallback-code"] },
|
|
134
|
+
children: [
|
|
135
|
+
{
|
|
136
|
+
type: "element",
|
|
137
|
+
tagName: "code",
|
|
138
|
+
properties: {},
|
|
139
|
+
children: [{ type: "text", value: code }],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
found = true;
|
|
146
|
+
return "skip";
|
|
147
|
+
});
|
|
148
|
+
if (found) injectPanzoom(tree);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function rehypePlantuml(): Transformer {
|
|
153
|
+
return (tree) => {
|
|
154
|
+
let found = false;
|
|
155
|
+
visit(tree, (node) => {
|
|
156
|
+
if (node.type !== "element" || node.tagName !== "div") return;
|
|
157
|
+
if (!hasClass(node, "plantuml-wrapper")) return;
|
|
158
|
+
const lightSrc = String(node.properties?.["data-light-src"] ?? "");
|
|
159
|
+
const darkSrc = String(node.properties?.["data-dark-src"] ?? lightSrc);
|
|
160
|
+
const alt = String(
|
|
161
|
+
node.properties?.["data-plantuml-alt"] || "PlantUML diagram",
|
|
162
|
+
);
|
|
163
|
+
const img = {
|
|
164
|
+
type: "element",
|
|
165
|
+
tagName: "img",
|
|
166
|
+
properties: {
|
|
167
|
+
className: ["plantuml-image"],
|
|
168
|
+
src: lightSrc,
|
|
169
|
+
alt,
|
|
170
|
+
"data-light-src": lightSrc,
|
|
171
|
+
"data-dark-src": darkSrc,
|
|
172
|
+
loading: "lazy",
|
|
173
|
+
decoding: "async",
|
|
174
|
+
},
|
|
175
|
+
children: [],
|
|
176
|
+
};
|
|
177
|
+
node.tagName = "div";
|
|
178
|
+
node.properties = {
|
|
179
|
+
className: ["diagram-container", "plantuml-diagram-container"],
|
|
180
|
+
};
|
|
181
|
+
node.children = [
|
|
182
|
+
{
|
|
183
|
+
type: "element",
|
|
184
|
+
tagName: "div",
|
|
185
|
+
properties: { className: ["plantuml-wrapper"] },
|
|
186
|
+
children: [img],
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
found = true;
|
|
190
|
+
return "skip";
|
|
191
|
+
});
|
|
192
|
+
if (found) {
|
|
193
|
+
injectPanzoom(tree);
|
|
194
|
+
// PlantUML 主题切换/失败重试脚本(与 panzoom 互相独立,各自防重入)
|
|
195
|
+
injectScript(tree, plantumlSwitchScript);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 单独挂载时的兜底:扫描 tree 中任意图表容器并注入 panzoom 脚本 */
|
|
201
|
+
export function rehypeDiagramPanZoom(): Transformer {
|
|
202
|
+
return (tree) => {
|
|
203
|
+
if (panzoomInjected.has(tree)) return;
|
|
204
|
+
let found = false;
|
|
205
|
+
visit(tree, (node) => {
|
|
206
|
+
if (found) return "skip";
|
|
207
|
+
if (
|
|
208
|
+
node.type === "element" &&
|
|
209
|
+
node.tagName === "div" &&
|
|
210
|
+
hasClass(node, "diagram-container")
|
|
211
|
+
) {
|
|
212
|
+
found = true;
|
|
213
|
+
return "skip";
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
if (found) injectPanzoom(tree);
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §4.74:Astro ClientRouter(View Transitions)下 EC 社区折叠插件的换页失效修复。
|
|
3
|
+
* §4.76:二次根因修正——data-astro-rerun 确实会重执行脚本,但被脚本自身的
|
|
4
|
+
* window 级幂等守卫拦截(早退),新页按钮依然无监听;首版修复曾试图把守卫
|
|
5
|
+
* 标志位改名规避,实测仍有同会话跨页残留(写作指南页无折叠块但注入脚本并
|
|
6
|
+
* 置标志,再 VT 进快速开始时读标志早退),最终改为整段删除守卫与赋值。
|
|
7
|
+
*
|
|
8
|
+
* 现象:经系列导航等 VT 换页进入新文章后,折叠代码块的展开按钮点击无效。
|
|
9
|
+
*
|
|
10
|
+
* 根因(两层):
|
|
11
|
+
* 1. expressive-code-collapsible@0.1.0 的交互脚本只在 DOMContentLoaded
|
|
12
|
+
* 绑定一次,且 MutationObserver 闭包引用换页前的旧 body——ClientRouter
|
|
13
|
+
* swap 整体替换 body 后:内联脚本不重新执行、observer 观察的旧 body 已
|
|
14
|
+
* 脱离文档,新页面的 .ec-collapse__toggle 无 click 监听。
|
|
15
|
+
* (EC core 官方脚本已用 astro:page-load 自适配 VT,copy 按钮等不受影响。)
|
|
16
|
+
* → 修复:标记 data-astro-rerun 让 ClientRouter swap 时重执行该脚本。
|
|
17
|
+
* 2. §4.76 两轮实测发现重执行仍无效:脚本首两行是 window 级早退守卫
|
|
18
|
+
* (if (window.ecCollapsibleInit) return; window.ecCollapsibleInit = true;),
|
|
19
|
+
* ClientRouter 换页只换 body 不换 window——标志位在同一浏览器会话内跨页
|
|
20
|
+
* 存活:任何注入过该脚本的页面(含无折叠代码块的文章,脚本照注入置位)
|
|
21
|
+
* 之后 VT 进入有折叠块的文章时,重执行被早退拦截,绑定整体短路。
|
|
22
|
+
* → 修复:脚本文本中整段删除该守卫与标志赋值,每次执行都完整初始化。
|
|
23
|
+
* 安全性:按钮级 dataset.init 守卫防重复绑定;MutationObserver 每次重
|
|
24
|
+
* 建并观察当前 body,旧 observer 附着已脱离文档的旧 body,随其 GC;
|
|
25
|
+
* data-astro-rerun 每次 swap 恰执行一次、full load 一次,无同 DOM 重入;
|
|
26
|
+
* 离开含折叠块的页面后脚本不存在、不执行,无额外开销。
|
|
27
|
+
*
|
|
28
|
+
* 仅含折叠代码块的页面才有该脚本;若上游插件改守卫名或改结构,删除正则
|
|
29
|
+
* 不命中即退化为 §4.74 行为(仅重执行,不删守卫),不会更糟。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** 内联脚本特征:社区折叠插件按钮选择器,官方 EC 脚本不含此串 */
|
|
33
|
+
const COLLAPSIBLE_SCRIPT_MARKER = "ec-collapse__toggle";
|
|
34
|
+
|
|
35
|
+
/** window 级早退守卫:if (window.ecCollapsibleInit) return; (含前导空白) */
|
|
36
|
+
const GUARD_RE = /\s*if\s*\(\s*window\.ecCollapsibleInit\s*\)\s*return\s*;?/;
|
|
37
|
+
/** 守卫标志赋值:window.ecCollapsibleInit = true; (含前导空白) */
|
|
38
|
+
const FLAG_SET_RE = /\s*window\.ecCollapsibleInit\s*=\s*true\s*;?/;
|
|
39
|
+
|
|
40
|
+
interface HastNode {
|
|
41
|
+
type: string;
|
|
42
|
+
tagName?: string;
|
|
43
|
+
properties?: Record<string, unknown>;
|
|
44
|
+
children?: HastNode[];
|
|
45
|
+
value?: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function scriptText(node: HastNode): string {
|
|
49
|
+
return (node.children ?? [])
|
|
50
|
+
.map((child) => (typeof child.value === "string" ? child.value : ""))
|
|
51
|
+
.join("");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walk(node: HastNode): void {
|
|
55
|
+
for (const child of node.children ?? []) {
|
|
56
|
+
if (child.type !== "element") continue;
|
|
57
|
+
if (
|
|
58
|
+
child.tagName === "script" &&
|
|
59
|
+
!child.properties?.src &&
|
|
60
|
+
scriptText(child).includes(COLLAPSIBLE_SCRIPT_MARKER)
|
|
61
|
+
) {
|
|
62
|
+
child.properties["data-astro-rerun"] = true;
|
|
63
|
+
// 删除 window 级早退守卫(§4.76 根因 2):守卫标志在 ClientRouter
|
|
64
|
+
// 换页间随 window 存活,任何先访问过含该脚本页面的会话都会让后续
|
|
65
|
+
// 重执行早退短路;删除后每次执行都完整绑定新页按钮
|
|
66
|
+
for (const text of child.children ?? []) {
|
|
67
|
+
if (typeof text.value === "string") {
|
|
68
|
+
text.value = text.value
|
|
69
|
+
.replace(GUARD_RE, "")
|
|
70
|
+
.replace(FLAG_SET_RE, "");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
walk(child);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function rehypeEcVtRerun() {
|
|
79
|
+
return (tree: HastNode) => walk(tree);
|
|
80
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 邮箱防爬(M3b-b4,移植自 orchid-blog)
|
|
3
|
+
*
|
|
4
|
+
* 构建期把 mailto: 链接的地址编码进 data-encoded-email,href 置为 '#',
|
|
5
|
+
* 并内联一段 onclick 解码脚本:点击时还原地址 → 回写 href → 延时补一次点击。
|
|
6
|
+
* 爬虫不执行点击,拿不到明文邮箱;解码逻辑随属性内联,无需额外客户端资源。
|
|
7
|
+
*
|
|
8
|
+
* 偏差说明:orchid 会向 head 注入占位样式,本主题统一并入 global.css
|
|
9
|
+
* (a[data-encoded-email] 光标提示),插件保持纯 DOM 变换。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type Transformer = (tree: any, file: any) => void;
|
|
13
|
+
|
|
14
|
+
/** 深度遍历辅助(与 rehype.ts 内 visit 同风格),fn 返回 'skip' 跳过子树 */
|
|
15
|
+
function visit(node: any, fn: (n: any) => void | "skip") {
|
|
16
|
+
const children = node.children;
|
|
17
|
+
if (!Array.isArray(children)) return;
|
|
18
|
+
for (const child of children) {
|
|
19
|
+
if (fn(child) !== "skip") visit(child, fn);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface EmailProtectionOptions {
|
|
24
|
+
/** base64:兼容性最好(btoa 仅支持 Latin1,超集地址跳过保护保持原样);rot13:零 API 依赖 */
|
|
25
|
+
method: "base64" | "rot13";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const encodeBase64 = (value: string): string | null => {
|
|
29
|
+
try {
|
|
30
|
+
// Buffer 在 Node 构建期可用;btoa 遇非 Latin1 会抛错,统一 try/catch 兜底
|
|
31
|
+
return typeof Buffer !== "undefined"
|
|
32
|
+
? Buffer.from(value, "utf8").toString("base64")
|
|
33
|
+
: btoa(value);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const encodeRot13 = (value: string): string =>
|
|
40
|
+
value.replace(/[a-zA-Z]/g, (c) => {
|
|
41
|
+
const base = c <= "Z" ? 65 : 97;
|
|
42
|
+
return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** 点击时还原地址并补一次点击(压缩成单行内联,this 为链接元素自身) */
|
|
46
|
+
const ONCLICK_TEMPLATE = (method: "base64" | "rot13") => {
|
|
47
|
+
const decode =
|
|
48
|
+
method === "base64"
|
|
49
|
+
? "function(e){return decodeURIComponent(escape(atob(e)))}"
|
|
50
|
+
: 'function(e){return e.replace(/[a-zA-Z]/g,function(c){var b=c.charCodeAt(0),s=c<="Z"?65:97;return String.fromCharCode((b-s+13)%26+s)})}';
|
|
51
|
+
return `(function(){var e=this.getAttribute('data-encoded-email');if(!e)return;this.removeAttribute('data-encoded-email');this.setAttribute('href','mailto:'+(${decode})(e));var a=this;setTimeout(function(){a.click()},0)}).call(this);return false`
|
|
52
|
+
.replace(/\s+/g, " ")
|
|
53
|
+
.trim();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function rehypeEmailProtection(
|
|
57
|
+
options: EmailProtectionOptions,
|
|
58
|
+
): Transformer {
|
|
59
|
+
const encode = options.method === "base64" ? encodeBase64 : encodeRot13;
|
|
60
|
+
const onclick = ONCLICK_TEMPLATE(options.method);
|
|
61
|
+
return (tree) => {
|
|
62
|
+
visit(tree, (node) => {
|
|
63
|
+
if (node.type !== "element" || node.tagName !== "a") return;
|
|
64
|
+
const href = String(node.properties?.href ?? "");
|
|
65
|
+
if (!href.toLowerCase().startsWith("mailto:")) return;
|
|
66
|
+
const address = href.slice("mailto:".length);
|
|
67
|
+
const encoded = encode(address);
|
|
68
|
+
// 编码失败(非 Latin1 的 base64 场景)保持原链接,不做半吊子保护
|
|
69
|
+
if (!encoded) return;
|
|
70
|
+
node.properties["data-encoded-email"] = encoded;
|
|
71
|
+
node.properties.href = "#";
|
|
72
|
+
node.properties.onclick = onclick;
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ::github{repo="owner/repo"} → GitHub 仓库卡片(A2,移植自参考模板
|
|
3
|
+
* rehype-component-github-card)
|
|
4
|
+
*
|
|
5
|
+
* remarkDirectives 已把 leaf 指令转成 <github repo="…"> 标签;本插件将其替换为
|
|
6
|
+
* 卡片结构 + 内联脚本:客户端 fetch api.github.com 填充描述/stars/forks/
|
|
7
|
+
* license/头像;fetch-waiting(骨架 pulsate 动画)→ 完成态 / fetch-error 两态。
|
|
8
|
+
* 数据获取发生在读者浏览器端,构建期零请求。
|
|
9
|
+
*/
|
|
10
|
+
import { h } from "hastscript";
|
|
11
|
+
|
|
12
|
+
function githubCard(properties: any, children: any[]): any {
|
|
13
|
+
if (Array.isArray(children) && children.length !== 0)
|
|
14
|
+
return h(
|
|
15
|
+
"div",
|
|
16
|
+
{ class: "hidden" },
|
|
17
|
+
`Invalid directive. ("github" directive must be leaf type '::github{repo="owner/repo"}')`,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const repo = String(properties.repo ?? "");
|
|
21
|
+
if (!repo.includes("/"))
|
|
22
|
+
return h(
|
|
23
|
+
"div",
|
|
24
|
+
{ class: "hidden" },
|
|
25
|
+
`Invalid repository. ("repo" attribute must be in the format "owner/repo")`,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// 每卡片随机 id 防同页多卡片串数据(碰撞概率可忽略)
|
|
29
|
+
const cardUuid = `GC${Math.random().toString(36).slice(-6)}`;
|
|
30
|
+
|
|
31
|
+
const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });
|
|
32
|
+
const nLanguage = h(
|
|
33
|
+
`span#${cardUuid}-language`,
|
|
34
|
+
{ class: "gc-language" },
|
|
35
|
+
"Waiting...",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const nTitle = h("div", { class: "gc-titlebar" }, [
|
|
39
|
+
h("div", { class: "gc-titlebar-left" }, [
|
|
40
|
+
h("div", { class: "gc-owner" }, [
|
|
41
|
+
nAvatar,
|
|
42
|
+
h("div", { class: "gc-user" }, repo.split("/")[0]),
|
|
43
|
+
]),
|
|
44
|
+
h("div", { class: "gc-divider" }, "/"),
|
|
45
|
+
h("div", { class: "gc-repo" }, repo.split("/")[1]),
|
|
46
|
+
]),
|
|
47
|
+
h("div", { class: "github-logo" }),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const nDescription = h(
|
|
51
|
+
`div#${cardUuid}-description`,
|
|
52
|
+
{ class: "gc-description" },
|
|
53
|
+
"Waiting for api.github.com...",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "00K");
|
|
57
|
+
const nForks = h(`div#${cardUuid}-forks`, { class: "gc-forks" }, "0K");
|
|
58
|
+
const nLicense = h(`div#${cardUuid}-license`, { class: "gc-license" }, "0K");
|
|
59
|
+
|
|
60
|
+
const nScript = h(
|
|
61
|
+
`script#${cardUuid}-script`,
|
|
62
|
+
{ type: "text/javascript" },
|
|
63
|
+
`
|
|
64
|
+
fetch('https://api.github.com/repos/${repo}', { referrerPolicy: "no-referrer" }).then(response => response.json()).then(data => {
|
|
65
|
+
document.getElementById('${cardUuid}-description').innerText = data.description?.replace(/:[a-zA-Z0-9_]+:/g, '') || "Description not set";
|
|
66
|
+
document.getElementById('${cardUuid}-language').innerText = data.language;
|
|
67
|
+
document.getElementById('${cardUuid}-forks').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.forks).replaceAll("\\u202f", '');
|
|
68
|
+
document.getElementById('${cardUuid}-stars').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.stargazers_count).replaceAll("\\u202f", '');
|
|
69
|
+
const avatarEl = document.getElementById('${cardUuid}-avatar');
|
|
70
|
+
avatarEl.style.backgroundImage = 'url(' + data.owner.avatar_url + '&s=32' + ')';
|
|
71
|
+
avatarEl.style.backgroundColor = 'transparent';
|
|
72
|
+
document.getElementById('${cardUuid}-license').innerText = data.license?.spdx_id || "no-license";
|
|
73
|
+
document.getElementById('${cardUuid}-card').classList.remove("fetch-waiting");
|
|
74
|
+
}).catch(err => {
|
|
75
|
+
document.getElementById('${cardUuid}-card')?.classList.add("fetch-error");
|
|
76
|
+
})
|
|
77
|
+
`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return h(
|
|
81
|
+
`a#${cardUuid}-card`,
|
|
82
|
+
{
|
|
83
|
+
class: "card-github fetch-waiting",
|
|
84
|
+
href: `https://github.com/${repo}`,
|
|
85
|
+
target: "_blank",
|
|
86
|
+
},
|
|
87
|
+
[
|
|
88
|
+
nTitle,
|
|
89
|
+
nDescription,
|
|
90
|
+
h("div", { class: "gc-infobar" }, [nStars, nForks, nLicense, nLanguage]),
|
|
91
|
+
nScript,
|
|
92
|
+
],
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function rehypeGithubCard(): any {
|
|
97
|
+
return (tree: any) => {
|
|
98
|
+
const walk = (node: any) => {
|
|
99
|
+
if (!Array.isArray(node.children)) return;
|
|
100
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
101
|
+
const child = node.children[i];
|
|
102
|
+
if (child.type === "element" && child.tagName === "github") {
|
|
103
|
+
node.children[i] = githubCard(
|
|
104
|
+
child.properties ?? {},
|
|
105
|
+
child.children ?? [],
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
walk(child);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
walk(tree);
|
|
113
|
+
};
|
|
114
|
+
}
|