使用 marked 解析 Markdown 並生成目錄導航 TOC 功能

yanthink發表於2019-09-16

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 {
  tocItems: TocItems = [];

  index: number = 0;

  constructor() {
    this.tocItems = [];
    this.index = 0;
  }

  add(text: string, level: number) {
    const anchor = `toc${level}${++this.index}`;
    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.index = 0;
  };

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

相關文章