Initial commit

This commit is contained in:
2026-02-02 22:47:52 +03:00
committed by GitHub
commit f53016aeda
239 changed files with 84360 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
import { definePlugin } from "@expressive-code/core";
import type { Element } from "hast";
export function pluginCollapseButton() {
return definePlugin({
name: "Collapse Button",
hooks: {
postprocessRenderedBlock: (context) => {
// If the code block has a title, we don't add the collapse button
// as it might conflict with the title bar layout
const classNames = (context.renderData.blockAst.properties?.className as string[]) || [];
if (classNames.includes("has-title")) {
return;
}
function processCodeBlock(node: Element) {
// Add classes to the root node to indicate it's collapsible and expanded by default
if (!node.properties) node.properties = {};
const classNames = (node.properties.className as string[]) || [];
if (!classNames.includes("collapsible")) {
classNames.push("collapsible");
}
if (!classNames.includes("expanded")) {
classNames.push("expanded");
}
node.properties.className = classNames;
const collapseButton = {
type: "element" as const,
tagName: "button",
properties: {
className: [
"collapse-btn"
],
"aria-label": "Collapse code",
},
children: [
{
type: "element" as const,
tagName: "div",
properties: {
className: [
"collapse-btn-icon",
],
},
children: [
{
type: "element" as const,
tagName: "svg",
properties: {
viewBox: "0 0 24 24",
xmlns: "http://www.w3.org/2000/svg",
className: [
"collapse-btn-icon",
"collapse-icon",
],
},
children: [
{
type: "element" as const,
tagName: "path",
properties: {
d: "M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"
},
children: [],
},
],
},
],
},
],
} as Element;
if (!node.children) {
node.children = [];
}
node.children.push(collapseButton);
}
processCodeBlock(context.renderData.blockAst);
},
},
});
}

View File

@@ -0,0 +1,88 @@
import { definePlugin } from "@expressive-code/core";
import type { Element } from "hast";
export function pluginCopyButton() {
return definePlugin({
name: "Copy Button",
hooks: {
postprocessRenderedBlock: (context) => {
function processCodeBlock(node: Element) {
const copyButton = {
type: "element" as const,
tagName: "button",
properties: {
className: [
"copy-btn"
],
"aria-label": "Copy code",
},
children: [
{
type: "element" as const,
tagName: "div",
properties: {
className: [
"copy-btn-icon"
],
},
children: [
{
type: "element" as const,
tagName: "svg",
properties: {
viewBox: "0 -960 960 960",
xmlns: "http://www.w3.org/2000/svg",
className: [
"copy-btn-icon",
"copy-icon",
],
},
children: [
{
type: "element" as const,
tagName: "path",
properties: {
d: "M368.37-237.37q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-474.26q0-34.48 24.26-58.74 24.26-24.26 58.74-24.26h378.26q34.48 0 58.74 24.26 24.26 24.26 24.26 58.74v474.26q0 34.48-24.26 58.74-24.26 24.26-58.74 24.26H368.37Zm0-83h378.26v-474.26H368.37v474.26Zm-155 238q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-515.76q0-17.45 11.96-29.48 11.97-12.02 29.33-12.02t29.54 12.02q12.17 12.03 12.17 29.48v515.76h419.76q17.45 0 29.48 11.96 12.02 11.97 12.02 29.33t-12.02 29.54q-12.03 12.17-29.48 12.17H213.37Zm155-238v-474.26 474.26Z",
},
children: [],
},
],
},
{
type: "element" as const,
tagName: "svg",
properties: {
viewBox: "0 -960 960 960",
xmlns: "http://www.w3.org/2000/svg",
className: [
"copy-btn-icon",
"success-icon",
],
},
children: [
{
type: "element" as const,
tagName: "path",
properties: {
d: "m389-377.13 294.7-294.7q12.58-12.67 29.52-12.67 16.93 0 29.61 12.67 12.67 12.68 12.67 29.53 0 16.86-12.28 29.14L419.07-288.41q-12.59 12.67-29.52 12.67-16.94 0-29.62-12.67L217.41-430.93q-12.67-12.68-12.79-29.45-.12-16.77 12.55-29.45 12.68-12.67 29.62-12.67 16.93 0 29.28 12.67L389-377.13Z",
},
children: [],
},
],
},
],
},
],
} as Element;
if (!node.children) {
node.children = [];
}
node.children.push(copyButton);
}
processCodeBlock(context.renderData.blockAst);
},
},
});
}

View File

@@ -0,0 +1,52 @@
/**
* Based on the discussion at https://github.com/expressive-code/expressive-code/issues/153#issuecomment-2282218684
*/
import { definePlugin } from "@expressive-code/core";
export function pluginLanguageBadge() {
return definePlugin({
name: "Language Badge",
hooks: {
postprocessRenderedBlock: ({ codeBlock, renderData }) => {
const language = codeBlock.language;
if (language && renderData.blockAst.properties) {
renderData.blockAst.properties["data-language"] = language;
}
},
},
baseStyles: ({}) => `
.frame[data-language]:not(.has-title):not(.is-terminal) {
position: relative;
&::after {
pointer-events: none;
position: absolute;
z-index: 2;
right: 0.5rem;
top: 0.5rem;
content: attr(data-language);
font-family: "JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
color: var(--btn-content);
background: var(--btn-regular-bg);
opacity: 0;
transition: opacity 0.3s;
padding: 0.1rem 0.5rem;
border-radius: 0.5rem;
}
@media (hover: hover) {
&::after {
opacity: 1;
}
&:hover::after {
opacity: 0;
}
}
}
`,
});
}

View File

@@ -0,0 +1,481 @@
(() => {
// 单例模式:检查是否已经初始化过
if (window.mermaidInitialized) {
return;
}
window.mermaidInitialized = true;
// 记录当前主题状态,避免不必要的重新渲染
let currentTheme = null;
let isRendering = false; // 防止并发渲染
let retryCount = 0;
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1秒
// 检查主题是否真的发生了变化
function hasThemeChanged() {
const isDark = document.documentElement.classList.contains("dark");
const newTheme = isDark ? "dark" : "default";
if (currentTheme !== newTheme) {
currentTheme = newTheme;
return true;
}
return false;
}
// 等待 Mermaid 库加载完成
function waitForMermaid(timeout = 10000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (window.mermaid && typeof window.mermaid.initialize === "function") {
resolve(window.mermaid);
} else if (Date.now() - startTime > timeout) {
reject(new Error("Mermaid library failed to load within timeout"));
} else {
setTimeout(check, 100);
}
}
check();
});
}
// 设置 MutationObserver 监听 html 元素的 class 属性变化
function setupMutationObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.attributeName === "class"
) {
// 检查是否是 dark 类的变化
const target = mutation.target;
const wasDark = mutation.oldValue
? mutation.oldValue.includes("dark")
: false;
const isDark = target.classList.contains("dark");
if (wasDark !== isDark) {
if (hasThemeChanged()) {
// 延迟渲染,避免主题切换时的闪烁
setTimeout(() => renderMermaidDiagrams(), 150);
}
}
}
});
});
// 开始观察 html 元素的 class 属性变化
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
attributeOldValue: true,
});
}
// 缩放平移
function attachZoomControls(element, svgElement) {
if (element.__zoomAttached) return;
element.__zoomAttached = true;
const wrapper = document.createElement("div");
wrapper.className = "mermaid-zoom-wrapper";
const svgParent = svgElement.parentNode;
wrapper.appendChild(svgElement);
svgParent.appendChild(wrapper);
let scale = 1;
let tx = 0;
let ty = 0;
const MIN_SCALE = 0.2;
const MAX_SCALE = 6;
function applyTransform() {
wrapper.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
}
const controls = document.createElement("div");
controls.className = "mermaid-zoom-controls";
controls.innerHTML = `
<button class="btn-regular rounded-lg h-10 w-10 active:scale-90" data-action="zoom-in" title="Zoom in">+</button>
<button class="btn-regular rounded-lg h-10 w-10 active:scale-90" data-action="zoom-out" title="Zoom out"></button>
<button class="btn-regular rounded-lg h-10 w-10 active:scale-90" data-action="reset" title="Reset">⤾</button>
`;
controls.addEventListener("click", (ev) => {
const action =
ev.target.getAttribute && ev.target.getAttribute("data-action");
if (!action) return;
switch (action) {
case "zoom-in":
scale = Math.min(MAX_SCALE, +(scale * 1.2).toFixed(3));
applyTransform();
break;
case "zoom-out":
scale = Math.max(MIN_SCALE, +(scale / 1.2).toFixed(3));
applyTransform();
break;
case "reset":
scale = 1;
tx = 0;
ty = 0;
applyTransform();
break;
}
});
element.appendChild(controls);
// 鼠标滚轮缩放
element.addEventListener(
"wheel",
(ev) => {
ev.preventDefault();
const delta = -ev.deltaY;
const zoomFactor = delta > 0 ? 1.12 : 1 / 1.12;
const prevScale = scale;
scale = Math.min(
MAX_SCALE,
Math.max(MIN_SCALE, +(scale * zoomFactor).toFixed(3)),
);
const rect = wrapper.getBoundingClientRect();
const cx = ev.clientX - rect.left;
const cy = ev.clientY - rect.top;
const worldX = cx / prevScale - tx;
const worldY = cy / prevScale - ty;
tx = cx / scale - worldX;
ty = cy / scale - worldY;
applyTransform();
},
{ passive: false },
);
let isPanning = false;
let startX = 0;
let startY = 0;
let startTx = 0;
let startTy = 0;
wrapper.style.touchAction = "none";
wrapper.addEventListener("pointerdown", (ev) => {
if (ev.button !== 0) return; // 仅左键
isPanning = true;
wrapper.setPointerCapture(ev.pointerId);
startX = ev.clientX;
startY = ev.clientY;
startTx = tx;
startTy = ty;
});
wrapper.addEventListener("pointermove", (ev) => {
if (!isPanning) return;
const dx = ev.clientX - startX;
const dy = ev.clientY - startY;
tx = startTx + dx / scale; // 根据当前缩放调整灵敏度
ty = startTy + dy / scale;
applyTransform();
});
wrapper.addEventListener("pointerup", (ev) => {
isPanning = false;
try {
wrapper.releasePointerCapture(ev.pointerId);
} catch (e) { }
});
wrapper.addEventListener("pointercancel", () => {
isPanning = false;
});
// 双击重置
wrapper.addEventListener("dblclick", () => {
scale = 1;
tx = 0;
ty = 0;
applyTransform();
});
applyTransform();
let resizeTimer = null;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
applyTransform();
}, 200);
});
}
// 设置其他事件监听器
function setupEventListeners() {
// 监听页面切换
document.addEventListener("astro:page-load", () => {
// 重新初始化主题状态
currentTheme = null;
retryCount = 0; // 重置重试计数
if (hasThemeChanged()) {
setTimeout(() => renderMermaidDiagrams(), 100);
}
});
// 监听页面可见性变化,页面重新可见时重新渲染
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
setTimeout(() => renderMermaidDiagrams(), 200);
}
});
}
async function initializeMermaid() {
try {
await waitForMermaid();
// 初始化 Mermaid 配置
window.mermaid.initialize({
startOnLoad: false,
theme: "default",
themeVariables: {
fontFamily: "inherit",
fontSize: "16px",
},
securityLevel: "loose",
// 添加错误处理配置
errorLevel: "warn",
logLevel: "error",
});
// 渲染所有 Mermaid 图表
await renderMermaidDiagrams();
} catch (error) {
console.error("Failed to initialize Mermaid:", error);
// 如果初始化失败,尝试重新加载
if (retryCount < MAX_RETRIES) {
retryCount++;
setTimeout(() => initializeMermaid(), RETRY_DELAY * retryCount);
}
}
}
async function renderMermaidDiagrams() {
// 防止并发渲染
if (isRendering) {
return;
}
// 检查 Mermaid 是否可用
if (!window.mermaid || typeof window.mermaid.render !== "function") {
console.warn("Mermaid not available, skipping render");
return;
}
isRendering = true;
try {
const mermaidElements = document.querySelectorAll(
".mermaid[data-mermaid-code]",
);
if (mermaidElements.length === 0) {
isRendering = false;
return;
}
// 延迟检测主题,确保 DOM 已经更新
await new Promise((resolve) => setTimeout(resolve, 100));
const htmlElement = document.documentElement;
const isDark = htmlElement.classList.contains("dark");
const theme = isDark ? "dark" : "default";
// 更新 Mermaid 主题(只需要更新一次)
window.mermaid.initialize({
startOnLoad: false,
theme: theme,
themeVariables: {
fontFamily: "inherit",
fontSize: "16px",
// 强制应用主题变量
primaryColor: isDark ? "#ffffff" : "#000000",
primaryTextColor: isDark ? "#ffffff" : "#000000",
primaryBorderColor: isDark ? "#ffffff" : "#000000",
lineColor: isDark ? "#ffffff" : "#000000",
secondaryColor: isDark ? "#333333" : "#f0f0f0",
tertiaryColor: isDark ? "#555555" : "#e0e0e0",
},
securityLevel: "loose",
errorLevel: "warn",
logLevel: "error",
});
// 批量渲染所有图表,添加重试机制
const renderPromises = Array.from(mermaidElements).map(
async (element, index) => {
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts) {
try {
const code = element.getAttribute("data-mermaid-code");
if (!code) {
break;
}
// 渲染图表
const { svg } = await window.mermaid.render(
`mermaid-${Date.now()}-${index}-${attempts}`,
code,
);
const parser = new DOMParser();
const doc = parser.parseFromString(
svg,
"image/svg+xml",
);
const svgElement = doc.documentElement;
element.innerHTML = "";
element.__zoomAttached = false;
element.appendChild(svgElement);
// 添加响应式支持
const insertedSvg = element.querySelector("svg");
if (insertedSvg) {
insertedSvg.setAttribute("width", "100%");
insertedSvg.removeAttribute("height");
insertedSvg.style.maxWidth = "100%";
insertedSvg.style.height = "auto";
//Todo 需要根据实际情况
insertedSvg.style.minHeight = "300px";
// 强制应用样式
if (isDark) {
svgElement.style.filter = "brightness(0.9) contrast(1.1)";
} else {
svgElement.style.filter = "none";
}
attachZoomControls(element, insertedSvg);
}
// 渲染成功,跳出重试循环
break;
} catch (error) {
attempts++;
console.warn(
`Mermaid rendering attempt ${attempts} failed for element ${index}:`,
error,
);
if (attempts >= maxAttempts) {
console.error(
`Failed to render Mermaid diagram after ${maxAttempts} attempts:`,
error,
);
element.innerHTML = `
<div class="mermaid-error">
<p>Failed to render diagram after ${maxAttempts} attempts.</p>
<button onclick="location.reload()" style="margin-top: 8px; padding: 4px 8px; background: var(--primary); color: white; border: none; border-radius: 4px; cursor: pointer;">
Retry Page
</button>
</div>
`;
} else {
// 等待一段时间后重试
await new Promise((resolve) =>
setTimeout(resolve, 500 * attempts),
);
}
}
}
},
);
// 等待所有渲染完成
await Promise.all(renderPromises);
retryCount = 0; // 重置重试计数
} catch (error) {
console.error("Error in renderMermaidDiagrams:", error);
// 如果渲染失败,尝试重新渲染
if (retryCount < MAX_RETRIES) {
retryCount++;
setTimeout(() => renderMermaidDiagrams(), RETRY_DELAY * retryCount);
}
} finally {
isRendering = false;
}
}
// 初始化主题状态
function initializeThemeState() {
const isDark = document.documentElement.classList.contains("dark");
currentTheme = isDark ? "dark" : "default";
}
// 加载 Mermaid 库
async function loadMermaid() {
if (typeof window.mermaid !== "undefined") {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src =
"https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
script.onload = () => {
console.log("Mermaid library loaded successfully");
resolve();
};
script.onerror = (error) => {
console.error("Failed to load Mermaid library:", error);
// 尝试备用 CDN
const fallbackScript = document.createElement("script");
fallbackScript.src = "https://unpkg.com/mermaid@11/dist/mermaid.min.js";
fallbackScript.onload = () => {
console.log("Mermaid library loaded from fallback CDN");
resolve();
};
fallbackScript.onerror = () => {
reject(
new Error(
"Failed to load Mermaid from both primary and fallback CDNs",
),
);
};
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
});
}
// 主初始化函数
async function initialize() {
try {
// 首先检查是否有 Mermaid 图表
const mermaidElements = document.querySelectorAll(
".mermaid[data-mermaid-code]",
);
if (mermaidElements.length === 0) {
return;
}
// 设置监听器
setupMutationObserver();
setupEventListeners();
// 初始化主题状态
initializeThemeState();
// 加载并初始化 Mermaid
await loadMermaid();
await initializeMermaid();
} catch (error) {
console.error("Failed to initialize Mermaid system:", error);
}
}
// 启动初始化
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initialize);
} else {
initialize();
}
})();

