跳到主要内容

remarkable搭配markdown-toc自动生成博文目录

· 阅读需 8 分钟
一介布衣
全栈开发者 / 技术写作者
  • 文章目录
  • 为什么要改进
  • 如何改进
  • 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 有个不太友好的地方:

  1. 生成的目录不能嵌套
    比如:
# 第一级标题
## 第二级标题
### 第三级标题
#### 第四级标题

我希望渲染后的效果是下面的:

  • 第一季标题
    • 第二级标题
      • 第三级标题
        • 第四级标题

但是实际效果并不是这样,或许是我试用中没有深入了解此插件.
导致我渲染出的结果只有一层...
所以决定尝试别的工具改进

  • 如何改进

  • 无意中发现了 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 = `&lt;a name="\${encodeURI(anchorName)}" &gt;&lt;/a&gt;&lt;br/&gt;`;
    return anchor + `&lt;h\${tokens[idx].hLevel } &gt;`;

    };

    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 = &lt;a name="\${encodeURI(anchorName)}" &gt;&lt;/a&gt;&lt;p style="color:white;"&gt; 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 文本