未分类

使用 marked 解析 Markdown 并生成目录导航 TOC 功能

# 1、新建一个 tocify.tsx ```js import React from 'react'; import { Anchor } from 'antd'; import { last } from 'lodash'; const...

Published
Author
汪苗
Reading
About 1 min
Updated
使用 marked 解析 Markdown 并生成目录导航 TOC 功能
READING BGM

Article soundtrack

Press play and listen while you read.

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 () 渲染就可以了