View File

@@ -0,0 +1,33 @@
/// <reference types="mdast" />
import { h } from "hastscript";
/**
* Creates an admonition component.
*
* @param {Object} properties - The properties of the component.
* @param {string} [properties.title] - An optional title.
* @param {('tip'|'note'|'important'|'caution'|'warning')} type - The admonition type.
* @param {import('mdast').RootContent[]} children - The children elements of the component.
* @returns {import('mdast').Parent} The created admonition component.
*/
export function AdmonitionComponent(properties, children, type) {
if (!Array.isArray(children) || children.length === 0)
return h(
"div",
{ class: "hidden" },
'Invalid admonition directive. (Admonition directives must be of block type ":::note{name="name"} <content> :::")',
);
let label = null;
if (properties?.["has-directive-label"]) {
label = children[0]; // The first child is the label
// biome-ignore lint/style/noParameterAssign: <check later>
children = children.slice(1);
label.tagName = "div"; // Change the tag <p> to <div>
}
return h("blockquote", { class: `admonition bdm-${type}` }, [
h("span", { class: "bdm-title" }, label ? label : type.toUpperCase()),
...children,
]);
}

View File

@@ -0,0 +1,95 @@
/// <reference types="mdast" />
import { h } from "hastscript";
/**
* Creates a GitHub Card component.
*
* @param {Object} properties - The properties of the component.
* @param {string} properties.repo - The GitHub repository in the format "owner/repo".
* @param {import('mdast').RootContent[]} children - The children elements of the component.
* @returns {import('mdast').Parent} The created GitHub Card component.
*/
export function GithubCardComponent(properties, children) {
if (Array.isArray(children) && children.length !== 0)
return h("div", { class: "hidden" }, [
'Invalid directive. ("github" directive must be leaf type "::github{repo="owner/repo"}")',
]);
if (!properties.repo || !properties.repo.includes("/"))
return h(
"div",
{ class: "hidden" },
'Invalid repository. ("repo" attributte must be in the format "owner/repo")',
);
const repo = properties.repo;
const cardUuid = `GC${Math.random().toString(36).slice(-6)}`; // Collisions are not important
const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });
const nLanguage = h(
`span#${cardUuid}-language`,
{ class: "gc-language" },
"Waiting...",
);
const nTitle = h("div", { class: "gc-titlebar" }, [
h("div", { class: "gc-titlebar-left" }, [
h("div", { class: "gc-owner" }, [
nAvatar,
h("div", { class: "gc-user" }, repo.split("/")[0]),
]),
h("div", { class: "gc-divider" }, "/"),
h("div", { class: "gc-repo" }, repo.split("/")[1]),
]),
h("div", { class: "github-logo" }),
]);
const nDescription = h(
`div#${cardUuid}-description`,
{ class: "gc-description" },
"Waiting for api.github.com...",
);
const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "00K");
const nForks = h(`div#${cardUuid}-forks`, { class: "gc-forks" }, "0K");
const nLicense = h(`div#${cardUuid}-license`, { class: "gc-license" }, "0K");
const nScript = h(
`script#${cardUuid}-script`,
{ type: "text/javascript", defer: true },
`
fetch('https://api.github.com/repos/${repo}', { referrerPolicy: "no-referrer" }).then(response => response.json()).then(data => {
document.getElementById('${cardUuid}-description').innerText = data.description?.replace(/:[a-zA-Z0-9_]+:/g, '') || "Description not set";
document.getElementById('${cardUuid}-language').innerText = data.language;
document.getElementById('${cardUuid}-forks').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.forks).replaceAll("\u202f", '');
document.getElementById('${cardUuid}-stars').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.stargazers_count).replaceAll("\u202f", '');
const avatarEl = document.getElementById('${cardUuid}-avatar');
avatarEl.style.backgroundImage = 'url(' + data.owner.avatar_url + ')';
avatarEl.style.backgroundColor = 'transparent';
document.getElementById('${cardUuid}-license').innerText = data.license?.spdx_id || "no-license";
document.getElementById('${cardUuid}-card').classList.remove("fetch-waiting");
console.log("[GITHUB-CARD] Loaded card for ${repo} | ${cardUuid}.")
}).catch(err => {
const c = document.getElementById('${cardUuid}-card');
c?.classList.add("fetch-error");
console.warn("[GITHUB-CARD] (Error) Loading card for ${repo} | ${cardUuid}.")
})
`,
);
return h(
`a#${cardUuid}-card`,
{
class: "card-github fetch-waiting no-styling",
href: `https://github.com/${repo}`,
target: "_blank",
repo,
},
[
nTitle,
nDescription,
h("div", { class: "gc-infobar" }, [nStars, nForks, nLicense, nLanguage]),
nScript,
],
);
}

