remarkable搭配markdown-toc自动生成博文目录
--> -->
文章目录
- 为什么要改进
- 如何改进
- markdown-toc使用中遇到的问题
- 如何解决问题
- markdown-toc的返回值
- 找到我们目标渲染文本对应的rule规则
- 重新rule渲染方法
- 包装remarkable render工具类
- 使用
⬅️点击查看
为什么要改进
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文档,返回类似下面的对象:
{
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值就是标签的文本, 通过 tokens[idx + 1].content 来获取 (这里需要url编码和特殊符号替换)
包装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;"> 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 文本