feat: 文章正文图片支持灯箱;删除未使用的 GitHub Card 插件
灯箱 - 相册页原本已有一套无依赖灯箱(事件委托 + ClientRouter 友好), 但它写在 gallery/[album].astro 里,只有相册页能用。 现抽成 src/components/ImageLightbox.astro,挂到 BaseLayout,全站可用。 - 新增 src/plugins/rehype-image-lightbox.mjs:给正文里的 <img> 注入 data-lightbox-src。跳过链接内的图片(点击应跳转)和显式标记 data-lightbox-ignore 的图片。 - 顺带让灯箱大图继承原图的 alt 文本(原先固定为空)。 - 正文图片加 cursor: zoom-in 提示。 清理 - 删除 src/plugins/rehype-component-github-card.mjs(165 行)及其在 rehypeComponents 里的 github 映射:内容里从未使用过 ::github 指令。 验证:构建 41 页通过;文章页正文图片 4/4 带上 data-lightbox-src, 相册页 5 个触发元素不受影响;灯箱脚本按页内联(2.8KB 压缩后); 正文渲染文本与线上基线一致,无回归。
This commit is contained in:
+2
-2
@@ -15,13 +15,13 @@ import remarkMath from "remark-math"
|
|||||||
import remarkSectionize from "remark-sectionize"
|
import remarkSectionize from "remark-sectionize"
|
||||||
import { remarkShiftHeadings } from "./src/plugins/remark-shift-headings.mjs"
|
import { remarkShiftHeadings } from "./src/plugins/remark-shift-headings.mjs"
|
||||||
import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs"
|
import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs"
|
||||||
import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs"
|
|
||||||
import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js"
|
import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js"
|
||||||
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js"
|
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js"
|
||||||
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"
|
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"
|
||||||
import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge"
|
import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge"
|
||||||
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button"
|
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button"
|
||||||
import { unified } from "@astrojs/markdown-remark"
|
import { unified } from "@astrojs/markdown-remark"
|
||||||
|
import { rehypeImageLightbox } from "./src/plugins/rehype-image-lightbox.mjs"
|
||||||
import { site } from "./src/data/site"
|
import { site } from "./src/data/site"
|
||||||
|
|
||||||
const createAdmonitionComponent = (type) => (properties = {}, children = []) => {
|
const createAdmonitionComponent = (type) => (properties = {}, children = []) => {
|
||||||
@@ -106,7 +106,6 @@ export default defineConfig({
|
|||||||
rehypeComponents,
|
rehypeComponents,
|
||||||
{
|
{
|
||||||
components: {
|
components: {
|
||||||
github: GithubCardComponent,
|
|
||||||
note: createAdmonitionComponent("note"),
|
note: createAdmonitionComponent("note"),
|
||||||
tip: createAdmonitionComponent("tip"),
|
tip: createAdmonitionComponent("tip"),
|
||||||
important: createAdmonitionComponent("important"),
|
important: createAdmonitionComponent("important"),
|
||||||
@@ -138,6 +137,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
rehypeImageLightbox,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* 无依赖灯箱。
|
||||||
|
*
|
||||||
|
* 任何带 `data-lightbox-src` 的元素点击即可放大:
|
||||||
|
* - 相册页的 `<button data-lightbox-src>`
|
||||||
|
* - 文章正文里的 `<img data-lightbox-src>`(由 src/plugins/rehype-image-lightbox.mjs 注入)
|
||||||
|
*
|
||||||
|
* 用事件委托 + 每次打开时重新收集当前页的图片,所以 ClientRouter 切页后依然有效;
|
||||||
|
* 遮罩只在第一次点击时才创建,没点过就不占 DOM。
|
||||||
|
*/
|
||||||
|
---
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
/* 正文里可放大的图片给出「可点击」提示 */
|
||||||
|
.prose img[data-lightbox-src] {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const LB_ID = "vv-lightbox"
|
||||||
|
|
||||||
|
const ensureOverlay = (): HTMLElement => {
|
||||||
|
let overlay = document.getElementById(LB_ID)
|
||||||
|
if (overlay) return overlay
|
||||||
|
|
||||||
|
overlay = document.createElement("div")
|
||||||
|
overlay.id = LB_ID
|
||||||
|
overlay.setAttribute("role", "dialog")
|
||||||
|
overlay.setAttribute("aria-modal", "true")
|
||||||
|
overlay.className =
|
||||||
|
"fixed inset-0 z-[100] hidden items-center justify-center bg-black/90 backdrop-blur-sm"
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<button type="button" data-lb-close aria-label="关闭"
|
||||||
|
class="absolute right-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20">×</button>
|
||||||
|
<button type="button" data-lb-prev aria-label="上一张"
|
||||||
|
class="absolute left-3 z-10 flex h-11 w-11 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20 sm:left-6">‹</button>
|
||||||
|
<button type="button" data-lb-next aria-label="下一张"
|
||||||
|
class="absolute right-3 z-10 flex h-11 w-11 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20 sm:right-6">›</button>
|
||||||
|
<img data-lb-img src="" alt="" class="max-h-[88vh] max-w-[92vw] object-contain" />
|
||||||
|
<div data-lb-count
|
||||||
|
class="absolute bottom-5 left-1/2 -translate-x-1/2 rounded-full bg-white/10 px-3 py-1 font-mono text-xs text-white/80"></div>
|
||||||
|
`
|
||||||
|
document.body.appendChild(overlay)
|
||||||
|
return overlay
|
||||||
|
}
|
||||||
|
|
||||||
|
let sources: string[] = []
|
||||||
|
let alts: string[] = []
|
||||||
|
let current = 0
|
||||||
|
|
||||||
|
const render = (overlay: HTMLElement) => {
|
||||||
|
const img = overlay.querySelector<HTMLImageElement>("[data-lb-img]")
|
||||||
|
const counter = overlay.querySelector<HTMLElement>("[data-lb-count]")
|
||||||
|
if (img) {
|
||||||
|
img.src = sources[current] ?? ""
|
||||||
|
img.alt = alts[current] ?? ""
|
||||||
|
}
|
||||||
|
if (counter) counter.textContent = `${current + 1} / ${sources.length}`
|
||||||
|
const single = sources.length < 2
|
||||||
|
for (const sel of ["[data-lb-prev]", "[data-lb-next]"]) {
|
||||||
|
const el = overlay.querySelector<HTMLElement>(sel)
|
||||||
|
if (el) el.style.display = single ? "none" : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAt = (index: number) => {
|
||||||
|
const overlay = ensureOverlay()
|
||||||
|
current = index
|
||||||
|
render(overlay)
|
||||||
|
overlay.classList.remove("hidden")
|
||||||
|
overlay.classList.add("flex")
|
||||||
|
document.body.style.overflow = "hidden"
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
const overlay = document.getElementById(LB_ID)
|
||||||
|
if (!overlay) return
|
||||||
|
overlay.classList.add("hidden")
|
||||||
|
overlay.classList.remove("flex")
|
||||||
|
document.body.style.overflow = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = (delta: number) => {
|
||||||
|
if (sources.length < 2) return
|
||||||
|
current = (current + delta + sources.length) % sources.length
|
||||||
|
const overlay = document.getElementById(LB_ID)
|
||||||
|
if (overlay) render(overlay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(window as unknown as { __vvLightboxBound?: boolean }).__vvLightboxBound) {
|
||||||
|
;(window as unknown as { __vvLightboxBound?: boolean }).__vvLightboxBound = true
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
const target = event.target as Element | null
|
||||||
|
|
||||||
|
const trigger = target?.closest<HTMLElement>("[data-lightbox-src]")
|
||||||
|
if (trigger) {
|
||||||
|
// 每次打开都重新收集当前页的图片
|
||||||
|
const all = Array.from(document.querySelectorAll<HTMLElement>("[data-lightbox-src]"))
|
||||||
|
sources = all.map((el) => el.dataset.lightboxSrc ?? "")
|
||||||
|
alts = all.map((el) => (el instanceof HTMLImageElement ? el.alt : ""))
|
||||||
|
openAt(Number(trigger.dataset.lightboxIndex ?? all.indexOf(trigger)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target?.closest("[data-lb-close]")) return close()
|
||||||
|
if (target?.closest("[data-lb-prev]")) return step(-1)
|
||||||
|
if (target?.closest("[data-lb-next]")) return step(1)
|
||||||
|
|
||||||
|
// 点背景关闭(点图片本身不关)
|
||||||
|
const overlay = document.getElementById(LB_ID)
|
||||||
|
if (overlay && target === overlay) close()
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
const overlay = document.getElementById(LB_ID)
|
||||||
|
if (!overlay || overlay.classList.contains("hidden")) return
|
||||||
|
if (event.key === "Escape") close()
|
||||||
|
if (event.key === "ArrowLeft") step(-1)
|
||||||
|
if (event.key === "ArrowRight") step(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切页时收拾干净,避免残留遮罩锁住滚动
|
||||||
|
document.addEventListener("astro:page-load", () => {
|
||||||
|
const overlay = document.getElementById(LB_ID)
|
||||||
|
if (overlay) overlay.remove()
|
||||||
|
document.body.style.overflow = ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -6,6 +6,7 @@ import "katex/dist/katex.min.css"
|
|||||||
import "@/styles/global.css"
|
import "@/styles/global.css"
|
||||||
import { ClientRouter } from "astro:transitions"
|
import { ClientRouter } from "astro:transitions"
|
||||||
import { analytics, homeTitle, site } from "@/data/site"
|
import { analytics, homeTitle, site } from "@/data/site"
|
||||||
|
import ImageLightbox from "@/components/ImageLightbox.astro"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string
|
title?: string
|
||||||
@@ -213,6 +214,7 @@ const resolvedTwitterDescription = twitterDescription || description
|
|||||||
</head>
|
</head>
|
||||||
<body class:list={["bg-background text-foreground", { "font-profile-interface": useHarmonySans }]}>
|
<body class:list={["bg-background text-foreground", { "font-profile-interface": useHarmonySans }]}>
|
||||||
<slot />
|
<slot />
|
||||||
|
<ImageLightbox />
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
// View Transitions 页面切换后重应用主题
|
// View Transitions 页面切换后重应用主题
|
||||||
(function() {
|
(function() {
|
||||||
|
|||||||
@@ -122,115 +122,3 @@ const description = album.description ?? "相册"
|
|||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* 无依赖灯箱。
|
|
||||||
* 用事件委托 + 每次打开时重新收集当前页的照片,因此 ClientRouter 切页后依然有效。
|
|
||||||
*/
|
|
||||||
const LB_ID = "vv-lightbox"
|
|
||||||
|
|
||||||
const ensureOverlay = (): HTMLElement => {
|
|
||||||
let overlay = document.getElementById(LB_ID)
|
|
||||||
if (overlay) return overlay
|
|
||||||
|
|
||||||
overlay = document.createElement("div")
|
|
||||||
overlay.id = LB_ID
|
|
||||||
overlay.setAttribute("role", "dialog")
|
|
||||||
overlay.setAttribute("aria-modal", "true")
|
|
||||||
overlay.className =
|
|
||||||
"fixed inset-0 z-[100] hidden items-center justify-center bg-black/90 backdrop-blur-sm"
|
|
||||||
overlay.innerHTML = `
|
|
||||||
<button type="button" data-lb-close aria-label="关闭"
|
|
||||||
class="absolute right-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20">×</button>
|
|
||||||
<button type="button" data-lb-prev aria-label="上一张"
|
|
||||||
class="absolute left-3 z-10 flex h-11 w-11 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20 sm:left-6">‹</button>
|
|
||||||
<button type="button" data-lb-next aria-label="下一张"
|
|
||||||
class="absolute right-3 z-10 flex h-11 w-11 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20 sm:right-6">›</button>
|
|
||||||
<img data-lb-img src="" alt="" class="max-h-[88vh] max-w-[92vw] object-contain" />
|
|
||||||
<div data-lb-count
|
|
||||||
class="absolute bottom-5 left-1/2 -translate-x-1/2 rounded-full bg-white/10 px-3 py-1 font-mono text-xs text-white/80"></div>
|
|
||||||
`
|
|
||||||
document.body.appendChild(overlay)
|
|
||||||
return overlay
|
|
||||||
}
|
|
||||||
|
|
||||||
let sources: string[] = []
|
|
||||||
let current = 0
|
|
||||||
|
|
||||||
const render = (overlay: HTMLElement) => {
|
|
||||||
const img = overlay.querySelector<HTMLImageElement>("[data-lb-img]")
|
|
||||||
const counter = overlay.querySelector<HTMLElement>("[data-lb-count]")
|
|
||||||
if (img) img.src = sources[current] ?? ""
|
|
||||||
if (counter) counter.textContent = `${current + 1} / ${sources.length}`
|
|
||||||
const single = sources.length < 2
|
|
||||||
for (const sel of ["[data-lb-prev]", "[data-lb-next]"]) {
|
|
||||||
const el = overlay.querySelector<HTMLElement>(sel)
|
|
||||||
if (el) el.style.display = single ? "none" : ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openAt = (index: number) => {
|
|
||||||
const overlay = ensureOverlay()
|
|
||||||
current = index
|
|
||||||
render(overlay)
|
|
||||||
overlay.classList.remove("hidden")
|
|
||||||
overlay.classList.add("flex")
|
|
||||||
document.body.style.overflow = "hidden"
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
const overlay = document.getElementById(LB_ID)
|
|
||||||
if (!overlay) return
|
|
||||||
overlay.classList.add("hidden")
|
|
||||||
overlay.classList.remove("flex")
|
|
||||||
document.body.style.overflow = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const step = (delta: number) => {
|
|
||||||
if (sources.length < 2) return
|
|
||||||
current = (current + delta + sources.length) % sources.length
|
|
||||||
const overlay = document.getElementById(LB_ID)
|
|
||||||
if (overlay) render(overlay)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(window as unknown as { __vvLightboxBound?: boolean }).__vvLightboxBound) {
|
|
||||||
;(window as unknown as { __vvLightboxBound?: boolean }).__vvLightboxBound = true
|
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
|
||||||
const target = event.target as Element | null
|
|
||||||
|
|
||||||
const trigger = target?.closest<HTMLElement>("[data-lightbox-src]")
|
|
||||||
if (trigger) {
|
|
||||||
// 每次打开都重新收集当前页的照片
|
|
||||||
const all = Array.from(document.querySelectorAll<HTMLElement>("[data-lightbox-src]"))
|
|
||||||
sources = all.map((el) => el.dataset.lightboxSrc ?? "")
|
|
||||||
openAt(Number(trigger.dataset.lightboxIndex ?? all.indexOf(trigger)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target?.closest("[data-lb-close]")) return close()
|
|
||||||
if (target?.closest("[data-lb-prev]")) return step(-1)
|
|
||||||
if (target?.closest("[data-lb-next]")) return step(1)
|
|
||||||
|
|
||||||
// 点背景关闭(点图片本身不关)
|
|
||||||
const overlay = document.getElementById(LB_ID)
|
|
||||||
if (overlay && target === overlay) close()
|
|
||||||
})
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
|
||||||
const overlay = document.getElementById(LB_ID)
|
|
||||||
if (!overlay || overlay.classList.contains("hidden")) return
|
|
||||||
if (event.key === "Escape") close()
|
|
||||||
if (event.key === "ArrowLeft") step(-1)
|
|
||||||
if (event.key === "ArrowRight") step(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 切页时收拾干净,避免残留遮罩锁住滚动
|
|
||||||
document.addEventListener("astro:page-load", () => {
|
|
||||||
const overlay = document.getElementById(LB_ID)
|
|
||||||
if (overlay) overlay.remove()
|
|
||||||
document.body.style.overflow = ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
/// <reference types="mdast" />
|
|
||||||
// @ts-nocheck
|
|
||||||
import { h } from "hastscript";
|
|
||||||
|
|
||||||
// GitHub language colors (subset of most common languages)
|
|
||||||
const LANGUAGE_COLORS = {
|
|
||||||
JavaScript: "#f1e05a",
|
|
||||||
TypeScript: "#3178c6",
|
|
||||||
Python: "#3572A5",
|
|
||||||
Java: "#b07219",
|
|
||||||
"C++": "#f34b7d",
|
|
||||||
C: "#555555",
|
|
||||||
"C#": "#178600",
|
|
||||||
Go: "#00ADD8",
|
|
||||||
Rust: "#dea584",
|
|
||||||
Ruby: "#701516",
|
|
||||||
PHP: "#4F5D95",
|
|
||||||
Swift: "#F05138",
|
|
||||||
Kotlin: "#A97BFF",
|
|
||||||
Dart: "#00B4AB",
|
|
||||||
Scala: "#c22d40",
|
|
||||||
Shell: "#89e051",
|
|
||||||
HTML: "#e34c26",
|
|
||||||
CSS: "#563d7c",
|
|
||||||
SCSS: "#c6538c",
|
|
||||||
Vue: "#41b883",
|
|
||||||
Svelte: "#ff3e00",
|
|
||||||
Astro: "#ff5a03",
|
|
||||||
MDX: "#fcb32c",
|
|
||||||
Markdown: "#083fa1",
|
|
||||||
JSON: "#292929",
|
|
||||||
YAML: "#cb171e",
|
|
||||||
Dockerfile: "#384d54",
|
|
||||||
Makefile: "#427819",
|
|
||||||
Lua: "#000080",
|
|
||||||
Perl: "#0298c3",
|
|
||||||
R: "#198CE7",
|
|
||||||
Julia: "#a270ba",
|
|
||||||
Haskell: "#5e5086",
|
|
||||||
Elixir: "#6e4a7e",
|
|
||||||
Clojure: "#db5855",
|
|
||||||
Objective_C: "#438eff",
|
|
||||||
"Objective-C": "#438eff",
|
|
||||||
Jupyter_Notebook: "#DA5B0B",
|
|
||||||
"Jupyter Notebook": "#DA5B0B",
|
|
||||||
Vim_Script: "#199f4b",
|
|
||||||
TeX: "#3D6117",
|
|
||||||
PowerShell: "#012456",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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" },
|
|
||||||
"—",
|
|
||||||
);
|
|
||||||
|
|
||||||
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" },
|
|
||||||
"Loading...",
|
|
||||||
);
|
|
||||||
|
|
||||||
const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "—");
|
|
||||||
const nForks = h(`div#${cardUuid}-forks`, { class: "gc-forks" }, "—");
|
|
||||||
const nLicense = h(`div#${cardUuid}-license`, { class: "gc-license" }, "—");
|
|
||||||
|
|
||||||
const nScript = h(
|
|
||||||
`script#${cardUuid}-script`,
|
|
||||||
{ type: "text/javascript", defer: true, "data-astro-rerun": true },
|
|
||||||
`
|
|
||||||
(function() {
|
|
||||||
const __gcLangColorsMap = ${JSON.stringify(LANGUAGE_COLORS)};
|
|
||||||
const debugEnabled = () => localStorage.getItem("debugGithubCard") === "1";
|
|
||||||
const debug = (...args) => { if (debugEnabled()) console.log("[GITHUB-CARD]", ...args); };
|
|
||||||
const cardEl = document.getElementById('${cardUuid}-card');
|
|
||||||
if (cardEl && cardEl.dataset.githubLoaded === "true") {
|
|
||||||
// already loaded by fallback loader
|
|
||||||
debug("skip (already loaded)", "${repo}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cardEl) cardEl.dataset.githubLoaded = "loading";
|
|
||||||
debug("inline fetch", "${repo}");
|
|
||||||
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, '') || "No description provided";
|
|
||||||
const lang = data.language || "";
|
|
||||||
const langEl = document.getElementById('${cardUuid}-language');
|
|
||||||
langEl.innerText = lang || "—";
|
|
||||||
if (lang && __gcLangColorsMap[lang]) {
|
|
||||||
langEl.style.setProperty('--gc-lang-color', __gcLangColorsMap[lang]);
|
|
||||||
}
|
|
||||||
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 || "—";
|
|
||||||
document.getElementById('${cardUuid}-card').classList.remove("fetch-waiting");
|
|
||||||
if (cardEl) cardEl.dataset.githubLoaded = "true";
|
|
||||||
debug("inline loaded", "${repo}");
|
|
||||||
}).catch(() => {
|
|
||||||
const c = document.getElementById('${cardUuid}-card');
|
|
||||||
c?.classList.add("fetch-error");
|
|
||||||
if (cardEl) cardEl.dataset.githubLoaded = "error";
|
|
||||||
console.warn("[GITHUB-CARD] Error loading card for ${repo}");
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return h(
|
|
||||||
`a#${cardUuid}-card`,
|
|
||||||
{
|
|
||||||
class: "card-github fetch-waiting not-prose",
|
|
||||||
href: `https://github.com/${repo}`,
|
|
||||||
target: "_blank",
|
|
||||||
rel: "noopener noreferrer",
|
|
||||||
"data-github-card": "",
|
|
||||||
"data-github-repo": repo,
|
|
||||||
repo,
|
|
||||||
},
|
|
||||||
[
|
|
||||||
nTitle,
|
|
||||||
nDescription,
|
|
||||||
h("div", { class: "gc-infobar" }, [nStars, nForks, nLicense, nLanguage]),
|
|
||||||
nScript,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* 给正文里的图片注入 `data-lightbox-src`,交给全局灯箱(ImageLightbox.astro)处理。
|
||||||
|
*
|
||||||
|
* 两种情况跳过:
|
||||||
|
* - 图片外面包着 <a>:点击应该跳转,而不是放大
|
||||||
|
* - 图片上有 `data-lightbox-ignore`:明确表示不要放大
|
||||||
|
*
|
||||||
|
* 不用 unist-util-visit,是因为这里需要知道祖先里有没有 <a>,
|
||||||
|
* 手写一次递归比引入 visit-parents 更直白。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @param {import('hast').Root} tree */
|
||||||
|
export function rehypeImageLightbox() {
|
||||||
|
/**
|
||||||
|
* @param {any} node
|
||||||
|
* @param {boolean} insideLink
|
||||||
|
*/
|
||||||
|
const walk = (node, insideLink) => {
|
||||||
|
if (!node || typeof node !== "object") return
|
||||||
|
|
||||||
|
if (node.type === "element") {
|
||||||
|
if (node.tagName === "img") {
|
||||||
|
// 链接里的图片保持跳转行为
|
||||||
|
if (insideLink) return
|
||||||
|
const props = node.properties || (node.properties = {})
|
||||||
|
if (props["data-lightbox-ignore"] !== undefined) return
|
||||||
|
const src = props.src
|
||||||
|
if (typeof src !== "string" || src.length === 0) return
|
||||||
|
if (props["data-lightbox-src"] === undefined) {
|
||||||
|
props["data-lightbox-src"] = src
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (node.tagName === "a") insideLink = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(node.children)) {
|
||||||
|
for (const child of node.children) walk(child, insideLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (tree) => {
|
||||||
|
walk(tree, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user