markdown-toc-redux
Version:
Generate a markdown TOC (table of contents) with Remarkable.
260 lines (221 loc) • 5.99 kB
JavaScript
/*!
* markdown-toc-redux <https://github.com/gregdan3/markdown-toc-redux>
*
* Copyright © 2013-2023, Jon Schlinkert.
* Copyright © 2023-2024, Gregory Danielson III.
* 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.utils = utils;
toc.bullets = bullets;
toc.linkify = linkify;
toc.slugify = utils.slugify;
toc.titleize = titleize;
toc.plugin = generate;
toc.strip = strip;
;