使用 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 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 4

大佬 你这个用的TypeJavascript 吗?有木有普通版的 lodash是个库吗?自己用React 写了个博客 MARKDOWN 是用marked渲染的 用的highlight的高亮,现在遇到这个目录的问题,看的你的了

5年前 评论
pingfan (楼主) 5年前
BY05012 (作者) 5年前
pingfan (楼主) 5年前
BY05012 (作者) 5年前
pingfan (楼主) 5年前
BY05012 (作者) 5年前
pingfan (楼主) 5年前
pingfan (楼主) 5年前
BY05012 (作者) 5年前
BY05012 (作者) 5年前
pingfan (楼主) 5年前

很想支持,你是怎么解决{this.renderToc(this.tocItems)} 异步数据问题的

4年前 评论
pingfan (楼主) 4年前
chenzx

这个组件有没有发布在npm啊

4年前 评论

@chenzx 没有哦~,就一个文件,具体用法你可以看我 github.com/yanthink/blog-v2/blob/m... 这个项目

4年前 评论
chenzx 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!