使用 marked 解析 Markdown 并生成目录导航 TOC 功能
1、新建一个 tocify.tsx
import React from 'react';
import { Anchor } from 'antd';
import { last } from 'lodash';
const { Link } = Anchor;
export interface TocItem {
anchor: string;
level: number;
text: string;
children?: TocItem[];
}
export type TocItems = TocItem[]; // TOC目录树结构
export default class Tocify {
anchors: string[];
tocItems: TocItems = [];
constructor() {
this.anchors = [];
this.tocItems = [];
}
add(text: string, level: number, id: string = '') {
const count = this.anchors.filter(anchor => anchor === text).length;
const anchor = id || (count ? `${text}${count}` : text);
this.anchors.push(anchor);
const item = { anchor, level, text };
const items = this.tocItems;
if (items.length === 0) { // 第一个 item 直接 push
items.push(item);
} else {
let lastItem = last(items) as TocItem; // 最后一个 item
if (item.level > lastItem.level) { // item 是 lastItem 的 children
for (let i = lastItem.level + 1; i <= 6; i++) {
const { children } = lastItem;
if (!children) { // 如果 children 不存在
lastItem.children = [item];
break;
}
lastItem = last(children) as TocItem; // 重置 lastItem 为 children 的最后一个 item
if (item.level <= lastItem.level) { // item level 小于或等于 lastItem level 都视为与 children 同级
children.push(item);
break;
}
}
} else { // 置于最顶级
items.push(item);
}
}
return anchor;
}
reset = () => {
this.tocItems = [];
this.anchors = [];
};
renderToc(items: TocItem[]) { // 递归 render
return items.map(item => (
<Link key={item.anchor} href={`#${item.anchor}`} title={item.text}>
{item.children && this.renderToc(item.children)}
</Link>
));
}
render() {
return (
<Anchor style={{ padding: 24 }} affix showInkInFixed>
{this.renderToc(this.tocItems)}
</Anchor>
);
}
}
2、重写 renderer.heading
import marked from 'marked';
import Tocify from './tocify';
const tocify = new Tocify();
const renderer = new marked.Renderer();
renderer.heading = function(text, level, raw) {
const anchor = tocify.add(text, level);
return `<a id="${anchor}" href="#${anchor}" class="anchor-fix"><h${level}>${text}</h${level}></a>\n`;
};
marked.setOptions({ renderer });
3、最后代码实现
(props) => (
<div>
<div
className="content"
dangerouslySetInnerHTML={{ __html: marked(props.content) }}
/>
<div className="toc">{tocify && tocify.render()}</div>
</div>
)
markdown 解析的时候会通过 rendeer.heading 解析标题,然后我们在 rendeer.heading 使用 tocify.add 来生成目录树(根据 level)并返回一个锚点,rendeer.heading 再根据这个锚点生成一个a链接,最后我们调用 tocify.render() 渲染就可以了
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: