1649 字
8 分钟Add commentMore actions
Fuwari添加Expressive Code渲染
前言
Expressive Code 提供与许多不同框架的集成,包括 Astro、Starlight、Next.js 以及任何支持 rehype 插件的框架。原生代码块的实用性不强,如没有折叠代码块、diff块复合语句和标签页功能等等,因此添加Expressive Code用于代码块渲染
Waiting for api.github.com...
Expressive Code 优点
- 功能齐全:完整的 VS Code 主题支持、准确的语法突出显示、编辑器和终端框架、复制到剪贴板、文本标记、可折叠部分等
- 框架无关:不依赖 React、Vue 或任何其他前端框架。兼容 Astro 和 Next.js 等热门网站生成器,以及纯 Markdown 和 MDX
- 基于插件:全新构建,易于扩展。所有关键功能均已默认内置,并提供强大的 API 供您创建自己的插件
- 无障碍:设计时充分考虑了无障碍功能。自动确保合适的色彩对比度,并支持暗黑模式。兼容屏幕阅读器和键盘导航
引入 Expressive Code
安装依赖
切换到你的 Astro 站点根目录,在终端里安装astro-expressive-code
pnpm astro add astro-expressive-code# 若需要手动更新到最新版本的话pnpm add astro-expressive-code@latest
Astro 会提示你修改你的 astro.config.mjs
文件,一路按 y(yes) 即可
修改文件
添加依赖
修改 package.json
文件(虽然我们在终端已经添加了依赖,但还有官方插件没有添加,所以我们手动添加依赖以及开发依赖):
"dependencies": { "astro-expressive-code": "^0.41.2", "@expressive-code/core": "^0.41.2", "@expressive-code/plugin-collapsible-sections": "^0.41.2", "@expressive-code/plugin-line-numbers": "^0.41.2",}
"devDependencies": { "@types/hast": "^3.0.4",}
NOTE分别添加核心,折叠与行号插件,还有更多插件可以去ec官网自行安装查看嗷 点击跳转
CAUTION修改完依赖后请务必在项目根目录终端运行
pnpm install
更新项目依赖,否则无法正常dev和build
注册并配置
将 Expressive Code 深度集成到项目中就要修改 astro 的核心配置
// 添加插件和样式import expressiveCode from "astro-expressive-code";import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
import { expressiveCodeConfig } from "./src/config.ts";import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge.ts";
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js";
然后在 integrations
数组中找到astro帮助你添加的 expressiveCode()
方法在括号中添加以下内容主要对代码块的外观和行为进行修改:
themes
: 设置代码块的主题。plugins
: 启用了行号、可折叠代码块、自定义语言徽章和自定义复制按钮等插件。defaultProps
: 设置了默认开启文本换行 (wrap: true),并为特定语言(如 shellsession)覆盖默认行为(不显示行号)。styleOverrides
: 这是定制化的核心。它覆盖了默认样式,使用了项目的 CSS 变量(如 var(—codeblock-bg)),确保代码块风格与网站主题完美统一。从背景色、边框、字体,到编辑器标签栏、激活状态指示器颜色,都进行了精细调整。frames
: 隐藏了库自带的复制按钮,因为开发者通过插件实现了自定义的复制按钮。
{ themes: [expressiveCodeConfig.theme, expressiveCodeConfig.theme], plugins: [ pluginCollapsibleSections(), pluginLineNumbers(), pluginLanguageBadge(), pluginCustomCopyButton() ], defaultProps: { wrap: true, overridesByLang: { 'shellsession': { showLineNumbers: false, }, }, }, styleOverrides: { codeBackground: "var(--codeblock-bg)", borderRadius: "0.75rem", borderColor: "none", codeFontSize: "0.875rem", codeFontFamily: "'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", codeLineHeight: "1.5rem", frames: { editorBackground: "var(--codeblock-bg)", terminalBackground: "var(--codeblock-bg)", terminalTitlebarBackground: "var(--codeblock-topbar-bg)", editorTabBarBackground: "var(--codeblock-topbar-bg)", editorActiveTabBackground: "none", editorActiveTabIndicatorBottomColor: "var(--primary)", editorActiveTabIndicatorTopColor: "none", editorTabBarBorderBottomColor: "var(--codeblock-topbar-bg)", terminalTitlebarBorderBottomColor: "none" }, textMarkers: { delHue: 0, insHue: 180, markHue: 250 } }, frames: { showCopyToClipboardButton: false, } }
//添加新配置格式export type ExpressiveCodeConfig = { theme: string;};
import { expressiveCodeConfig } from "@/config";
同文件下在 applyThemeToDocument
函数里添加:
export function applyThemeToDocument(theme: LIGHT_DARK_MODE) { switch (theme) { case LIGHT_MODE: document.documentElement.classList.remove("dark"); break; case DARK_MODE: document.documentElement.classList.add("dark"); break; case AUTO_MODE: if (window.matchMedia("(prefers-color-scheme: dark)").matches) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } break; }
// Set the theme for Expressive Code document.documentElement.setAttribute( "data-theme", expressiveCodeConfig.theme, );}
更改布局
更改 src\components\misc\Markdown.astro
布局文件的 <script>
标签:
<script>document.addEventListener("click", function (e: MouseEvent) { const target = e.target as Element | null; if (target && target.classList.contains("copy-btn")) { const preEle = target.closest("pre"); const codeEle = preEle?.querySelector("code"); const code = Array.from(codeEle?.querySelectorAll(".code:not(summary *)") ?? []) .map(el => el.textContent) .map(t => t === "\n" ? "" : t) .join("\n"); navigator.clipboard.writeText(code);
const timeoutId = target.getAttribute("data-timeout-id"); if (timeoutId) { clearTimeout(parseInt(timeoutId)); }
target.classList.add("success");
// 设置新的timeout并保存ID到按钮的自定义属性中 const newTimeoutId = setTimeout(() => { target.classList.remove("success"); }, 1000);
target.setAttribute("data-timeout-id", newTimeoutId.toString()); }});</script>
删除 src/layouts/Layout.astro
文件的 preElements
常量
const preElements = document.querySelectorAll('pre'); preElements.forEach((ele) => { OverlayScrollbars(ele, { scrollbars: { theme: 'scrollbar-base scrollbar-dark px-2', autoHide: 'leave', autoHideDelay: 500, autoHideSuspend: false } });});
添加组件
添加以下文件:
import { definePlugin } from "@expressive-code/core";import type { Element } from "hast";
export function pluginCustomCopyButton() { return definePlugin({ name: "Custom Copy Button", hooks: { postprocessRenderedBlock: (context) => { function traverse(node: Element) { if (node.type === "element" && node.tagName === "pre") { processCodeBlock(node); return; } if (node.children) { for (const child of node.children) { if (child.type === "element") traverse(child); } } }
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); }
traverse(context.renderData.blockAst); }, }, });}
/** * 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", baseStyles: ({ cssVar }) => ` [data-language]::before { position: absolute; z-index: 2; right: 0.5rem; top: 0.5rem; padding: 0.1rem 0.5rem; content: attr(data-language); font-size: 0.75rem; font-weight: bold; text-transform: uppercase; color: oklch(0.75 0.1 var(--hue)); background: oklch(0.33 0.035 var(--hue)); border-radius: 0.5rem; pointer-events: none; transition: opacity 0.3s; opacity: 0; } .frame:not(.has-title):not(.is-terminal) { @media (hover: none) { & [data-language]::before { opacity: 1; margin-right: 3rem; } & [data-language]:active::before { opacity: 0; } } @media (hover: hover) { & [data-language]::before { opacity: 1; } &:hover [data-language]::before { opacity: 0; } } } `, });}
.expressive-code .frame { @apply !shadow-none;}
删除原生代码自带的复制按钮样式:
.copy-btn-icon { @apply absolute top-1/2 left-1/2 transition -translate-x-1/2 -translate-y-1/2 } .copy-btn .copy-icon { @apply opacity-100 fill-white dark:fill-white/75 } .copy-btn.success .copy-icon { @apply opacity-0 fill-[var(--deep-text)] }.copy-btn .success-icon { @apply opacity-0 } .copy-btn.success .success-icon { @apply opacity-100 }
删除 markdown.css
中的 pre
样式并添加以下样式:
pre { @apply bg-[var(--codeblock-bg)] !important; @apply rounded-xl px-5;
code { @apply bg-transparent text-inherit text-sm p-0;
.copy-btn { all: initial;Add commentMore actions @apply btn-regular-dark opacity-0 shadow-lg shadow-black/50 absolute active:scale-90 h-8 w-8 top-3 right-3 text-sm rounded-lg transition-all ease-in-out z-20 cursor-pointer; } .frame:hover .copy-btn { opacity: 1; }
.copy-btn-icon { @apply absolute top-1/2 left-1/2 transition -translate-x-1/2 -translate-y-1/2 w-4 h-4 fill-white pointer-events-none; } .copy-btn .copy-icon { @apply opacity-100 fill-white dark:fill-white/75; } .copy-btn.success .copy-icon { @apply opacity-0 fill-[var(--deep-text)] } .copy-btn .success-icon { @apply opacity-0 fill-white; } .copy-btn.success .success-icon { @apply opacity-100 }
.expressive-code { @apply my-4; ::selection { @apply bg-[var(--codeblock-selection)];
在 src/styles/variables.styl
中修改
--codeblock-bg: oklch(0.2 0.015 var(--hue)) oklch(0.17 0.015 var(--hue))
--codeblock-bg: oklch(0.17 0.015 var(--hue)) oklch(0.17 0.015 var(--hue)) --codeblock-topbar-bg: oklch(0.3 0.02 var(--hue)) oklch(0.12 0.015 var(--hue))
使用 Expressive Code
Fuwari添加Expressive Code渲染
https://p1ume.vercel.app/posts/fuwari/code-express/