自定义 Markdown 代码块语法,通过注释声明高亮区域
技术markdown, DX

自定义 Markdown 代码块语法,通过注释声明高亮区域

Published On
| 更新于
Updated On
11分钟阅读概述:本文主要聚焦于对 Markdown 代码块声明中,如何支持高亮块声明的方案探究,以及最终使用了更加友好的注释声明

最前

开门见山的,推荐来自 开源 Docusaurus 下的对行间注释声明高亮块源码实现

如果业务场景有类似需求,这会是一个质量很高的实现方案参考。所以也是从这个角度写下本文,作小小分享。

背景

常规的,我们如下在 Markdown 中声明一个代码块

```js
var a = 42;
cosole.log(`the answer is ${a}`);
```

在处理输入的 markdown 源文档,最终输出一份 HTML 文档,辅以 CSS 样式表文件,最终渲染呈现在页面上如下:

var a = 42;
cosole.log(`the answer is ${a}`);

不同的人群对于自己的写作博客显然有着不同的需求,对于大众写作者来说,上面的代码块支持程度就已经能满足其使用,甚至于日常写作中基本不会接触到代码块的使用语法。

而对于服务于开发者(前端、后端、算法、运维、...)的写作博客,Markdown 支持的代码块语法显然是高频使用的内容角色。

本站点高定风格代码块

而吾辈在对本站点前期开发工作中,对代码块支持及渲染展示中,也是作了多方面细节的追求,比如如下的声明:

```js:/static/info-logo-script.js showLineNumbers focusBlur
var _t_author = "ninohx96";
// hl-11
var _t_name = "¡HolaH! Blog";
(function () {
  console.log(
    // hl-00
    `%c💎${window._t_name}%c🍉 ${window._t_author}`,
    "color:#fff;background-color:#d6409f;line-height:20px;border-radius: 5px 0 0 5px;padding: 0 8px",
    // hl-99
    "color:#fff;background-color:#3e63dd;line-height:20px;border-radius: 0 5px 5px 0;padding: 0 8px;",
  );
})();
```

最终在页面上的呈现效果如下:

/static/info-logo-script.js
var _t_author = "ninohx96";
var _t_name = "¡HolaH! Blog";
(function () {
  console.log(
    `%c💎${window._t_name}%c🍉 ${window._t_author}`,
    "color:#fff;background-color:#d6409f;line-height:20px;border-radius: 5px 0 0 5px;padding: 0 8px",
    "color:#fff;background-color:#3e63dd;line-height:20px;border-radius: 0 5px 5px 0;padding: 0 8px;",
  );
})();

算是心里预期的理想的代码块展示效果。而这当中,不可忽略的是,通过行间注释语法实现对高亮区域的声明带来的极大便捷。

Markdown 解析

本站点主要使用第三方库 mdx-bundler 作为 md 文件解析所依赖的核心库,但如果没有对 mdx 这样的格式文件的支持需求,使用另一更底层的第三方库unified 即可。两者共享同样的插件使用机制。

两套插件索引:

  1. rehype 下插件
  2. remark 下插件

下面给出一段简短的示例代码(部分字段声明作了省略)(同时如下的代码中,吾辈通过 emoji: 🎈 标注了一些必要插件)

async function md2html(markdownContent: string): Promise<string> {
  const res = await unified()
    /* ===handle on Markdown AST=== */
    .use(remarkParse) // 🎈 通用且必要项插件
    .use(remarkGfm)
    .use(remarkRehype) // 🎈 通用且必要项插件

    /* ===handle on HTML AST=== */
    .use(rehypePrism, options4RehypePrism)
    .use(rehypeStringify) // 🎈 通用且必要项插件
    .process(markdownContent);
  return res.toString();
}

总之,基于其插件生态,我们能够高度自定义的操作由 md 输入,到 html 文件输出这全过程中,密切涉及的两颗语法树:Markdown AST & HTML AST

插件应用举例

  1. 扩充 markdown 的图片插入语法

比如可以定义一个 rehype 插件扩充 markdown 的图片插入语法, 从中剥离出图像的尺寸信息,以及希望该图片交互动作下的可能存在的跳转地址。

如下的:

