2024-06-18    2024-08-19     1202 字  3 分钟

我的网站目前是基于 Hugo 框架,并结合 virgo 主题来构建的。随着内容的扩展,我可能需要在同一篇文章中展示多种编程语言的代码片段。为了提升阅读体验,我希望通过选项卡切换的方式展示不同语言的代码,这样读者可以轻松查看自己感兴趣的代码实现,而无需反复滚动页面。

为了实现这一目标,我需要实现以下功能:

  1. 多语言代码展示:在文章中能够包含多个不同语言的代码片段;每个代码片段可以通过选项卡进行切换,避免长篇幅的代码段落占用页面空间。
  2. 语法高亮:每个代码片段需要支持语法高亮,以便读者更轻松地阅读和理解代码。
  3. 用户体验:选项卡应能够通过点击进行切换,且当前选中的语言应当高亮显示。
  4. 易于编辑:在文章中插入代码片段时,编辑方式应当简单直观,并且支持 Markdown 格式。

Hugo 允许我们自定义 Shortcode,这使得在 Markdown 文档中嵌入自定义的功能变得非常方便。因此,我计划自定义一个 Shortcode 来实现上述需求,使其能够解析和渲染多语言代码片段,并通过选项卡切换展示不同的代码内容。

以下是名为 code-tabs.html 的 Shortcode:

<div class="code-tabs-container">
    <!-- 首先渲染所有内部内容,以确保所有 code-tab 已经执行 -->
    {{ .Inner }}

    <!-- 获取存储在 Scratch 中的 tabs 数据,使用 code-tabs 的唯一 id -->
    {{ $id := .Get "id" }} {{ $tabs := $.Page.Scratch.Get (printf "tabs-%s" $id)
    | default (slice) }}

    <ul class="code-tabs-list">
        {{ if gt (len $tabs) 0 }} {{ range $tabs }}
        <li class="code-tab">
            <button class="code-tab-button" data-target="tab-content-{{ .id }}">
                {{ .id | $.Page.RenderString }}
            </button>
        </li>
        {{ end }} {{ else }}
        <p>No tabs data available</p>
        {{ end }}
    </ul>

    <div class="code-tabs-contents">
        {{ range $tabs }}
        <div id="tab-content-{{ .id }}" class="code-tab-content">
            {{ highlight .content .lang "" | $.Page.RenderString }}
        </div>
        {{ end }}
    </div>
</div>

<script>
    document.addEventListener("DOMContentLoaded", function () {
        const tabContainers = document.querySelectorAll(".code-tabs-container");

        tabContainers.forEach((container) => {
            const tabs = container.querySelectorAll(".code-tab-button");
            const contents = container.querySelectorAll(".code-tab-content");

            tabs.forEach((tab, index) => {
                tab.addEventListener("click", function () {
                    tabs.forEach((t) => t.classList.remove("active"));
                    contents.forEach((c) => c.classList.remove("active"));

                    tab.classList.add("active");
                    contents[index].classList.add("active");
                });
            });

            if (tabs.length > 0) {
                tabs[0].classList.add("active");
                contents[0].classList.add("active");
            }
        });
    });
</script>

code-tab.html:

{{ $id := .Get "parentID" }}
{{ $tabs := $.Page.Scratch.Get (printf "tabs-%s" $id) | default (slice) }}
{{ $tab := dict "id" (.Get "id") "lang" (.Get "lang") "content" (.Inner | safeHTML) }}
{{ $.Page.Scratch.Set (printf "tabs-%s" $id) ($tabs | append $tab) }}
// assets/scss/partials/codeTab.scss

.code-tabs-container {
    border: 1px solid #ddd;
    border-radius: 5px;
    margin: 20px 0;
    display: flex;
    flex-direction: column;

    .code-tabs-list {
        list-style: none;
        display: flex;
        padding: 0;
        margin: 0;
        // border-bottom: 1px solid #ddd;

        .code-tab {
            // margin-right: 10px;

            .code-tab-button {
                background: none;
                border: none;
                padding: 10px 20px;
                cursor: pointer;
                font-size: 14px;
                border-bottom: 2px solid transparent;
                transition: all 0.3s ease;

                &.active {
                    border-bottom: 2px solid #007acc;
                    font-weight: bold;
                }

                &:hover {
                    color: #007acc;
                }
            }
        }
    }

    // .code-tabs-contents {
    //     // padding: 0; /* 移除上下 padding */
    //     // margin: 0; /* 确保没有额外的 margin */
    //     // flex: 1; /* 让内容部分自动填充高度 */
    //     // display: flex; /* Flex 布局 */
    //     // flex-direction: column; /* 垂直排列内容 */
    // }

    .code-tab-content {
        display: none;
        //     overflow-x: auto;
        // flex: 1; /* 让内容部分自动填充父容器的高度 */
        // max-width: 100%;
        //     white-space: pre-wrap;
        //     word-wrap: break-word;
        //     padding: 0; /* 去掉内部 padding */

        &.active {
            display: block;
        }
    }
}

基于以上设计的 Shortcode,我可以在 Markdown 文档中使用如下的方式

picture 1

实现如下的卡片式显示:


from abstractcollection import AbstractCollection

class AbstractStack(AbstractCollection):
    """An abstract stack implementation."""

    def __init__(self, sourceCollection):
        AbstractCollection.__init__(self, sourceCollection)
    def add(self, item):
        self.push(item)

#include <iostream>
using namespace std;

int main() {
    cout << "Hello, world!" << endl;
    return 0;
}

console.log("Hello, world!");

该shortcode也可以嵌入到protected shortcode中 (该块内容在输入密码:hakuna-mat 后,将显示和上面一致的内容):

Incorrect password!