View File

@@ -0,0 +1,53 @@
import { h } from "hastscript";
import { visit } from "unist-util-visit";
import mermaidRenderScript from "./mermaid-render-script.js?raw";
export function rehypeMermaid() {
return (tree) => {
visit(tree, "element", (node) => {
if (
node.tagName === "div" &&
node.properties &&
node.properties.className &&
node.properties.className.includes("mermaid-container")
) {
const mermaidCode = node.properties["data-mermaid-code"] || "";
const mermaidId = `mermaid-${Math.random().toString(36).slice(-6)}`;
// 创建 Mermaid 容器
const mermaidContainer = h(
"div",
{
class: "mermaid-wrapper",
id: mermaidId,
},
[
h(
"div",
{
class: "mermaid",
"data-mermaid-code": mermaidCode,
},
mermaidCode,
),
],
);
// 创建客户端渲染脚本
const renderScript = h(
"script",
{
type: "text/javascript",
},
mermaidRenderScript,
);
// 替换原始节点
node.tagName = "div";
node.properties = { class: "mermaid-diagram-container" };
node.children = [mermaidContainer, renderScript];
}
});
};
}

View File

@@ -0,0 +1,31 @@
import { h } from "hastscript";
import { visit } from "unist-util-visit";
export function parseDirectiveNode() {
return (tree, { _data }) => {
visit(tree, (node) => {
if (
node.type === "containerDirective" ||
node.type === "leafDirective" ||
node.type === "textDirective"
) {
// biome-ignore lint/suspicious/noAssignInExpressions: <check later>
const data = node.data || (node.data = {});
node.attributes = node.attributes || {};
if (
node.children.length > 0 &&
node.children[0].data &&
node.children[0].data.directiveLabel
) {
// Add a flag to the node to indicate that it has a directive label
node.attributes["has-directive-label"] = true;
}
const hast = h(node.name, node.attributes);
data.hName = hast.tagName;
data.hProperties = hast.properties;
}
});
};
}

