Markdown Table of Contents Generator
Markdown Table of Contents Generator
by remarkablemark
HTML
<h1>Markdown Table of Contents Generator</h1>
<p class="container">
<textarea
autofocus
cols="50"
id="input"
placeholder="Enter Markdown..."
rows="15"
></textarea>
<textarea
cols="50"
id="output"
placeholder="Table of Contents"
readonly
rows="15"
></textarea>
</p>
<button id="copy">Copy Table of Contents</button>
CSS
body {
font-family: "Lucida Grande", Helvetica, Arial, sans-serif;
height: 375px;
}
.container {
display: flex;
}
textarea {
flex: 1;
}
TypeScript
const HEADINGS_REGEX = /^(#{1,6}[ \t].+)$|^(.+[\r\n][=-]{3,})$/gm;
const HEADING_REGEX = /^(#+)[ \t](.+)$|^(.+)[\r\n]([=-])/;
const WHITESPACE_REGEX = /\s/;
const INVALID_FRAGMENT_REGEX = /[^a-zA-Z0-9_-]/g;
const SPACE = ' ';
const HYPHEN = '-';
const MARKDOWN_BULLET = HYPHEN + SPACE;
const MARKDOWN_INDENT_LENGTH = 2;
const MARKDOWN_INDENT = Array(MARKDOWN_INDENT_LENGTH + 1).join(SPACE); // SPACE + SPACE
const NEWLINE = '\n';
type FragmentCountMap = Record<string, number>;
/**
* Transforms heading to URL fragment (without #).
*/
function getFragment(heading: string): string {
return heading
.toLowerCase()
.split(WHITESPACE_REGEX)
.join('-')
.replace(INVALID_FRAGMENT_REGEX, '');
}
/**
* Transforms heading and fragment to Markdown.
*/
function getLink(heading: string, fragment: string): string {
return '[' + heading + '](#' + fragment + ')';
}
/**
* Generates table of contents given Markdown.
*/
function mdtocs(markdown: string): string {
if (typeof markdown !== 'string') {
throw new TypeError('First argument must be a string');
}
const headings = markdown.match(HEADINGS_REGEX);
if (headings === null) {
return '';
}
let result = '';
let previousLevel = null;
let indent = '';
const fragmentCountMap: FragmentCountMap = {};
for (let i = 0, len = headings.length; i < len; i++) {
const heading = headings[i].match(HEADING_REGEX);
if (heading === null) {
continue;
}
let currentLevel;
let headingText;
// get heading level and text
if (heading[4]) {
headingText = heading[3].trim();
if (!headingText) {
continue;
}
currentLevel = heading[4] === HYPHEN ? 2 : 1;
} else {
headingText = heading[2].trim();
if (!headingText) {
continue;
}
currentLevel = heading[1].length;
}
// build indent
if (!previousLevel) {
// continue
} else if (currentLevel === 1) {
indent = '';
} else if...