markdown-toc
Version:
Generate a markdown TOC (table of contents) with Remarkable.
255 lines (216 loc) • 5.87 kB
JavaScript
/*!
* markdown-toc <https://github.com/jonschlinkert/markdown-toc>
*
* Copyright © 2013-2017, Jon Schlinkert.
* Released under the MIT License.
*/
var utils = require('./lib/utils');
var querystring = require('querystring');
/**
* expose `toc`
*/
module.exports = toc;
/**
* Load `generate` as a remarkable plugin and
* expose the `toc` function.
*
* @param {String} `str` String of markdown
* @param {Object} `options`
* @return {String} Markdown-formatted table of contents
*/
function toc(str, options) {
return new utils.Remarkable()
.use(generate(options))
.render(str);
}
/**
* Expose `insert` method
*/
toc.insert = require('./lib/insert');
/**
* Generate a markdown table of contents. This is the
* function that does all of the main work with Remarkable.
*
* @param {Object} `options`
* @return {String}
*/
function generate(options) {
var opts = utils.merge({firsth1: true, maxdepth: 6}, options);
var stripFirst = opts.firsth1 === false;
if (typeof opts.linkify === 'undefined') opts.linkify = true;
return function(md) {
md.renderer.render = function(tokens) {
tokens = tokens.slice();
var seen = {};
var len = tokens.length, i = 0, num = 0;
var tocstart = -1;
var arr = [];
var res = {};
while (len--) {
var token = tokens[i++];
if (/<!--[ \t]*toc[ \t]*-->/.test(token.content)) {
tocstart = token.lines[1];
}
if (token.type === 'heading_open') {
tokens[i].lvl = tokens[i - 1].hLevel;
tokens[i].i = num++;
arr.push(tokens[i]);
}
}
var result = [];
res.json = [];
// exclude headings that come before the actual
// table of contents.
var alen = arr.length, j = 0;
while (alen--) {
var tok = arr[j++];
if (tok.lines && (tok.lines[0] > tocstart)) {
var val = tok.content;
if (tok.children && tok.children[0].type === 'link_open') {
if (tok.children[1].type === 'text') {
val = tok.children[1].content;
}
}
if (!seen.hasOwnProperty(val)) {
seen[val] = 0;
} else {
seen[val]++;
}
tok.seen = opts.num = seen[val];
tok.slug = utils.slugify(val, opts);
res.json.push(utils.pick(tok, ['content', 'slug', 'lvl', 'i', 'seen']));
if (opts.linkify) tok = linkify(tok, opts);
result.push(tok);
}
}
opts.highest = highest(result);
res.highest = opts.highest;
res.tokens = tokens;
if (stripFirst) result = result.slice(1);
res.content = bullets(result, opts);
res.content += (opts.append || '');
return res;
};
};
}
/**
* Render markdown list bullets
*
* @param {Array} `arr` Array of listitem objects
* @param {Object} `opts`
* @return {String}
*/
function bullets(arr, options) {
var opts = utils.merge({indent: ' '}, options);
opts.chars = opts.chars || opts.bullets || ['-', '*', '+'];
var unindent = 0;
var listitem = utils.li(opts);
var fn = typeof opts.filter === 'function'
? opts.filter
: null;
// Keep the first h1? This is `true` by default
if (opts && opts.firsth1 === false) {
unindent = 1;
}
var len = arr.length;
var res = [];
var i = 0;
while (i < len) {
var ele = arr[i++];
ele.lvl -= unindent;
if (fn && !fn(ele.content, ele, arr)) {
continue;
}
if (ele.lvl > opts.maxdepth) {
continue;
}
var lvl = ele.lvl - opts.highest;
res.push(listitem(lvl, ele.content, opts));
}
return res.join('\n');
}
/**
* Get the highest heading level in the array, so
* we can un-indent the proper number of levels.
*
* @param {Array} `arr` Array of tokens
* @return {Number} Highest level
*/
function highest(arr) {
var res = arr.slice().sort(function(a, b) {
return a.lvl - b.lvl;
});
if (res && res.length) {
return res[0].lvl;
}
return 0;
}
/**
* Turn headings into anchors
*/
function linkify(tok, options) {
var opts = utils.merge({}, options);
if (tok && tok.content) {
opts.num = tok.seen;
var text = titleize(tok.content, opts);
var slug = utils.slugify(tok.content, opts);
slug = querystring.escape(slug);
if (opts && typeof opts.linkify === 'function') {
return opts.linkify(tok, text, slug, opts);
}
tok.content = utils.mdlink(text, '#' + slug);
}
return tok;
}
/**
* Titleize the title part of a markdown link.
*
* @name options.titleize
* @param {String} `str` The string to titleize
* @param {Object} `opts` Pass a custom titleize function on `titleize`
* @return {String}
* @api public
*/
function titleize(str, opts) {
if (opts && opts.strip) { return strip(str, opts); }
if (opts && opts.titleize === false) return str;
if (opts && typeof opts.titleize === 'function') {
return opts.titleize(str, opts);
}
str = utils.getTitle(str);
str = str.split(/<\/?[^>]+>/).join('');
str = str.split(/[ \t]+/).join(' ');
return str.trim();
}
/**
* Optionally strip specified words from heading text (not url)
*
* @name options.strip
* @param {String} `str`
* @param {String} `opts`
* @return {String}
*/
function strip(str, opts) {
opts = opts || {};
if (!opts.strip) return str;
if (typeof opts.strip === 'function') {
return opts.strip(str, opts);
}
if (Array.isArray(opts.strip) && opts.strip.length) {
var res = opts.strip.join('|');
var re = new RegExp(res, 'g');
str = str.trim().replace(re, '');
return str.replace(/^-|-$/g, '');
}
return str;
}
/**
* Expose utils
*/
toc.bullets = bullets;
toc.linkify = linkify;
toc.slugify = utils.slugify;
toc.titleize = titleize;
toc.plugin = generate;
toc.strip = strip;
;