1649 字
8 分钟Add commentMore actions
Fuwari添加Expressive Code渲染

前言#

Expressive Code 提供与许多不同框架的集成,包括 Astro、Starlight、Next.js 以及任何支持 rehype 插件的框架。原生代码块的实用性不强,如没有折叠代码块、diff块复合语句和标签页功能等等,因此添加Expressive Code用于代码块渲染

expressive-code
/
expressive-code
Waiting for api.github.com...
00K
0K
0K
Waiting...

Expressive Code 优点#

  • 功能齐全:完整的 VS Code 主题支持、准确的语法突出显示、编辑器和终端框架、复制到剪贴板、文本标记、可折叠部分等
  • 框架无关:不依赖 React、Vue 或任何其他前端框架。兼容 Astro 和 Next.js 等热门网站生成器,以及纯 Markdown 和 MDX
  • 基于插件:全新构建,易于扩展。所有关键功能均已默认内置,并提供强大的 API 供您创建自己的插件
  • 无障碍:设计时充分考虑了无障碍功能。自动确保合适的色彩对比度,并支持暗黑模式。兼容屏幕阅读器和键盘导航

引入 Expressive Code#

安装依赖#

切换到你的 Astro 站点根目录,在终端里安装astro-expressive-code

Terminal window
pnpm astro add astro-expressive-code
# 若需要手动更新到最新版本的话
pnpm add astro-expressive-code@latest

Astro 会提示你修改你的 astro.config.mjs 文件,一路按 y(yes) 即可

修改文件#

添加依赖#

修改 package.json 文件(虽然我们在终端已经添加了依赖,但还有官方插件没有添加,所以我们手动添加依赖以及开发依赖):

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 的核心配置

astro.config.mjs
// 添加插件和样式
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: 隐藏了库自带的复制按钮,因为开发者通过插件实现了自定义的复制按钮。
astro.config.mjs
{
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,
}
}
src/types/config.ts
//添加新配置格式
export type ExpressiveCodeConfig = {
theme: string;
};
src/utils/setting-utils.ts
import { expressiveCodeConfig } from "@/config";

同文件下在 applyThemeToDocument 函数里添加:

src/utils/setting-utils.ts
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> 标签:

src\components\misc\Markdown.astro
<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
}
});
});

添加组件#

添加以下文件:

src/plugins/expressive-code/custom-copy-button.ts
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);
},
},
});
}
src/plugins/expressive-code/language-badge.ts
/**
* 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;
}
}
}
`,
});
}
src/styles/expressive-code.css
.expressive-code .frame {
@apply !shadow-none;
}

删除原生代码自带的复制按钮样式:

src/styles/main.css
.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 样式并添加以下样式:

src\styles\markdown.css
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/
作者
p1ume
发布于
2025-06-12
许可协议
CC BY-NC-SA 4.0