View File

@@ -0,0 +1,18 @@
// biome-ignore lint/suspicious/noShadowRestrictedNames: <toString from mdast-util-to-string>
import { toString } from "mdast-util-to-string";
/* Use the post's first paragraph as the excerpt */
export function remarkExcerpt() {
return (tree, { data }) => {
let excerpt = "";
for (const node of tree.children) {
if (node.type !== "paragraph") {
continue;
}
excerpt = toString(node);
break;
}
data.astro.frontmatter.excerpt = excerpt;
};
}

View File

@@ -0,0 +1,20 @@
import { visit } from "unist-util-visit";
export function remarkMermaid() {
return (tree) => {
visit(tree, "code", (node) => {
if (node.lang === "mermaid") {
// 将 mermaid 代码块转换为自定义节点类型
node.type = "mermaid";
node.data = {
hName: "div",
hProperties: {
className: ["mermaid-container"],
"data-mermaid-code": node.value,
},
};
}
});
};
}

View File

@@ -0,0 +1,16 @@
// biome-ignore lint/suspicious/noShadowRestrictedNames: <toString from mdast-util-to-string>
import { toString } from "mdast-util-to-string";
import getReadingTime from "reading-time";
export function remarkReadingTime() {
return (tree, { data }) => {
const textOnPage = toString(tree);
const readingTime = getReadingTime(textOnPage);
data.astro.frontmatter.minutes = Math.max(
1,
Math.round(readingTime.minutes),
);
data.astro.frontmatter.words = readingTime.words;
};
}

