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 { remarkShiftHeadings } from "./src/plugins/remark-shift-headings.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 { remarkExcerpt } from "./src/plugins/remark-excerpt.js"
|
||||
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"
|
||||
import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge"
|
||||
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button"
|
||||
import { unified } from "@astrojs/markdown-remark"
|
||||
import { rehypeImageLightbox } from "./src/plugins/rehype-image-lightbox.mjs"
|
||||
import { site } from "./src/data/site"
|
||||
|
||||
const createAdmonitionComponent = (type) => (properties = {}, children = []) => {
|
||||
@@ -106,7 +106,6 @@ export default defineConfig({
|
||||
rehypeComponents,
|
||||
{
|
||||
components: {
|
||||
github: GithubCardComponent,
|
||||
note: createAdmonitionComponent("note"),
|
||||
tip: createAdmonitionComponent("tip"),
|
||||
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 { ClientRouter } from "astro:transitions"
|
||||
import { analytics, homeTitle, site } from "@/data/site"
|
||||
import ImageLightbox from "@/components/ImageLightbox.astro"
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
@@ -213,6 +214,7 @@ const resolvedTwitterDescription = twitterDescription || description
|
||||
</head>
|
||||
<body class:list={["bg-background text-foreground", { "font-profile-interface": useHarmonySans }]}>
|
||||
<slot />
|
||||
<ImageLightbox />
|
||||
<script is:inline>
|
||||
// View Transitions 页面切换后重应用主题
|
||||
(function() {
|
||||
|
||||
@@ -122,115 +122,3 @@ const description = album.description ?? "相册"
|
||||
break-inside: avoid;
|
||||
}
|
||||
</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