Cadence-Skill 语法高亮集成

在 Hugo Stack 主题中集成 Cadence/SKILL 语言的 Prism.js 语法高亮支持

Cadence-Skill 语法高亮集成

本文详细记录在 Hugo Stack 主题中集成 Cadence/SKILL 语言的 Prism.js 语法高亮功能的完整过程。

概述

Hugo Stack 主题默认使用 Chroma 进行代码高亮,但 Chroma 不支持 Cadence/SKILL 语言。为了实现该语言的语法高亮,我们采用 Prism.js 作为补充方案,通过服务端模板路由的方式,让 skill/cadence 代码块使用 Prism.js,其他语言继续使用 Chroma。

架构设计

核心思路

  1. 服务端模板路由:在 Hugo 渲染阶段,通过自定义 render-codeblock.html 模板判断语言类型
  2. 双引擎共存:skill/cadence 使用 Prism.js,其他语言使用 Chroma
  3. 样式统一:确保 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>

修改 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 文件中使用代码块时,指定语言为 skillcadence

```skill
procedure(myFunction()
  let((x y)
    x = 1
    y = 2
    println(list(x y))
  )
)
```

```cadence
; 这是注释
let((x) 
  x = 5
)
```

工作原理

渲染流程

  1. Markdown 解析:Hugo 解析 Markdown 中的代码块
  2. 模板路由render-codeblock.html 根据语言类型选择渲染方式
    • skill/cadence → 输出 Prism.js HTML 结构
    • 其他语言 → 使用 Chroma 高亮
  3. 页面加载:浏览器加载页面时执行以下步骤
    • 加载 Prism.js 核心库
    • 加载 SKILL 语法定义
    • 调用 Prism.highlightAll() 高亮所有代码块
  4. 样式应用:CSS 样式确保代码块外观统一

关键设计决策

  1. 服务端模板路由:在 Hugo 渲染阶段就决定使用哪个高亮引擎,避免客户端转换
  2. 双引擎共存:Prism.js 和 Chroma 互不干扰,各自处理对应的语言
  3. 样式统一:通过 CSS 变量和统一的结构,确保两种引擎的代码块外观一致
  4. 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 语法高亮支持。核心实现包括:

  1. 自定义语言定义:定义 SKILL/Cadence 的语法规则
  2. 服务端模板路由:在 Hugo 渲染阶段选择高亮引擎
  3. 脚本加载与初始化:加载 Prism.js 并初始化语法高亮
  4. 样式统一:确保 Prism.js 代码块样式与 Chroma 一致,支持明暗主题

这种实现方式既保持了与现有 Chroma 高亮的兼容性,又为特定语言提供了更好的语法高亮支持。