12365
src/plugins/translate.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
/**
* 强力滚动保护脚本
* 通过劫持 window.scrollTo 和相关滚动方法来阻止意外的滚动跳转
* 专门解决 Twikoo 评论系统的滚动问题
*/
(() => {
// 保存原始的滚动方法
const originalScrollTo = window.scrollTo;
const originalScrollBy = window.scrollBy;
const originalScrollIntoView = Element.prototype.scrollIntoView;
// 滚动保护状态
const scrollProtection = {
enabled: false,
allowedY: null,
startTime: 0,
duration: 0,
timeout: null,
};
// 检测是否为TOC导航触发的滚动
function checkIsTOCNavigation() {
// 检查调用堆栈看是否来自TOC组件
const stack = new Error().stack;
if (stack && (stack.includes('handleAnchorClick') || stack.includes('TOC.astro'))) {
return true;
}
// 检查最近是否有TOC点击事件
if (window.tocClickTimestamp && Date.now() - window.tocClickTimestamp < 1000) {
return true;
}
// 检查是否在TOC元素上
const activeElement = document.activeElement;
if (activeElement && activeElement.closest('#toc, .table-of-contents')) {
return true;
}
return false;
}
// 启动滚动保护
function enableScrollProtection(duration = 3000, currentY = null) {
scrollProtection.enabled = true;
scrollProtection.allowedY =
currentY !== null ? currentY : window.scrollY || window.pageYOffset;
scrollProtection.startTime = Date.now();
scrollProtection.duration = duration;
// 清除之前的定时器
if (scrollProtection.timeout) {
clearTimeout(scrollProtection.timeout);
}
// 设置保护结束时间
scrollProtection.timeout = setTimeout(() => {
scrollProtection.enabled = false;
console.log("[强力滚动保护] 保护期结束");
}, duration);
console.log(
`[强力滚动保护] 启动保护 ${duration}ms允许Y位置:`,
scrollProtection.allowedY,
);
}
// 检查滚动是否被允许
function isScrollAllowed(x, y) {
if (!scrollProtection.enabled) {
return true;
}
// 检查是否是TOC或MD导航触发的滚动
const isTOCNavigation = checkIsTOCNavigation();
if (isTOCNavigation) {
console.log('[强力滚动保护] 检测到TOC导航允许滚动');
return true;
}
// 允许小幅度的滚动调整±50像素
const tolerance = 50;
const allowedY = scrollProtection.allowedY;
if (Math.abs(y - allowedY) <= tolerance) {
return true;
}
// 如果尝试滚动到顶部y < 100而当前位置在更下方阻止
if (y < 100 && allowedY > 100) {
console.log(
"[强力滚动保护] 阻止滚动到顶部目标Y:",
y,
"允许Y:",
allowedY,
);
return false;
}
return true;
}
// 劫持 window.scrollTo
window.scrollTo = (x, y) => {
// 处理参数为对象的情况
if (typeof x === "object") {
const options = x;
x = options.left || 0;
y = options.top || 0;
}
if (isScrollAllowed(x, y)) {
originalScrollTo.call(window, x, y);
} else {
console.log("[强力滚动保护] 阻止 scrollTo:", x, y);
// 如果被阻止,滚动到允许的位置
originalScrollTo.call(window, x, scrollProtection.allowedY);
}
};
// 劫持 window.scrollBy
window.scrollBy = (x, y) => {
const currentY = window.scrollY || window.pageYOffset;
const targetY = currentY + y;
if (typeof x === "object") {
const options = x;
x = options.left || 0;
y = options.top || 0;
}
if (isScrollAllowed(x, targetY)) {
originalScrollBy.call(window, x, y);
} else {
console.log("[强力滚动保护] 阻止 scrollBy:", x, y);
}
};
// 劫持 Element.scrollIntoView
Element.prototype.scrollIntoView = function (options) {
if (!scrollProtection.enabled) {
originalScrollIntoView.call(this, options);
return;
}
// 在保护期内,尝试阻止 scrollIntoView
const rect = this.getBoundingClientRect();
const currentY = window.scrollY || window.pageYOffset;
const targetY = currentY + rect.top;
if (isScrollAllowed(0, targetY)) {
originalScrollIntoView.call(this, options);
} else {
console.log("[强力滚动保护] 阻止 scrollIntoView");
}
};
// 监听 Twikoo 相关的交互事件
document.addEventListener(
"click",
(event) => {
const target = event.target;
// 检查是否点击了TOC导航
if (target.closest('#toc, .table-of-contents') && target.closest('a[href^="#"]')) {
window.tocClickTimestamp = Date.now();
console.log('[强力滚动保护] 检测到TOC导航点击');
return; // 不启动保护允许TOC正常工作
}
// 检查是否点击了 Twikoo 相关元素
if (
target.closest("#tcomment") ||
target.matches(
".tk-action-icon, .tk-submit, .tk-cancel, .tk-preview, .tk-owo, .tk-admin, .tk-edit, .tk-delete, .tk-reply, .tk-expand",
) ||
target.closest(
".tk-action-icon, .tk-submit, .tk-cancel, .tk-preview, .tk-owo, .tk-admin, .tk-edit, .tk-delete, .tk-reply, .tk-expand",
)
) {
// 立即启动保护
enableScrollProtection(4000); // 增加保护时间到4秒
console.log("[强力滚动保护] 检测到 Twikoo 交互,启动保护");
}
// 特别检查管理面板相关操作(包括关闭操作)
if (
target.matches(
".tk-admin-panel, .tk-admin-overlay, .tk-modal, .tk-dialog, .tk-admin-close, .tk-close",
) ||
target.closest(
".tk-admin-panel, .tk-admin-overlay, .tk-modal, .tk-dialog, .tk-admin-close, .tk-close",
) ||
target.classList.contains("tk-admin") ||
target.closest(".tk-admin")
) {
enableScrollProtection(6000); // 管理面板操作保护更长时间
console.log("[强力滚动保护] 检测到 Twikoo 管理面板操作,启动长期保护");
}
// 检查是否点击了遮罩层(通常用于关闭模态框)
if (
target.classList.contains("tk-overlay") ||
target.classList.contains("tk-mask") ||
target.matches('[class*="overlay"]') ||
target.matches('[class*="mask"]') ||
target.matches('[class*="backdrop"]')
) {
// 检查是否在 Twikoo 区域内
const tcommentEl = document.querySelector("#tcomment");
if (
tcommentEl &&
(target.closest("#tcomment") || tcommentEl.contains(target))
) {
enableScrollProtection(4000);
console.log("[强力滚动保护] 检测到 Twikoo 遮罩层点击,启动保护");
}
}
},
true,
); // 使用捕获阶段
// 监听表单提交
document.addEventListener(
"submit",
(event) => {
if (event.target.closest("#tcomment")) {
enableScrollProtection(4000);
console.log("[强力滚动保护] 检测到 Twikoo 表单提交,启动保护");
}
},
true,
);
// 监听键盘事件(特别是 ESC 键,用于关闭管理面板)
document.addEventListener(
"keydown",
(event) => {
if (event.key === "Escape" || event.keyCode === 27) {
// 检查是否在 Twikoo 区域内有活动的管理面板
const tcommentEl = document.querySelector("#tcomment");
if (tcommentEl) {
// 检查是否有可见的管理面板或模态框
const adminPanel = tcommentEl.querySelector(
".tk-admin-panel, .tk-modal, .tk-dialog, [class*='admin'], [class*='modal']",
);
if (adminPanel && adminPanel.offsetParent !== null) {
// 面板可见,启动保护
enableScrollProtection(3000);
console.log(
"[强力滚动保护] 检测到 ESC 键关闭 Twikoo 管理面板,启动保护",
);
}
}
}
},
true,
);
// 监听 DOM 变化,检测管理面板的关闭
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList" || mutation.type === "attributes") {
const target = mutation.target;
// 检查是否是 Twikoo 相关的 DOM 变化
if (target.closest && target.closest("#tcomment")) {
// 检查是否有元素被移除或隐藏(可能是面板关闭)
if (
mutation.removedNodes.length > 0 ||
(mutation.type === "attributes" &&
mutation.attributeName === "style")
) {
enableScrollProtection(2000);
console.log(
"[强力滚动保护] 检测到 Twikoo DOM 变化(可能是面板关闭),启动保护",
);
}
}
}
});
});
// 开始监听 DOM 变化
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class"],
});
} else {
document.addEventListener("DOMContentLoaded", () => {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class"],
});
});
}
// 提供全局接口
window.scrollProtectionManager = {
enable: enableScrollProtection,
disable: () => {
scrollProtection.enabled = false;
if (scrollProtection.timeout) {
clearTimeout(scrollProtection.timeout);
}
console.log("[强力滚动保护] 手动停止保护");
},
isEnabled: () => scrollProtection.enabled,
getStatus: () => ({ ...scrollProtection }),
// 新增:强制保护模式(用于调试)
forceProtect: (duration = 10000) => {
enableScrollProtection(duration);
console.log(`[强力滚动保护] 强制保护模式启动 ${duration}ms`);
},
// 新增:获取当前滚动位置
getCurrentScroll: () => {
return {
x: window.scrollX || window.pageXOffset,
y: window.scrollY || window.pageYOffset,
};
},
// 新增:检测 Twikoo 状态
checkTwikooStatus: () => {
const tcomment = document.querySelector("#tcomment");
if (!tcomment) return { exists: false };
const adminPanels = tcomment.querySelectorAll(
".tk-admin-panel, .tk-modal, .tk-dialog, [class*='admin'], [class*='modal']",
);
const visiblePanels = Array.from(adminPanels).filter(
(panel) => panel.offsetParent !== null,
);
return {
exists: true,
adminPanelsCount: adminPanels.length,
visiblePanelsCount: visiblePanels.length,
hasVisiblePanels: visiblePanels.length > 0,
};
},
};
console.log("[强力滚动保护] 初始化完成");
})();

