0%

关于 callout 块渲染

💡 TIP

记录一下callout支持过程。

初期想法

因为hexo支持markdown基础语法,而笔者在使用hexo之前,恰好使用过Obsidian笔记软件,非常喜欢使用Obsidian中的Callout块。

但发现网上并没有相关解决方案,虽然可以使用自定义代码块的方式书写类似callout块的效果,但是这样就和当前使用的markdown编辑器不兼容,而且自定义代码块的样式并不好看。笔者还是比较喜欢即写即渲染的编辑模式

所以,笔者就想提供一个专门用于渲染callout块的自定义渲染器,笔者曾经写过一个pythonmd渲染器,就在书写本文章不久前,也是为了解决callout块问题

笔者对callout还是非常喜爱的

实际实现

首先,安装hexo-renderer-markdown-it 以及 markdown-it-container 插件来扩展 Markdown 功能:

1
2
npm install hexo-renderer-markdown-it --save
npm install markdown-it-container --save

hexo-renderer-markdown-it 配置中启用容器解析: 修改 Hexo_config.yml 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
markdown:
render:
html: true
xhtmlOut: true
breaks: true
linkify: true
typographer: true
plugins:
- markdown-it-container
anchors:
level: 2
collisionSuffix: ''
permalink: true
permalinkClass: header-anchor
permalinkSymbol: '¶'

此时,hexo已经支持运行自定义的markdown了。

接下来,我们书写callout渲染器。

其实笔者在该部分废了老大的劲

我先贴下代码

自定义渲染器代码

二编了🤡代码最终还是没躲过bug,当然鲁棒性肯定更高了

三编了,这次是加了支持表情符的功能,这个也是typora支持的😴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
const iconConfig = {
"important": { "color": "#3178c6", "icon": "ℹ️" },
"warning": { "color": "#e5a700", "icon": "⚠️" },
"caution": { "color": "#d32f2f", "icon": "❌" },
"success": { "color": "#2e7d32", "icon": "✔️" },
"note": { "color": "#673ab7", "icon": "📝" },
"tip": { "color": "#009688", "icon": "💡" }
};

function loadIcon(iconName) {
const iconInfo = iconConfig[iconName] || iconConfig["important"];
return `<span class="callout-head-icon">${iconInfo.icon}</span>`;
}

function generateCalloutHTML(type, tokens, state) {
// 初始化 markdown-it 实例
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
breaks: true
}).use(require('markdown-it-emoji')); // 添加 Emoji 插件

// 合并渲染参数
const options = { ...state.options, breaks: true };
const wrappedTokens = [
{ type: 'paragraph_open', tag: 'p', attrs: null, map: null, nesting: 1, level: 0 },
...tokens,
{ type: 'paragraph_close', tag: 'p', attrs: null, map: null, nesting: -1, level: 0 }
];

// 渲染 Callout 内容
let calloutBody = "";
try {
calloutBody = md.renderer.render(wrappedTokens, options, state.env);
} catch (error) {
console.error("Callout rendering error:", error);
calloutBody = "<!-- Render Error -->";
}

const iconInfo = iconConfig[type] || iconConfig["important"];
return `
<div class="callout callout-${type}">
<div class="callout-head">
${loadIcon(type)}
<span class="callout-head-text" style="color:${iconInfo.color}">${type.toUpperCase()}</span>
</div>
<div class="callout-contents">
${calloutBody}
</div>
</div>`;
}

// 注册 Hexo 的 markdown-it 渲染器扩展
hexo.extend.filter.register('markdown-it:renderer', function (md) {
md.core.ruler.push('render_callout', function (state) {
const tokens = state.tokens;

// 倒序遍历 token,找到 blockquote 块
for (let i = tokens.length - 1; i >= 0; i--) {
const token = tokens[i];

// 检测到 blockquote_open 类型
if (token.type === 'blockquote_open') {
let j = i + 1;
const blockquoteTokens = [];

// 收集 blockquote 内容
while (tokens[j] && tokens[j].type !== 'blockquote_close') {
blockquoteTokens.push(tokens[j]);
j++;
}

// 确保找到了 blockquote_close
if (tokens[j]?.type === 'blockquote_close') {
let calloutType = null;
const calloutBodyTokens = [];

// 检测 blockquote 内容中的 callout 类型
blockquoteTokens.forEach(token => {
if (token.type === 'inline' && !calloutType) {
const match = token.content.match(/\[!(\w+)]/);
if (match) calloutType = match[1].toLowerCase();
} else {
calloutBodyTokens.push(token);
}
});

// 如果检测到了合法的 callout 类型
if (calloutType) {
const calloutHtml = generateCalloutHTML(calloutType, calloutBodyTokens, state);

// 替换 blockquote_open 类型为 html_block
token.type = 'html_block';
token.content = calloutHtml;

// 删除多余的 token
tokens.splice(i + 1, j - i);
}
}
}
}
});
});

其实在阅读这段代码我们能够发现,这段脚本支持用户自定义自己喜欢的callout块,这是后话

二编:发现有hexo有时候会报breaksxhtmlOut的错,最后看了一下详细的报错信息发现和这个callout脱不了干系,果不其然要改了😰

代码放置位置

然后就是代码的放置位置,虽然笔者并不了解hexo

  1. 总之,在你的博客根目录
    (虽然不知道博客根目录是否难懂,但如果跟着教程配好了博客的人一定都很聪明)

  2. 新建一个scripts文件夹当然,也可能有的大佬已经有了,萌新瑟瑟发抖

  3. 然后在该文件夹下新建文件markdown-it-callout.js当然,笔者的文件名取的是这个,但并不清楚其他名字是否可用

  4. 然后把上面那段callout渲染代码贴进去

自定义css样式

然后由于笔者使用的callout块是自己渲染的,需要自定义css样式

这个部分在客制化css有详细介绍

支持表情符部分

安装 markdown-it-emoji 插件

使用 npm 安装 markdown-it-emoji

1
npm install markdown-it-emoji

当然,如果不喜欢表情符的话,可以把.use(require('markdown-it-emoji')删除。


然后!然后!然后!你就获得了一个可以渲染callouthexo

然后就是效果展示了

其实看写在前面就可以了


(完)