Last active
September 28, 2017 08:34
-
-
Save aduh95/795a402f0d11ac43a640d89c7f4d707a to your computer and use it in GitHub Desktop.
Generate a table of contents, based on headings in the page, for Markdown and HTML document using JS and CSS counters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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