You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
parttimejob/node_modules/markdown-it-toc-and-anchor/src/index.js

314 lines
8.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import clone from "clone";
import uslug from "uslug";
import Token from "markdown-it/lib/token";
const TOC = "@[toc]";
const TOC_RE = /^@\[toc\]/im;
let markdownItSecondInstance = () => {};
let headingIds = {};
let tocHtml = "";
const repeat = (string, num) => new Array(num + 1).join(string);
const makeSafe = (string, headingIds, slugifyFn) => {
const key = slugifyFn(string); // slugify
if (!headingIds[key]) {
headingIds[key] = 0;
}
headingIds[key]++;
return key + (headingIds[key] > 1 ? `-${headingIds[key]}` : "");
};
const space = () => {
return { ...new Token("text", "", 0), content: " " };
};
const renderAnchorLinkSymbol = options => {
if (options.anchorLinkSymbolClassName) {
return [
{
...new Token("span_open", "span", 1),
attrs: [["class", options.anchorLinkSymbolClassName]]
},
{
...new Token("text", "", 0),
content: options.anchorLinkSymbol
},
new Token("span_close", "span", -1)
];
} else {
return [
{
...new Token("text", "", 0),
content: options.anchorLinkSymbol
}
];
}
};
const renderAnchorLink = (anchor, options, tokens, idx) => {
const attrs = [];
if (options.anchorClassName != null) {
attrs.push(["class", options.anchorClassName]);
}
attrs.push(["href", `#${anchor}`]);
const openLinkToken = {
...new Token("link_open", "a", 1),
attrs
};
const closeLinkToken = new Token("link_close", "a", -1);
if (options.wrapHeadingTextInAnchor) {
tokens[idx + 1].children.unshift(openLinkToken);
tokens[idx + 1].children.push(closeLinkToken);
} else {
const linkTokens = [
openLinkToken,
...renderAnchorLinkSymbol(options),
closeLinkToken
];
// `push` or `unshift` according to anchorLinkBefore option
// space is at the opposite side.
const actionOnArray = {
false: "push",
true: "unshift"
};
// insert space between anchor link and heading ?
if (options.anchorLinkSpace) {
linkTokens[actionOnArray[!options.anchorLinkBefore]](space());
}
tokens[idx + 1].children[actionOnArray[options.anchorLinkBefore]](
...linkTokens
);
}
};
const treeToMarkdownBulletList = (tree, indent = 0) =>
tree
.map(item => {
const indentation = " ";
let node = `${repeat(indentation, indent)}*`;
if (item.heading.content) {
const contentWithoutAnchor = item.heading.content.replace(
/\[([^\]]*)\]\([^)]*\)/g,
"$1"
);
node += " " + `[${contentWithoutAnchor}](#${item.heading.anchor})\n`;
} else {
node += "\n";
}
if (item.nodes.length) {
node += treeToMarkdownBulletList(item.nodes, indent + 1);
}
return node;
})
.join("");
const generateTocMarkdownFromArray = (headings, options) => {
const tree = { nodes: [] };
// create an ast
headings.forEach(heading => {
if (
heading.level < options.tocFirstLevel ||
heading.level > options.tocLastLevel
) {
return;
}
let i = 1;
let lastItem = tree;
for (; i < heading.level - options.tocFirstLevel + 1; i++) {
if (lastItem.nodes.length === 0) {
lastItem.nodes.push({
heading: {},
nodes: []
});
}
lastItem = lastItem.nodes[lastItem.nodes.length - 1];
}
lastItem.nodes.push({
heading: heading,
nodes: []
});
});
return treeToMarkdownBulletList(tree.nodes);
};
export default function(md, options) {
options = {
toc: true,
tocClassName: "markdownIt-TOC",
tocFirstLevel: 1,
tocLastLevel: 6,
tocCallback: null,
anchorLink: true,
anchorLinkSymbol: "#",
anchorLinkBefore: true,
anchorClassName: "markdownIt-Anchor",
resetIds: true,
anchorLinkSpace: true,
anchorLinkSymbolClassName: null,
wrapHeadingTextInAnchor: false,
...options
};
markdownItSecondInstance = clone(md);
// initialize key ids for each instance
headingIds = {};
md.core.ruler.push("init_toc", function(state) {
const tokens = state.tokens;
// reset key ids for each document
if (options.resetIds) {
headingIds = {};
}
const tocArray = [];
let tocMarkdown = "";
let tocTokens = [];
const slugifyFn =
(typeof options.slugify === "function" && options.slugify) || uslug;
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].type !== "heading_close") {
continue;
}
const heading = tokens[i - 1];
const heading_close = tokens[i];
if (heading.type === "inline") {
let content;
if (
heading.children &&
heading.children.length > 0 &&
heading.children[0].type === "link_open"
) {
// headings that contain links have to be processed
// differently since nested links aren't allowed in markdown
content = heading.children[1].content;
heading._tocAnchor = makeSafe(content, headingIds, slugifyFn);
} else {
content = heading.content;
heading._tocAnchor = makeSafe(
heading.children.reduce((acc, t) => acc + t.content, ""),
headingIds,
slugifyFn
);
}
if (options.anchorLinkPrefix) {
heading._tocAnchor = options.anchorLinkPrefix + heading._tocAnchor;
}
tocArray.push({
content,
anchor: heading._tocAnchor,
level: +heading_close.tag.substr(1, 1)
});
}
}
tocMarkdown = generateTocMarkdownFromArray(tocArray, options);
tocTokens = markdownItSecondInstance.parse(tocMarkdown, {});
// Adding tocClassName to 'ul' element
if (
typeof tocTokens[0] === "object" &&
tocTokens[0].type === "bullet_list_open"
) {
const attrs = (tocTokens[0].attrs = tocTokens[0].attrs || []);
if (options.tocClassName != null) {
attrs.push(["class", options.tocClassName]);
}
}
tocHtml = markdownItSecondInstance.renderer.render(
tocTokens,
markdownItSecondInstance.options
);
if (typeof state.env.tocCallback === "function") {
state.env.tocCallback.call(undefined, tocMarkdown, tocArray, tocHtml);
} else if (typeof options.tocCallback === "function") {
options.tocCallback.call(undefined, tocMarkdown, tocArray, tocHtml);
} else if (typeof md.options.tocCallback === "function") {
md.options.tocCallback.call(undefined, tocMarkdown, tocArray, tocHtml);
}
});
md.inline.ruler.after("emphasis", "toc", (state, silent) => {
let token;
let match;
if (
// Reject if the token does not start with @[
state.src.charCodeAt(state.pos) !== 0x40 ||
state.src.charCodeAt(state.pos + 1) !== 0x5b ||
// Dont run any pairs in validation mode
silent
) {
return false;
}
// Detect TOC markdown
match = TOC_RE.exec(state.src);
match = !match ? [] : match.filter(m => m);
if (match.length < 1) {
return false;
}
// Build content
token = state.push("toc_open", "toc", 1);
token.markup = TOC;
token = state.push("toc_body", "", 0);
token = state.push("toc_close", "toc", -1);
// Update pos so the parser can continue
state.pos = state.pos + 6;
return true;
});
const originalHeadingOpen =
md.renderer.rules.heading_open ||
function(...args) {
const [tokens, idx, options, , self] = args;
return self.renderToken(tokens, idx, options);
};
md.renderer.rules.heading_open = function(...args) {
const [tokens, idx, , ,] = args;
const attrs = (tokens[idx].attrs = tokens[idx].attrs || []);
const anchor = tokens[idx + 1]._tocAnchor;
attrs.push(["id", anchor]);
if (options.anchorLink) {
renderAnchorLink(anchor, options, ...args);
}
return originalHeadingOpen.apply(this, args);
};
md.renderer.rules.toc_open = () => "";
md.renderer.rules.toc_close = () => "";
md.renderer.rules.toc_body = () => "";
if (options.toc) {
md.renderer.rules.toc_body = () => tocHtml;
}
}