使用 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 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 4

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

4年前 评论
yanthink (楼主) 4年前
BY05012 (作者) 4年前
yanthink (楼主) 4年前
BY05012 (作者) 4年前
yanthink (楼主) 4年前
BY05012 (作者) 4年前
yanthink (楼主) 4年前
yanthink (楼主) 4年前
BY05012 (作者) 4年前
BY05012 (作者) 4年前
yanthink (楼主) 4年前

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

4年前 评论
yanthink (楼主) 3年前
chenzx

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

3年前 评论

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

3年前 评论
chenzx 3年前

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