markdown-it-ftoc
Version:
markdown-it plugin to add toc and anchor links in headings
182 lines (164 loc) • 5.11 kB
JavaScript
/**
* 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)
}