博客园Markdown编辑器实现多语言代码块

public class Main {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
print("Hello, World!")
#include <iostream>

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

最终效果


步骤

0xFF 前置条件

需要在 博客后台#基本设置 中申请JS权限

0x00 代码块HTML格式

对于一个 .md 格式的代码块

```java
public class Main {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
```

在发布后会被markdown.toHTML(input)函数转换为如下HTML标记

<pre data-highlight-status="highlighted" class="highlighter-hljs" data-dark-theme="true">
<code class="language-java highlighter-hljs hljs" data-dark-theme="true"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Main</span> {

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> {
        System.out.println(<span class="hljs-string">"Hello, World!"</span>);
    }
}
</code>
</pre>

对于不同的格式设置,标记对应的 prop 和 attr 也会有所不同,但总体都能抽象为如下结构

<pre>
  <code class="language-*">
    <!--...-->
  </code>
</pre>

这里不对没有指定 Syntax Highlighting 的情况作讨论,因此我们可以一般地认为一个代码块的DOM,可以用 jQuery 选择器通过code[class^=language-]模糊查询定位到

0x01 .md 定义

理论上,单纯的通过 Markdown 对 web 的支持,可以不通过 JS 就实现多语言代码块这个功能,但这样泛用性就会大打折扣,每一次使用应都会复写一定量前端代码

因此我们简单定义<div class=__tabs />为多语言代码块的容器,当其中仅包含指定 Syntax Highlighting 的 Markdown 代码块时,通过 JS 将其转换成多语言代码块

即该文最终效果对应的 .md 源代码应为

<div class=__tabs>

```java
public class Main {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
```

```python
print("Hello, World!")
```

```cpp
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}
```
</div>

0x02 常量定义

const __prefix = 'language-' // 定位 code 标签其 class 属性的前缀
const __tabs__nav__height = '40px' // 多语言代码块 Header 高度

const __regularExp = new RegExp(`\\b${__prefix}\\w+\\b`) // 截取包含前缀的 class 所用的正则表达式

/*
* parse(clazz):根据 class 获取 nav item 的 label
* other:自定义 label 的对应关系
*/
const __label = {
    'language-java': 'Java',
    parse: function(clazz) {
        return this[clazz] ?? clazz.slice(__prefix.length)
    }
}

0x03 完整代码

由于自定义页脚 HTML 至少是在 DOM 完全就绪后才插入的,因此直接操作<div class=__tabs />即可,对应代码

<script type="text/javascript">
const __prefix = 'language-'
const __tabs__nav__height = '40px'

const __regularExp = new RegExp(`\\b${__prefix}\\w+\\b`)

const __label = {
    'language-java': 'Java',
    parse: function(clazz) {
        return this[clazz] ?? clazz.slice(__prefix.length)
    }
}

$('.__tabs').each(function() {
    let flag = true
    const containers = []
    $(this).find(`code[class*=${__prefix}]`).each(function() {
        const matched = $(this).attr('class').match(__regularExp)
        if (!matched || matched.length != 1) {
            flag = false
        }
        const container = $(this).parent()
        if (container.prop('tagName') !== 'PRE') {
            flag = false
        }
        if (!flag) return false
        containers.push({
            label: __label.parse(matched[0]),
            dom: container
        })
    })
    if (!flag || !containers.length) return true
    const header = $('<ul>', {
        class: '__tabs__nav-wrap',
        css: {
            listStyle: 'none',
            userSelect: 'none',
            height: __tabs__nav__height,
            fontSize: 0,
            margin: 0,
            padding: 0
        }
    })
    let actived;
    $.each(containers, function(index, container) {
        const item = $('<li>', {
            text: container.label,
            class: index ? '__tabs__item' : '__tabs__item is-active',
            css: {
                display: 'inline-block',
                position: 'relative',
                lineHeight: __tabs__nav__height,
                fontSize: '16px',
                margin: 0,
                padding: '0 8px'
            }
        })
        container.dom.css('margin-top', 0)
        if (index) container.dom.hide()
        else actived = item
        item.click(function() {
            if (actived == item) return
            $.each(containers, (_, container) => container.dom.hide())
            actived.removeClass('is-active')
            item.addClass('is-active')
            actived = item
            container.dom.show()
        })
        header.append(item)
    })
    $(this).prepend(header)
})
</script>

稍微在页面定制 CSS 代码修饰一下

.__tabs {
    border: 1px solid #e5e5e5;
    border-radius: .5rem;
    overflow: hidden;
}

.__tabs__nav-wrap {
    color: #3c3c434d;
}

.__tabs__item {
    padding: 0 12px !important;
}

.__tabs__item.is-active, .__tabs__item:hover {
    color: #262626;
}

.__tabs__item~.__tabs__item:before {
    background-color: #0000000d;
    content: " ";
    height: 12px;
    left: 0;
    position: absolute;
    top: calc(50% - 6px);
    width: 1px;
}

需要自取,当然能标明出处更好

posted @ 2024-06-25 21:44  肖有量  阅读(229)  评论(0编辑  收藏  举报

我永远喜欢中野二乃