![a cute cat@@size=400/500@@caption=由 @placekitten 提供@@link=http://placekitten.com/](http://placekitten.com/400/500)

最终在页面中显示图片效果如下:

  1. 各级标题内嵌拼音风格的锚点链接
## 图片显示效果

则通过 rehype-slug-pinyin (本站点有如此的自定义插件)处理后,得到的结果是

<h2 id="Tu-Pian-Xian-Shi-Xiao-Guo">
  图片显示效果
  <a class="anchor" data-role="page-anchor" href="#Tu-Pian-Xian-Shi-Xiao-Guo"></a>
</h2>

处理代码块的 rehype 插件

然后下面引入一个开发者timlrx维护的服务于 md 代码块处理的第三方库: rehype-prism-plus,其对代码块语法作了丰富的扩充,本站点也用到该插件。

其支持的功能点之一,就是通过代码块头部的 meta 数据来约定高亮块,例子如下:

```js {1,3-4} showLineNumbers
function fancyAlert(arg) {
  if (arg) {
    $.facebox({ div: '#foo' })
  }
}
```

呈现效果:

function fancyAlert(arg) {
  if (arg) {
    $.facebox({ div: "#foo" });
  }
}

能够满足开发的使用,但是我却在简短的体验过程中,立马否定了这样的语法,虽然之前也了解过/使用过这样的高亮声明方案,并且在一些支持编程语言代码块的笔记工具中,高亮声明也多是如此的语法。但是在自己参与到开发中,还是想探寻有没有更优解。

先简要说为何否定该通过 meta 元数据约定高亮块的声明语法,最大的两个原因:

  1. 高亮的行号确认工作的繁重。代码简短或许还不会体验到其累赘之处,但如果开发者需要在技术分享正文中贴上一大段代码,那么高亮行号就需要手动的确认,且人为的确认,就一定存在偏差的可能。(当然也有无奈的解决办法,在将代码复制粘贴到正文中前,提前粘贴到如 vscode 这样的编辑器新打开的一个空白文档中,然后提前确认下需高亮范围的行号。)
  2. 代码变动后,行号需二次确认及更改。文档中的例子代码,总存在后期更改的情况,而如此,一开始的 meta 行号声明就不再正确,需要删去由写作者二次手动确认。

通过行间注释来声明高亮块

显然的,通过行间注释来声明高亮块的语法,则不会有上面两个显著弊端。某种程度上,也是 DX 层面-开发体验上更加友好。

更详实的说明可以点击:来自 Meta 的建站工具 Docusaurus 开源文档对于 markdown 代码块的高亮说明。

Prefer highlighting with comments where you can. By inlining highlight in the code, you don't have to manually count the lines if your code block becomes long. If you add/remove lines, you also don't have to offset your line ranges.

正题

现成轮子不再造

虽然,行间注释声明高亮块方案比 meta 元数据行号声明方案 在 DX 上来得更加友好,但是在功能实现上却来得更加的痛苦。

首先,我们需要明确的是:无论哪一种方案,我们最终都需要获得 md 文档中代码块高亮行所在的行号集合

对于 meta 元数据行号声明方案,功能开发过程中,非常简单的,从 meta 元数据中剥离出对应的行号就行。

而对于行间注释声明高亮块方案,需要处理的场景就复杂了很多,吾辈开发能力有限,所以一开始也明确了去找到学习现成的轮子。

而对于复杂在哪里,吾辈在这里简单总结如下(或许并不全面的)几点:

  1. 不同编程语言的不同注释语法,常规的 html 中的 <!-- ...word -->, 或者 bash 脚本的 # ...word,或者更常见的注释语法: // ...word/* ...word */, 还有更多。所以,功能开发中,约定的高亮标记注释需要同时兼容这些注释语法。
  2. 在逐行的解析代码块内容时,需要删去高亮标记注释本身所在行(我们当然不希望该类注释最终也出现在页面中)。这也就意味着,没有办法一遍简单的遍历,就获得高亮行号。

官方源码以及客制化

尊重开源,这里附上最终吾辈参考的开源实现——来自Docusaurus 开源代码

吾辈对其作了一些修改,以让其更适用本站点开发工作下基于 remark / rehype 插件生态下使用,其中一个主要修改就是更改了核心功能函数 #parseLines的返回值,其接收代码块下的原始内容(rawContent),而返回值更改为了如下的返回:

{
  code: "...",              /* 代码块去掉高亮注释行后的 rawContent */
  hlightLineNumList: [...], /* 高亮行号集合 */
}

附录

插件的定义及使用

本节可不作阅读,只是对核心源码#parseLines的基于 remark / rehype 插件生态下的配套使用

因为核心代码可参考如上的开源链接中对 #parseLines 的实现,而将其何处调用,服务于 MD / HTML AST 的解析和处理,实际是偏向于业务的代码了。

当然,吾辈也作开发思路上的简单分享:

  1. 首先可以声明这样一个 remark 插件,调用上面的 #parseLines 功能函数,处理效果为:更新代码块内容为去掉高亮注释行后的 rawContent,同时将高亮行号集合信息添加到代码块头部的 meta 元数据部分。添加形如: @hlightLineNumList:[2,3,5,8]

  2. 再需声明一个 rehype 插件,效果为:处理 meta 元数据中的 @hlightLineNumList 字段, 对相关高亮行所在的 dom 元素,添加约定的 CSS 类名。

someCodeLineSpan.properties.className.push("highlight-line");

THX,感谢开源,和开发者的集思广益

如下的链接,是吾辈在搜索如何实现行间注释声明高亮块这一主题中,检索到的一些较为久远的历史发表。

对于杰出的开源作品(比如 Docusaurus),和开发者的开源分享精神是由心的敬佩。

自定义 Markdown 代码块语法,通过注释声明高亮区域

https://blog.ninoh.cc/blog/a4-md-highlight-comment[Copy]
本文作者
ninohx96
创建/发布于
Published On
更新/发布于
Updated On
许可协议
CC BY-NC-SA 4.0

转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。