remarkable搭配markdown-toc自动生成博文目录
文章目录
前段时间写过一篇关于showdown和showdown-toc插件自动生成目录的博客 ⬅️点击查看
为什么要改进
showdown-toc作为专门为showdown开发的 toc 插件,搭配使用都非常方便
- 安装showdown ,showdown-toc
- toc插件集成到showdown代码中
const Showdown = require('showdown');
const showdownToc = require('showdown-toc');
const showdownHighlight = require("showdown-highlight");
const toc = [{
anchor: 'header-1',
level: 1,
text: 'header 1'
}, // # header 1
{
anchor: 'header-2',
level: 2,
text: 'header 2'
}
];
const showdown = new Showdown.Converter({
extensions: [showdownToc({
toc
}), showdownHighlight]
});
module.exports = showdown;
引用上面的工具类, showdown.render(markdown文本内容) 即可渲染,
设置toc目录也非常简单方便,
在你的markdown文本中,只要加入占位符 [toc] 即可在渲染中生成目录替换占位符.
但是, showdown-toc 有个不太友好的地方:
- 生成的目录不能嵌套
比如:
# 第一级标题
## 第二级标题
### 第三级标题
#### 第四级标题
我希望渲染后的效果是下面的:
- 第一季标题
- 第二级标题
- 第三级标题
- 第四级标题
- 第三级标题
- 第二级标题
但是实际效果并不是这样,或许是我试用中没有深入了解此插件.
导致我渲染出的结果只有一层...
所以决定尝试别的工具改进
如何改进
无意中发现了 markdown-toc 这个插件,
它的存在比较独立,也更加灵活,
你可以独立使用它,也可以和其他渲染markdown工具集成,听上去非常美好.
markdown-toc
开源地址:https://github.com/jonschlinkert/markdown-toc
安装:npm install markdown-toc --save
搭配:remarkable 渲染工具
开源地址:https://github.com/jonschlinkert/remarkable
安装:npm install remarkable --save
markdown-toc使用中遇到的问题
安装官方文档搭配 remarkable
var Remarkable = require('remarkable');
var toc = require('markdown-toc');
function render(str, options) {
return new Remarkable()
.use(toc.plugin(options)) // <= register the plugin
.render(str);
}
上面的使用方法并没有像官方文档提示的一样,render渲染后得到的json对象,并不是html (一脸懵)
开始以为是 remarkable render 方法的问题,
最后发现问题出现的 markdown-toc,在注册插件以后,render 方法返回 json 对象, 如果不注册插件,直接用 remarkable 的 render方法,一点问题没有,直接渲染成 html .
但是 markdown-toc 生成的目录确实是我需要的.
- 保证了正确的层级关系,而且没侵入性
- 不需要占位符
- 对于以前写的博文也可以直接生成目录
这决定了我想好好研究下 markdown-toc
如何解决问题
markdown-toc的返回值
直接用markdown-toc 渲染 markdown文档,返回类似下面的对象:
{
json:
[
{ content: 'AAA', slug: 'aaa', lvl: 1 },
{ content: 'BBB', slug: 'bbb', lvl: 2 },
{ content: 'CCC', slug: 'ccc', lvl: 3 }
],
tokens:
[
{
"type": "heading_open",
"tag": "h1",
"attrs": null,
"map": [
0,
1
],
"nesting": 1,
"level": 0,
"children": null,
"content": "",
"markup": "#",
"info": "",
"meta": null,
"block": true,
"hidden": false
},
{
"type": "inline",
"tag": "",
"attrs": null,
"map": [
0,
1
],
"nesting": 0,
"level": 1,
"children": [
{
"type": "text",
"tag": "",
"attrs": null,
"map": null,
"nesting": 0,
"level": 0,
"children": null,
"content": "test",
"markup": "",
"info": "",
"meta": null,
"block": false,
"hidden": false
}
],
"content": "test",
"markup": "",
"info": "",
"meta": null,
"block": true,
"hidden": false
},
{
"type": "heading_close",
"tag": "h1",
"attrs": null,
"map": null,
"nesting": -1,
"level": 0,
"children": null,
"content": "",
"markup": "#",
"info": "",
"meta": null,
"block": true,
"hidden": false
}
],
content:"markdown格式目录字符串"
}
从上面可以看到 markdown-toc 渲染markdown文本返回值中有2部分是我们可以利用的:
- content对应自动生成的目录文本(markdown格式)
- tokens数组是返回markdown文本解析出来的rules解析规则数组
那也就是说我们可以拿到rules解析规则数组后,
再让 remarkable 工具render 一次即可.
于是 remarkable 再次render一次
这次渲染包含俩部分 ( markdown-toc 渲染后拿到的 content[ markdown格式目录字符串] 和 原来的 markdown文章 )
问题再一次出现:
文章头部有了目录,也有了目录层级结构,
但是markdown原文解析后没有加上锚点.....
引入remarkable 组件 linkify
const {
linkify
} = require('remarkable/linkify');
详细代码如下:
const {
Remarkable,
} = require('remarkable');
const {
linkify
} = require('remarkable/linkify');
const hljs = require('highlight.js');
const toc = require('markdown-toc');
const md = new Remarkable('full', {
html: true,
xhtmlOut: true,
breaks: true,
langPrefix: 'language-',
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value;
} catch (err) {}
}
try {
return hljs.highlightAuto(str).value;
} catch (err) {}
return ''; // use external default escaping
}
});
md.use(linkify);
这样去 render 后,一样出现上面的问题,内容没有锚点,
几次尝试修改配置依然不行,
最后只好重新rules规则方法.
找到我们目标渲染文本对应的rule规则
markdown文本中,我们需要自动加入锚点的标记符号是:
# (H1)
## (H2)
### (H3)
#### (H4)
##### (H5)
###### (H6)
这6类标题对应渲染规则是 :heading_open
rule数组中对应的标题解析元素如下:
[
{
"type": "heading_open",
"hLevel":1,
"tag": "h1",
"attrs": null,
"map": [
0,
1
],
"nesting": 1,
"level": 0,
"children": null,
"content": "",
"markup": "#",
"info": "",
"meta": null,
"block": true,
"hidden": false
},
{
"type": "inline",
"tag": "",
"attrs": null,
"map": [
0,
1
],
"nesting": 0,
"level": 1,
"children": [
{
"type": "text",
"tag": "",
"attrs": null,
"map": null,
"nesting": 0,
"level": 0,
"children": null,
"content": "test",
"markup": "",
"info": "",
"meta": null,
"block": false,
"hidden": false
}
],
"content": "test",
"markup": "",
"info": "",
"meta": null,
"block": true,
"hidden": false
},
{
"type": "heading_close",
"hLevel":1,
"tag": "h1",
"attrs": null,
"map": null,
"nesting": -1,
"level": 0,
"children": null,
"content": "",
"markup": "#",
"info": "",
"meta": null,
"block": true,
"hidden": false
}
]
heading_open 对应
下一个元素 "type": "inline", 对应的就是 文本 test
heading_close 对应
上面3个元素对应解析渲染的html是
test
这样思路就有了,我们手动重新 heading_open 和 heading_close 俩个渲染方法
重新rule渲染方法
md.renderer.rules.heading_open = function (tokens, idx /*, options, env */ ) {
const anchorName = tokens[idx + 1].content;
const anchor = `<a name="${encodeURI(anchorName)}" ></a><br/>`;
return anchor + `<h${tokens[idx].hLevel } >`;
};
md.renderer.rules.heading_close = function (tokens, idx /*, options, env */ ) {
return '</h' + tokens[idx].hLevel + ' > ';
};
我们在
a链接锚点name值就是
包装remarkable render工具类
const {
Remarkable,
} = require('remarkable');
const {
linkify
} = require('remarkable/linkify');
const hljs = require('highlight.js');
const toc = require('markdown-toc');
const md = new Remarkable('full', {
html: true, // Enable HTML tags in source
xhtmlOut: true, // Use '/' to close single tags (<br />)
breaks: true, // Convert '\n' in paragraphs into <br>
langPrefix: 'language-', // CSS language prefix for fenced blocks
// Enable some language-neutral replacement + quotes beautification
typographer: false,
// Double + single quotes replacement pairs, when typographer enabled,
// and smartquotes on. Set doubles to '«»' for Russian, '„“' for German.
quotes: '“”‘’',
// Highlighter function. Should return escaped HTML,
// or '' if the source string is not changed
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value;
} catch (err) {}
}
try {
return hljs.highlightAuto(str).value;
} catch (err) {}
return ''; // use external default escaping
}
});
md.use(linkify);
const option = {
html: true, // Enable HTML tags in source
xhtmlOut: true, // Use '/' to close single tags (<br />)
breaks: true, // Convert '\n' in paragraphs into <br>
langPrefix: 'language-', // CSS language prefix for fenced blocks
// Enable some language-neutral replacement + quotes beautification
typographer: false,
// Double + single quotes replacement pairs, when typographer enabled,
// and smartquotes on. Set doubles to '«»' for Russian, '„“' for German.
quotes: '“”‘’',
// Highlighter function. Should return escaped HTML,
// or '' if the source string is not changed
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value;
} catch (err) {}
}
try {
return hljs.highlightAuto(str).value;
} catch (err) {}
return ''; // use external default escaping
}
}
md.renderer.rules.heading_open = function (tokens, idx /*, options, env */ ) {
const anchorName = tokens[idx + 1].content;
const anchor = `<a name="${encodeURI(anchorName)}" ></a><p style="color:white;">出自:<a style="color:white;" href="https://yijiebuyi.com/blog/50af73b51d4a400d7412a277295afa15.html" >remarkable搭配markdown-toc自动生成博文目录</a></p><br/>`;
return anchor + `<h${tokens[idx].hLevel } >`;
};
md.renderer.rules.heading_close = function (tokens, idx /*, options, env */ ) {
return '</h' + tokens[idx].hLevel + ' > ';
};
module.exports = {
md,
option,
toc
}
上面是完成的工具类,
md 是remankable 类,
option 是渲染参数,
toc 是 markdonwn-toc 类.
使用
const {
md,
toc
} = require('../utils/remarkable _helper');
const markdonwContent='这里是markdonw文章内容';
const tocMarkDown = '\n\n---\n**文章目录**\n' + toc(markDownContent).content + '\n\n---'; //自动生成的目录
const rebuildMarkDown = tocMarkDown + markdonwContent; // 目录+文章内容
blogInfo.content = md.render(rebuildMarkDown); //remarkable 渲染全部markdown 文本