Cadence-Skill 语法高亮集成
本文详细记录在 Hugo Stack 主题中集成 Cadence/SKILL 语言的 Prism.js 语法高亮功能的完整过程。
概述
Hugo Stack 主题默认使用 Chroma 进行代码高亮,但 Chroma 不支持 Cadence/SKILL 语言。为了实现该语言的语法高亮,我们采用 Prism.js 作为补充方案,通过服务端模板路由的方式,让 skill/cadence 代码块使用 Prism.js,其他语言继续使用 Chroma。
架构设计
核心思路
- 服务端模板路由:在 Hugo 渲染阶段,通过自定义
render-codeblock.html模板判断语言类型 - 双引擎共存:skill/cadence 使用 Prism.js,其他语言使用 Chroma
- 样式统一:确保 Prism.js 代码块样式与 Chroma 保持一致,支持明暗主题切换
实现架构
Markdown 代码块
↓
render-codeblock.html (服务端模板)
├─ skill/cadence → Prism.js HTML 结构
└─ 其他语言 → Chroma HTML 结构
↓
页面加载
├─ 加载 Prism.js 核心库
├─ 加载 SKILL 语法定义
└─ 调用 Prism.highlightAll() 进行高亮
↓
CSS 样式应用
├─ 统一代码块样式
└─ 支持明暗主题实现步骤
1. 创建自定义 Prism.js 语言定义
1.1 创建静态 JS 文件
创建 static/js/prism-skill.js,定义 SKILL/Cadence 语言的语法规则:
/**
* Prism.js 语言定义:SKILL/Cadence
* 基于 SKILL 语言的语法特性(Cadence SKILL 语言)
*/
(function (Prism) {
Prism.languages.skill = {
'comment': [
{
pattern: /;.*/,
greedy: true
},
{
pattern: /\/\/.*/,
greedy: true
}
],
'string': {
pattern: /"(?:[^"\\]|\\.)*"/,
greedy: true
},
'keyword': /\b(?:procedure|pro|let|when|unless|if|then|else|case|cond|for|foreach|while|until|break|continue|return|defun|defmacro|defvar|defstruct|lambda|setq|setf|car|cdr|cons|list|append|length|mapcar|apply|eval|quote|and|or|not|null|t|nil|error|warn|load|require|let\*|progn|prog1|prog2|block|catch|throw|tagbody|go|return-from|do|do\*|dolist|dotimes|loop|typecase|ccase|ecase|check-type|assert|ignore-errors|handler-case|handler-bind|restart-case|restart-bind|with-simple-restart|compute-restarts|find-restart|invoke-restart|abort|continue|muffle-warning|store-value|use-value|make-condition|signal|warn|error|cerror|break|trace|untrace|time|describe|compile|compile-file|load|require|provide|read|read-line|read-char|read-char-no-hang|peek-char|listen|unread-char|read-byte|write|prin1|print|pprint|princ|format|clear-input|clear-output|finish-output|force-output|y-or-n-p|yes-or-no-p|make-string-input-stream|make-string-output-stream|get-output-stream-string|pathname|make-pathname|pathnamep|pathname-host|pathname-device|pathname-directory|pathname-name|pathname-type|pathname-version|directory|probe-file|truename|file-author|file-write-date|rename-file|delete-file|open|stream|streamp|input-stream-p|output-stream-p|stream-element-type|close|make-broadcast-stream|make-concatenated-stream|make-echo-stream|make-synonym-stream|make-two-way-stream|get-output-stream-string|interactive-stream-p|end-of-file|readtable|readtablep|copy-readtable|make-dispatch-macro-character|set-macro-character|get-macro-character|set-dispatch-macro-character|get-dispatch-macro-character)\b/i,
'function': {
pattern: /\b[a-zA-Z_][a-zA-Z0-9_]*\s*(?=\()/,
lookbehind: false
},
'variable': {
pattern: /\b[a-zA-Z_][a-zA-Z0-9_]*\b/,
lookbehind: false
},
'number': /\b(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?\b/,
'operator': /[+\-*\/=<>!&|]+/,
'punctuation': /[()\[\]{},;]/
};
// 添加 cadence 作为 skill 的别名
Prism.languages.cadence = Prism.languages.skill;
}(Prism));语法定义说明:
- 注释:支持
;和//两种注释格式 - 字符串:双引号字符串,支持转义字符
- 关键字:包含 SKILL 常用关键字(procedure、let、if、for 等)
- 函数:识别函数名(后跟括号的标识符)
- 变量:识别变量名
- 数字:支持整数、小数、科学计数法
- 运算符:支持常见运算符
- 标点:括号、逗号等
1.2 创建 TypeScript 版本(可选)
为了更好的类型支持和代码组织,创建 assets/ts/prism-skill.ts:
/*!
* Prism.js SKILL/Cadence 语法高亮集成
* 配合服务端模板 render-codeblock.html 使用
*/
declare global {
interface Window {
Prism: any;
}
}
/**
* 初始化 Prism.js 并处理 SKILL/Cadence 代码块
* 由于使用了服务端模板,代码块已经在服务端渲染为 Prism.js 格式
* 这里只需要加载语法定义并调用 highlightAll 进行高亮
*/
export function initPrismSkill() {
// 检查是否已加载 Prism.js
if (typeof window.Prism === 'undefined') {
console.warn('Prism.js is not loaded. SKILL/Cadence syntax highlighting will not work.');
return;
}
// 加载 SKILL 语法定义
loadSkillGrammar();
// 使用 Prism.js 的高亮所有代码块(服务端模板已准备好结构)
if (window.Prism.highlightAll) {
window.Prism.highlightAll();
}
}
/**
* 加载 SKILL 语法定义
*/
function loadSkillGrammar() {
const Prism = window.Prism;
// 如果已经定义,跳过
if (Prism.languages.skill) {
return;
}
// SKILL/Cadence 语法定义(与 static/js/prism-skill.js 相同)
Prism.languages.skill = {
'comment': [
{
pattern: /;.*/,
greedy: true
},
{
pattern: /\/\/.*/,
greedy: true
}
],
'string': {
pattern: /"(?:[^"\\]|\\.)*"/,
greedy: true
},
'keyword': /\b(?:procedure|pro|let|when|unless|if|then|else|case|cond|for|foreach|while|until|break|continue|return|defun|defmacro|defvar|defstruct|lambda|setq|setf|car|cdr|cons|list|append|length|mapcar|apply|eval|quote|and|or|not|null|t|nil|error|warn|load|require|let\*|progn|prog1|prog2|block|catch|throw|tagbody|go|return-from|do|do\*|dolist|dotimes|loop|typecase|ccase|ecase|check-type|assert|ignore-errors|handler-case|handler-bind|restart-case|restart-bind|with-simple-restart|compute-restarts|find-restart|invoke-restart|abort|continue|muffle-warning|store-value|use-value|make-condition|signal|warn|error|cerror|break|trace|untrace|time|describe|compile|compile-file|load|require|provide|read|read-line|read-char|read-char-no-hang|peek-char|listen|unread-char|read-byte|write|prin1|print|pprint|princ|format|clear-input|clear-output|finish-output|force-output|y-or-n-p|yes-or-no-p|make-string-input-stream|make-string-output-stream|get-output-stream-string|pathname|make-pathname|pathnamep|pathname-host|pathname-device|pathname-directory|pathname-name|pathname-type|pathname-version|directory|probe-file|truename|file-author|file-write-date|rename-file|delete-file|open|stream|streamp|input-stream-p|output-stream-p|stream-element-type|close|make-broadcast-stream|make-concatenated-stream|make-echo-stream|make-synonym-stream|make-two-way-stream|get-output-stream-string|interactive-stream-p|end-of-file|readtable|readtablep|copy-readtable|make-dispatch-macro-character|set-macro-character|get-macro-character|set-dispatch-macro-character|get-dispatch-macro-character)\b/i,
'function': {
pattern: /\b[a-zA-Z_][a-zA-Z0-9_]*\s*(?=\()/,
lookbehind: false
},
'variable': {
pattern: /\b[a-zA-Z_][a-zA-Z0-9_]*\b/,
lookbehind: false
},
'number': /\b(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?\b/,
'operator': /[+\-*\/=<>!&|]+/,
'punctuation': /[()\[\]{},;]/
};
// 添加 cadence 作为 skill 的别名
Prism.languages.cadence = Prism.languages.skill;
}2. 创建服务端代码块渲染模板
创建 layouts/_default/_markup/render-codeblock.html,实现语言路由:
{{- $lang := .Type -}}
{{- $code := .Inner -}}
{{- /* 对于 skill 和 cadence 语言,使用 Prism.js */ -}}
{{- if or (eq $lang "skill") (eq $lang "cadence") -}}
<div class="highlight">
<pre class="language-{{ $lang }}"><code class="language-{{ $lang }}">{{ $code }}</code></pre>
</div>
{{- else -}}
{{- /* 其他语言使用 Hugo 的 Chroma 高亮 */ -}}
{{- $highlighted := highlight $code $lang -}}
<div class="highlight">
<pre class="chroma"><code>{{ $highlighted }}</code></pre>
</div>
{{- end -}}关键点:
- 使用
div.highlight容器包裹,确保与 Chroma 代码块结构一致 - skill/cadence 输出 Prism.js 所需的
pre.language-*结构 - 其他语言继续使用 Chroma 的
highlight函数
3. 加载 Prism.js 脚本
3.1 创建 Prism.js 加载模板
创建 layouts/partials/footer/prism.html:
{{- /* 加载 Prism.js 核心库和主题(仅用于 SKILL/Cadence 语法高亮) */ -}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css"></noscript>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markup.min.js"></script>
{{- /* 加载 SKILL 语法定义(从 static/js/prism-skill.js) */ -}}
<script src="{{ "js/prism-skill.js" | relURL }}"></script>3.2 在 footer 中引入
修改 layouts/partials/footer/include.html:
{{ partialCached "footer/components/script.html" . }}
{{ partialCached "footer/components/custom-font.html" . }}
{{- /* 加载 Prism.js 用于 SKILL/Cadence 语法高亮 */ -}}
{{ partial "footer/prism.html" . }}
{{ partial "footer/custom.html" . }}3.3 初始化 Prism.js
在主题的 assets/ts/main.ts 中调用初始化函数:
import { initPrismSkill } from 'ts/prism-skill';
// ... 其他代码 ...
/**
* Initialize Prism.js for SKILL/Cadence syntax highlighting
*/
// 延迟执行,确保 Prism.js 已加载
if (typeof window.Prism !== 'undefined') {
initPrismSkill();
} else {
// 如果 Prism.js 还未加载,等待加载完成
const checkPrism = setInterval(() => {
if (typeof window.Prism !== 'undefined') {
clearInterval(checkPrism);
initPrismSkill();
}
}, 100);
// 10秒后停止检查
setTimeout(() => clearInterval(checkPrism), 10000);
}4. 自定义样式
创建或修改 assets/scss/custom.scss,添加 Prism.js 代码块样式:
/*!
* Custom styles for Prism.js SKILL/Cadence syntax highlighting
* 确保 Prism.js 代码块样式与 Chroma 兼容
*/
/* Prism.js 代码块样式(服务端模板输出在 div.highlight 内) */
.article-content {
// Prism.js 代码块(服务端模板输出的格式:div.highlight > pre.language-*)
.highlight {
pre[class*="language-"] {
background-color: var(--pre-background-color);
color: var(--pre-text-color);
padding: var(--card-padding);
border-radius: var(--card-border-radius);
margin: 0;
overflow-x: auto;
font-family: var(--code-font-family);
line-height: 1.428571429;
word-break: break-all;
// 保持代码块 LTR
[dir="rtl"] & {
direction: ltr;
}
code[class*="language-"] {
color: var(--pre-text-color);
background: transparent;
border: none;
padding: 0;
font-family: var(--code-font-family);
font-size: inherit;
text-shadow: none !important; // 移除 Prism.js 默认的 text-shadow
}
// Prism.js 语法高亮颜色调整
.token.comment {
color: #6a9955; // 绿色注释
}
.token.string {
color: #ce9178; // 橙色字符串
}
.token.keyword {
color: #569cd6; // 蓝色关键字
}
.token.function {
color: #dcdcaa; // 黄色函数
}
.token.variable {
color: #9cdcfe; // 浅蓝色变量
}
.token.number {
color: #b5cea8; // 浅绿色数字
}
.token.operator {
color: #d4d4d4; // 白色操作符
}
.token.punctuation {
color: #d4d4d4; // 白色标点
}
}
}
// 确保 Chroma 和 Prism.js 代码块样式一致
.highlight,
.prism-highlight,
pre[class*="language-"] {
// 共享的复制按钮样式
.copyCodeButton {
position: absolute;
top: calc(var(--card-padding));
right: calc(var(--card-padding));
background: var(--card-background);
border: none;
box-shadow: var(--shadow-l2);
border-radius: var(--card-border-radius);
color: var(--card-text-color-main);
cursor: pointer;
font-size: 0.875rem;
padding: 0.5rem 1rem;
opacity: 0;
transition: opacity 0.2s ease-in-out;
z-index: 1;
&:hover {
opacity: 1;
}
}
&:hover {
.copyCodeButton {
opacity: 1;
}
}
}
}
// 深色模式下的 Prism.js 样式调整
[data-scheme="dark"] {
.article-content {
// 确保 dark 模式下 Prism.js 代码块背景色正确
.highlight {
// 确保容器背景色正确(覆盖 Prism.js 默认白色背景)
background-color: var(--pre-background-color) !important; // #272822
pre[class*="language-"] {
background-color: var(--pre-background-color) !important; // #272822
color: var(--pre-text-color) !important; // #f8f8f2
code[class*="language-"] {
color: var(--pre-text-color) !important;
}
.token.comment {
color: #6a9955;
}
.token.string {
color: #ce9178;
}
.token.keyword {
color: #569cd6;
}
.token.function {
color: #dcdcaa;
}
.token.variable {
color: #9cdcfe;
}
.token.number {
color: #b5cea8;
}
.token.operator {
color: #d4d4d4;
}
.token.punctuation {
color: #d4d4d4;
}
}
}
}
}样式要点:
- 使用 CSS 变量
var(--pre-background-color)和var(--pre-text-color)适配主题 - Dark 模式使用
!important覆盖 Prism.js 默认的白色背景 - 语法高亮颜色使用 VS Code 风格的配色方案
- 确保与 Chroma 代码块样式一致
文件结构
项目根目录/
├── static/
│ └── js/
│ └── prism-skill.js # Prism.js SKILL 语法定义(静态文件)
├── assets/
│ ├── ts/
│ │ └── prism-skill.ts # TypeScript 版本的语法定义和初始化
│ └── scss/
│ └── custom.scss # Prism.js 代码块样式
├── layouts/
│ ├── _default/
│ │ └── _markup/
│ │ └── render-codeblock.html # 代码块渲染模板(服务端路由)
│ └── partials/
│ └── footer/
│ ├── include.html # Footer 入口
│ └── prism.html # Prism.js 脚本加载
└── themes/
└── hugo-theme-stack/
└── assets/
└── ts/
└── main.ts # 主题主脚本(调用 initPrismSkill)使用方式
在 Markdown 文件中使用代码块时,指定语言为 skill 或 cadence:
```skill
procedure(myFunction()
let((x y)
x = 1
y = 2
println(list(x y))
)
)
```
```cadence
; 这是注释
let((x)
x = 5
)
```工作原理
渲染流程
- Markdown 解析:Hugo 解析 Markdown 中的代码块
- 模板路由:
render-codeblock.html根据语言类型选择渲染方式- skill/cadence → 输出 Prism.js HTML 结构
- 其他语言 → 使用 Chroma 高亮
- 页面加载:浏览器加载页面时执行以下步骤
- 加载 Prism.js 核心库
- 加载 SKILL 语法定义
- 调用
Prism.highlightAll()高亮所有代码块
- 样式应用:CSS 样式确保代码块外观统一
关键设计决策
- 服务端模板路由:在 Hugo 渲染阶段就决定使用哪个高亮引擎,避免客户端转换
- 双引擎共存:Prism.js 和 Chroma 互不干扰,各自处理对应的语言
- 样式统一:通过 CSS 变量和统一的结构,确保两种引擎的代码块外观一致
- Dark 模式支持:使用
!important覆盖 Prism.js 默认样式,确保 dark 模式下背景色正确
注意事项
1. Prism.js 加载顺序
确保 Prism.js 核心库在语法定义之前加载:
<!-- 1. 先加载核心库 -->
<script src="prism-core.min.js"></script>
<!-- 2. 再加载语法定义 -->
<script src="prism-skill.js"></script>2. 初始化时机
由于 Prism.js 通过 CDN 异步加载,需要在页面加载完成后检查并初始化:
if (typeof window.Prism !== 'undefined') {
initPrismSkill();
} else {
// 轮询检查,最多等待 10 秒
const checkPrism = setInterval(() => {
if (typeof window.Prism !== 'undefined') {
clearInterval(checkPrism);
initPrismSkill();
}
}, 100);
setTimeout(() => clearInterval(checkPrism), 10000);
}3. Dark 模式背景色
Prism.js 默认主题在 dark 模式下可能显示白色背景,需要通过 CSS 强制覆盖:
[data-scheme="dark"] {
.highlight {
background-color: var(--pre-background-color) !important;
pre[class*="language-"] {
background-color: var(--pre-background-color) !important;
}
}
}4. 复制按钮兼容性
确保复制按钮功能同时支持 Chroma 和 Prism.js 代码块:
// 支持 Chroma (code[data-lang]) 和 Prism.js (code.language-*)
const codeBlock = highlight.querySelector('code[data-lang]') ||
highlight.querySelector('code[class*="language-"]') ||
highlight.querySelector('code');测试验证
1. 基本功能测试
- skill 代码块正确高亮
- cadence 代码块正确高亮(作为 skill 别名)
- 其他语言代码块继续使用 Chroma
- 复制按钮功能正常
2. 样式测试
- Light 模式下代码块背景色正确
- Dark 模式下代码块背景色正确(深色,非白色)
- 语法高亮颜色正确显示
- 代码块样式与 Chroma 一致
3. 兼容性测试
- 页面加载时 Prism.js 正确初始化
- 代码块在页面加载后正确高亮
- 主题切换时样式正确更新
总结
通过以上步骤,我们成功在 Hugo Stack 主题中集成了 Cadence/SKILL 语言的 Prism.js 语法高亮支持。核心实现包括:
- 自定义语言定义:定义 SKILL/Cadence 的语法规则
- 服务端模板路由:在 Hugo 渲染阶段选择高亮引擎
- 脚本加载与初始化:加载 Prism.js 并初始化语法高亮
- 样式统一:确保 Prism.js 代码块样式与 Chroma 一致,支持明暗主题
这种实现方式既保持了与现有 Chroma 高亮的兼容性,又为特定语言提供了更好的语法高亮支持。