Skip to content

Instantly share code, notes, and snippets.

@aduh95 aduh95/generate_toc.js
Last active Sep 28, 2017

Embed
What would you like to do?
Generate a table of contents, based on headings in the page, for Markdown and HTML document using JS and CSS counters
/**
* Based on the [Stuart Langridge script](https://kryogenix.org/code/browser/generated-toc/generated_toc.js)
* Generated TOC
* aduh95, 2017-09-28
*
* Generate a table of contents, based on headings in the page.
*
* To place the TOC on your Markdown or HTML document, insert this
* script and add the following `nav` element to your document where
* you want the TOC to be generated.
* N.B.: All the headings before the `nav` element will be ignored.
*
* ```html
* <!-- In the <head> in HTML or at the begining in Markdown -->
* <script src="https://cdn.rawgit.com/aduh95/795a402f0d11ac43a640d89c7f4d707a/raw/bebb36090ee3f17d62435ec353c330395662d517/generate_toc.js"></script>
*
* <!-- ... -->
*
* <!-- The TOC will appear here -->
* <nav id="generated-toc"></nav>
* ```
*
*/
(function() {
const SUMMARY_TEXT = "Table of content";
const ID_TOC_ELEMENT = "generated-toc";
generated_toc = {
getHeadings: (generate_from, tocElement) => {
// add all levels of heading we're paying attention to to the
// headings_to_treat dictionary, ready to be filled in later
let headings_to_treat = { h6: "" };
for (let i = 5; i >= parseInt(generate_from); i--) {
headings_to_treat["h" + i] = "";
}
// get headings. We can't say
// getElementsByTagName("h1" or "h2" or "h3"), etc, so get all
// elements and filter them ourselves
// need to use .all here because IE doesn't support gEBTN('*')
let headings = document.querySelectorAll(
Object.keys(headings_to_treat).join(",")
);
// make the basic eements of the TOC itself, ready to fill into
let wantedHeadings = [];
for (let heading of headings) {
generated_toc.addToArrayIfNotPreviousSibbling(
wantedHeadings,
tocElement,
heading
);
}
return wantedHeadings;
},
generate: function(headings, generate_from) {
let cur_head_lvl = generate_from;
let details = document.createElement("details");
let summary = document.createElement("summary");
summary.appendChild(document.createTextNode(SUMMARY_TEXT));
let cur_list_el = document.createElement("ol");
details.appendChild(summary);
details.appendChild(cur_list_el);
// now walk through our saved heading nodes
for (let this_head_el of headings) {
this_head_lvl = parseInt(this_head_el.nodeName.slice(1)) || 6;
if (!this_head_el.id) {
// if heading doesn't have an ID, give it one
this_head_el.id = generated_toc.randID();
this_head_el.setAttribute("tabindex", "-1");
}
while (this_head_lvl > cur_head_lvl) {
// this heading is at a lower level than the last one;
// create additional nested lists to put it at the right level
// get the *last* LI in the current list, and add our new UL to it
let last_listitem_el =
cur_list_el.lastChild || document.createElement("li");
let new_list_el = document.createElement("ol");
last_listitem_el.appendChild(new_list_el);
cur_list_el.appendChild(last_listitem_el);
cur_list_el = new_list_el;
cur_head_lvl++;
}
while (this_head_lvl < cur_head_lvl) {
// this heading is at a higher level than the last one;
// go back up the TOC to put it at the right level
cur_list_el = cur_list_el.parentNode.parentNode;
cur_head_lvl--;
}
// create a link to this heading, and add it to the TOC
cur_list_el.appendChild(
(function(li) {
let a = document.createElement("a");
a.href = "#" + this_head_el.id;
a.appendChild(
document.createTextNode(generated_toc.innerText(this_head_el))
);
li.appendChild(a);
return li;
})(document.createElement("li"))
);
}
// go through the TOC and find all LIs that are "empty", i.e., contain
// only ULs and no links, and give them class="missing"
let alllis = details.getElementsByTagName("li");
for (let i = 0; i < alllis.length; i++) {
let foundlink = false;
for (let j = 0; j < alllis[i].childNodes.length; j++) {
if (alllis[i].childNodes[j].nodeName.toLowerCase() == "a") {
foundlink = true;
}
}
if (!foundlink) {
alllis[i].className = "missing";
} else {
alllis[i].className = "notmissing";
}
}
return details;
},
addToArrayIfNotPreviousSibbling: (
notPreviousSibblings,
refElement,
element
) => {
if (element.parentNode === refElement.parentNode) {
for (let child of element.parentNode.childNodes) {
if (child === element) {
return;
}
if (child === refElement) {
notPreviousSibblings.push(element);
return;
}
}
} else {
notPreviousSibblings.push(element);
}
},
randID: function() {
let _return;
do {
_return = Math.random()
.toString(36)
.substr(2, 10);
} while (document.getElementById(_return));
return _return;
},
innerText: function(el) {
return typeof el.innerText != "undefined"
? el.innerText
: typeof el.textContent != "undefined"
? el.textContent
: el.innerHTML.replace(/<[^>]+>/g, "");
},
getFirstHeaderLevel: function(tocElement) {
let generate_from = 0;
tocElement.classList.forEach(function(className) {
if (className.match(/^generate_from_h[1-6]$/)) {
generate_from = parseInt(className.substr(className.length - 1, 1));
// } else if (className.match(/^generate_for_[a-z]+$/)) {
// generate_for = className.match(/^generate_for_([a-z])+$/)[1];
}
});
// set top_node to be the element in the document under which
// we'll be analysing headings
// If there isn't a specified header level to generate from, work
// out what the first header level inside top_node is
// and make that the specified header level
return parseInt(
generate_from ||
generated_toc.findFirstHeaderElement(tocElement).slice(1)
);
},
findFirstHeaderElement: function(node) {
// a recursive function which returns the first header it finds inside
// node, or null if there are no functions inside node.
let nn = node.nodeName.toLowerCase();
if (nn.match(/^h[1-6]$/)) {
// this node is itself a header; return our name
return nn;
} else {
let subvalue;
if (node.nextElementSibling) {
subvalue = generated_toc.findFirstHeaderElement(
node.nextElementSibling
);
}
if (subvalue) return subvalue;
if (node.hasChildNodes()) {
subvalue = generated_toc.findFirstHeaderElement(
node.firstElementChild
);
}
if (subvalue) return subvalue;
// no headers in this node at all
return null;
}
},
init: function() {
// quit if this function has already been called
if (arguments.callee.done) return;
// flag this function so we don't do the same thing twice
arguments.callee.done = true;
// Identify our TOC element, and what it applies to
let tocElement = document.getElementById(ID_TOC_ELEMENT);
if (!tocElement) {
return;
}
let style = document.createElement("style");
style.appendChild(document.createTextNode(""));
document.head.appendChild(style);
style.sheet.insertRule(
"#" +
ID_TOC_ELEMENT +
" ol{counter-reset: section;list-style-type: none;}"
);
style.sheet.insertRule(
"#" +
ID_TOC_ELEMENT +
" li::before{counter-increment: section;content: counters(section,'.') ' ';}"
);
let generate_from = generated_toc.getFirstHeaderLevel(
tocElement,
document.body
);
if (generate_from) {
style.sheet.insertRule(
":root{counter-reset: heading" + generate_from + ";}"
);
for (let i = generate_from; i <= 6; i++) {
let content = "";
for (let j = generate_from; i >= j; j++) {
content += "counter(heading" + j + ") '.'";
}
style.sheet.insertRule(
"#" +
ID_TOC_ELEMENT +
"~h" +
i +
"{counter-reset:heading" +
(i + 1) +
";}"
);
style.sheet.insertRule(
"#" +
ID_TOC_ELEMENT +
"~h" +
i +
"::before{counter-increment:heading" +
i +
";content:" +
content +
" ' ';}"
);
}
// for (let rule of style.sheet.cssRules) console.log(rule.cssText);
tocElement.appendChild(
generated_toc.generate(
generated_toc.getHeadings(generate_from, tocElement),
generate_from
)
);
}
},
};
document.addEventListener("DOMContentLoaded", generated_toc.init);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.