UNPKG

markdown-it-ftoc

Version:

markdown-it plugin to add toc and anchor links in headings

182 lines (164 loc) 5.11 kB
/** * markdown-it-toc * https://github.com/tylerlong/markdown-it-toc * modified by fchengjin@126.com @2019-06-18 12:23:00 * @param {MarkdownIt} md markdown实例 * @param {Object} opt 可选参数 */ var makeSafe = function (label) { return label.replace(/[^\w\s]/gi, '').split(' ').join('_') } module.exports = function (md, opt) { const options = { tocClassName: 'markdown-toc', tocFirstLevel: 1, tocLastLevel: 6, tocCallback: null, anchorLinkSymbol: '#', anchorLinkSymbolClassName: 'header-anchor', anchorClassName: 'markdown-toc-anchor', anchorLinkSpace: true, slugifyArguments: [ { allowedChars: '_-' } ], ...opt } const slugifyFn = (typeof options.slugify === 'function' && options.slugify) || makeSafe var TOC_REGEXP = /^@\[toc\](?:\((?:\s+)?([^)]+)(?:\s+)?\)?)?(?:\s+?)?$/im var TOC_DEFAULT = '' const TOC = '@[toc]' let headingIds = {} let gstate let tocHtml = '' function toc (state, silent) { while (state.src.indexOf('\n') >= 0 && state.src.indexOf('\n') < state.src.indexOf('@[toc]')) { if (state.tokens.slice(-1)[0].type === 'softbreak') { state.src = state.src.split('\n').slice(1).join('\n') state.pos = 0 } } let token // trivial rejections if (state.src.charCodeAt(state.pos) !== 0x40 /* @ */) { return false } if (state.src.charCodeAt(state.pos + 1) !== 0x5B /* [ */) { return false } let match = TOC_REGEXP.exec(state.src) if (!match) { return false } match = match.filter(function (m) { return m }) if (match.length < 1) { return false } if (silent) { // don't run any pairs in validation mode return false } token = state.push('toc_open', 'toc', 1) token.markup = TOC token = state.push('toc_body', '', 0) var label = TOC_DEFAULT if (match.length > 1) { label = match.pop() } token.content = label token = state.push('toc_close', 'toc', -1) let offset = 0 const newline = state.src.indexOf('\n') if (newline !== -1) { offset = state.pos + newline } else { offset = state.pos + state.posMax + 1 } state.pos = offset return true } function createId (string) { const key = slugifyFn(string, ...options.slugifyArguments)// slugify if (!headingIds[key]) { headingIds[key] = 0 } headingIds[key]++ return key + (headingIds[key] > 1 ? `-${headingIds[key]}` : '') } md.renderer.rules.heading_open = function (tokens, index) { const level = tokens[index].tag const label = tokens[index + 1] if (label.type === 'inline') { // const anchor = label.map[0]; const id = label.id return `<${level} id="${id}"><a href="#${id}" ${options.anchorLinkSymbolClassName ? ' class="' + options.anchorLinkSymbolClassName + '"' : ''}>${options.anchorLinkSymbol}</a>${options.anchorLinkSpace ? ' ' : ''}` } return '</h1>' } md.renderer.rules.toc_open = function () { return '' } md.renderer.rules.toc_close = function () { return '' } md.renderer.rules.toc_body = function (tokens, index) { return '<h3>' + tokens[index].content + '</h3>' + tocHtml } md.core.ruler.push('grab_state', function (state) { gstate = state headingIds = {} tocHtml = renderToc() }) const renderToc = function () { // Wanted to avoid linear search through tokens here, // but this seems the only reliable way to identify headings const headings = [] const gtokens = gstate.tokens const size = gtokens.length for (let i = 0; i < size; i++) { if (gtokens[i].type !== 'heading_close') { continue } const token = gtokens[i] const heading = gtokens[i - 1] if (heading.type === 'inline') { const id = createId(heading.content) heading.id = id headings.push({ level: +token.tag.substr(1, 1), anchor: heading.map[0], content: heading.content, id }) } } let indent = options.tocFirstLevel - 1 const list = headings.map(function (heading) { let res = [] if (heading.level < options.tocFirstLevel || heading.level > options.tocLastLevel) { return '' } if (heading.level > indent) { const ldiff = heading.level - indent for (let i = 0; i < ldiff; i++) { res.push(`<ul class="${options.tocClassName}">`) indent++ } } else if (heading.level < indent) { const ldiff = (indent - heading.level) for (let i = 0; i < ldiff; i++) { res.push('</ul>') indent-- } } res = res.concat([ `<li class="${options.anchorClassName}"><a href="#`, heading.id, '">', heading.content, '</a></li>' ]) return res.join('') }) const result = list.join('') + new Array(indent + 1).join('</ul>') typeof options.tocCallback === 'function' && options.tocCallback(result) return result } md.inline.ruler.after('emphasis', 'toc', toc) }