109
src/plugins/umami-share.js Normal file
View File

@@ -0,0 +1,109 @@
(function (global) {
const cacheKey = 'umami-share-cache';
const cacheTTL = 3600_000; // 1h
/**
* 获取网站统计数据
* @param {string} baseUrl - Umami Cloud API基础URL
* @param {string} apiKey - API密钥
* @param {string} websiteId - 网站ID
* @returns {Promise<object>} 网站统计数据
*/
async function fetchWebsiteStats(baseUrl, apiKey, websiteId) {
// 检查缓存
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const parsed = JSON.parse(cached);
if (Date.now() - parsed.timestamp < cacheTTL) {
return parsed.value;
}
} catch {
localStorage.removeItem(cacheKey);
}
}
const currentTimestamp = Date.now();
const statsUrl = `${baseUrl}/v1/websites/${websiteId}/stats?startAt=0&endAt=${currentTimestamp}`;
const res = await fetch(statsUrl, {
headers: {
'x-umami-api-key': apiKey
}
});
if (!res.ok) {
throw new Error('获取网站统计数据失败');
}
const stats = await res.json();
// 缓存结果
localStorage.setItem(cacheKey, JSON.stringify({ timestamp: Date.now(), value: stats }));
return stats;
}
/**
* 获取特定页面的统计数据
* @param {string} baseUrl - Umami Cloud API基础URL
* @param {string} apiKey - API密钥
* @param {string} websiteId - 网站ID
* @param {string} urlPath - 页面路径
* @param {number} startAt - 开始时间戳
* @param {number} endAt - 结束时间戳
* @returns {Promise<object>} 页面统计数据
*/
async function fetchPageStats(baseUrl, apiKey, websiteId, urlPath, startAt = 0, endAt = Date.now()) {
const statsUrl = `${baseUrl}/v1/websites/${websiteId}/stats?startAt=${startAt}&endAt=${endAt}&url=${encodeURIComponent(urlPath)}`;
const res = await fetch(statsUrl, {
headers: {
'x-umami-api-key': apiKey
}
});
if (!res.ok) {
throw new Error('获取页面统计数据失败');
}
return await res.json();
}
/**
* 获取 Umami 网站统计数据
* @param {string} baseUrl - Umami Cloud API基础URL
* @param {string} apiKey - API密钥
* @param {string} websiteId - 网站ID
* @returns {Promise<object>} 网站统计数据
*/
global.getUmamiWebsiteStats = async function (baseUrl, apiKey, websiteId) {
try {
return await fetchWebsiteStats(baseUrl, apiKey, websiteId);
} catch (err) {
throw new Error(`获取Umami统计数据失败: ${err.message}`);
}
};
/**
* 获取特定页面的 Umami 统计数据
* @param {string} baseUrl - Umami Cloud API基础URL
* @param {string} apiKey - API密钥
* @param {string} websiteId - 网站ID
* @param {string} urlPath - 页面路径
* @param {number} startAt - 开始时间戳(可选)
* @param {number} endAt - 结束时间戳(可选)
* @returns {Promise<object>} 页面统计数据
*/
global.getUmamiPageStats = async function (baseUrl, apiKey, websiteId, urlPath, startAt, endAt) {
try {
return await fetchPageStats(baseUrl, apiKey, websiteId, urlPath, startAt, endAt);
} catch (err) {
throw new Error(`获取Umami页面统计数据失败: ${err.message}`);
}
};
global.clearUmamiShareCache = function () {
localStorage.removeItem(cacheKey);
};
})(window);