UNPKG

@leisn/markdown-it-toc

Version:

Markdown-it plugin to make table of contents, default parse tag [toc].

256 lines (242 loc) 8.31 kB
import * as MarkdownIt from 'markdown-it'; declare namespace MarkdownItToc { export interface TocOptions { /** * Regular expression to detect TOC tag * ( `/\[toc\]/im` for match `[toc]` by default ). */ tocRegexp?: RegExp; /** * Tag for toc root element ( `div` by default ). */ tocTag?: string; /** * Attributes for toc root element ( `id="toc" class="toc-wrapper"` by default ). */ tocAttrs?: string; /** * Attributes for headings `h1,h2,h3...` ( `class="toc-heading"` by default ) * ``` * -------------------------- * # h1 title * -------------------------- * <h1 {headingAttrs}> * <span>h1 title</span> * </h1> * ``` */ headingAttrs?: string; /** * Attributes for headings content `span` ( `class="toc-heading-content"` by default ) * ``` * -------------------------- * # headingContent * -------------------------- * <h1> * <span {headingContentAttrs} > headingContent </span> * </h1> * ``` */ headingContentAttrs?: string; /** * Get toc result, return false if you don't want it show in the result html document, else return true(default `null`) * @param toc toc result in html */ getToc?(toc: string): boolean; /** * Get id content for forwarding in page * (e.g. for info.paths=[1,2] default return `toc-no.1-2` ) * @param info heading info */ getPathId?(info: HeadingInfo): string; /** * Content inside toc item `a` (html, include item heading text) * @param info heading info */ getTocItemContent?(info: HeadingInfo): string; /** * Content inside heading (html, before heading text) * @param info heading info * ``` * -------------------------- * # h1 title * -------------------------- * <h1> * {getHeadingPrefix} * <span>h1 title</span> * </h1> * ``` */ getHeadingPrefix?(info: HeadingInfo): string; /** * Content inside heading (html, after heading text) * @param info heading info * ``` * -------------------------- * # h1 title * -------------------------- * <h1> * <span>h1 title</span> * {getHeadingSuffix} * </h1> * ``` */ getHeadingSuffix?(info: HeadingInfo): string; } export interface HeadingInfo { /** * Heading level path * e.g. * ``` * |h1 [1] * | h2 [1,1] * | h2 [1,2] * | h3 [1,2,1] * |h1 [2] * | h2 [2,1] * ``` */ paths: number[], /** * Heading tag * e.g. `h1` , `h2` , `h3`... */ tag: string, /** * Tag in markdown * e.g. `#` , `=` , `-` */ markup: string, /** * Content of tag * e.g. for `## titlexxx` content is `titlexxx` */ content: string } } function MarkdownItToc(md: MarkdownIt, options?: MarkdownItToc.TocOptions) { var opt = Object.assign({ tocRegexp: /\[toc\]/im, tocTag: 'div', tocAttrs: 'id="toc" class="toc-wrapper"', headingAttrs: 'class="toc-heading"', headingContentAttrs: 'class="toc-heading-content"', getPathId: function (info: MarkdownItToc.HeadingInfo) { return 'toc.' + info.paths.join('-'); }, getTocItemContent: function (info: MarkdownItToc.HeadingInfo) { return '<span class="toc-item-index">' + info.paths.join('.') + '</span>'+info.content; }, getHeadingPrefix: function (info: MarkdownItToc.HeadingInfo) { return '<span class="toc-heading-prefix">' + info.paths.join('.') + '</span>'; }, getHeadingSuffix: function (info: MarkdownItToc.HeadingInfo) { return '<a href="#toc" class="toc-heading-suffix">#</a>'; }, }, options); var hInfos: MarkdownItToc.HeadingInfo[] = []; md.inline.ruler.after('emphasis', 'toc', (state, silent) => { if (silent) return false; let src = state.src; let match = opt.tocRegexp.exec(src); if (!match) return false; match.filter(m => { return m; }) const mk = match.pop(); if (!mk) return false; let token = state.push("toc_open", "toc", 1); token.markup = mk; token = state.push("toc_body", "", 0); token.content = mk; token = state.push("toc_close", "toc", -1); state.pos = state.pos + mk.length; return true; }); md.core.ruler.push('init_toc', state => { let heads = [-1, 0, 0, 0, 0, 0, 0, 0, 0, 0]; let tokens = state.tokens; tokens.forEach((token, index) => { if (token.type === 'heading_open') { let level = Math.max(parseInt(token.tag[1]), token.markup.length); if (level > 9)// ######### title level = 9; heads[level] += 1; let subPath = []; for (let i = 1; i < heads.length; i++) { if (i <= level) subPath.push(heads[i]); else heads[i] = 0; } let hPath = { paths: subPath, tag: token.tag, markup: token.markup, content: tokens[index + 1].content, } hInfos.push(hPath); } }); return true; }); md.renderer.rules.toc_open = () => { let toc = '<' + opt.tocTag + ' '; toc += opt.tocAttrs; return toc + ' >'; }; md.renderer.rules.toc_close = () => { return '</' + opt.tocTag + '>'; }; md.renderer.rules.toc_body = () => { let result = []; let lastLevel = 0; for (let i = 0; i < hInfos.length; i++) { let hinfo = hInfos[i]; let xpath = hinfo.paths; let diff = xpath.length - lastLevel; if (diff > 0) { for (let _ = 0; _ < diff; _++) result.push('<ul>'); } else if (diff < 0) { for (let _ = 0; _ > diff; _--) result.push('</ul>'); } let litem = '<li><a href="#' + opt.getPathId(hinfo) + '">'; litem += opt.getTocItemContent(hinfo); litem += '</a></li>'; result.push(litem); lastLevel = xpath.length; } for (let i = 0; i < lastLevel; i++) { result.push('</ul>'); } let html = result.join(''); if (opt.getToc) { if (!opt.getToc(html)) return ''; } return html; }; md.renderer.rules.heading_open = (tokens, index) => { let token = tokens[index]; let hinfo = hInfos[0]; if (!hinfo) return '<' + token.tag + ">"; let result = '<' + token.tag + ' id="' + opt.getPathId(hinfo) + '" ' + opt.headingAttrs + '>'; result += opt.getHeadingPrefix(hinfo); result += '<span ' + opt.headingContentAttrs + ' >'; return result; }; md.renderer.rules.heading_close = (tokens, index) => { let token = tokens[index]; let hinfo = hInfos.shift(); if (!hinfo) return '</span></' + token.tag + ">"; return '</span>' + opt.getHeadingSuffix(hinfo) + '</' + token.tag + '>'; } } export = MarkdownItToc;