Automatically adds a table of contents to Kanka entity pages under the Pins sidebar.
当前为
// ==UserScript==
// @name Kanka Automatic Table of Contents
// @namespace http://tampermonkey.net/
// @version 11
// @description Automatically adds a table of contents to Kanka entity pages under the Pins sidebar.
// @author Salvatos
// @match https://app.kanka.io/*
// @exclude */html-export*
// @icon https://www.google.com/s2/favicons?domain=kanka.io
// @grant GM_addStyle
// ==/UserScript==
// Run only on entity Story pages
if (document.getElementById('app').parentNode.classList.contains("entity-story")) {
/* Preferences */
const addTopLink = "";
/* Set arrays */
var headings = [];
var tag_names = { h1:1, h2:1, h3:1, h4:1, h5:1, h6:1 };
/* Pre-cleaning: remove stray line breaks left by Summernote at the end of headings so our TOC link doesn't get pushed to a new line */
$('h1 br:last-child, h2 br:last-child, h3 br:last-child, h4 br:last-child, h5 br:last-child, h6 br:last-child').remove();
/* Walks through DOM looking for selected elements */
function walk( root ) {
if( root.nodeType === 1 && root.nodeName !== 'script' && !root.classList.contains("calendar") && !root.classList.contains("modal") ) { // Added a check to exclude modals and calendar dates
if( tag_names.hasOwnProperty(root.nodeName.toLowerCase()) ) {
headings.push( root );
} else {
for( var i = 0; i < root.childNodes.length; i++ ) {
walk( root.childNodes[i] );
}
}
}
}
// Find and walk through the main content block
walk( document.getElementsByClassName('entity-main-block')[0] );
/* Start main list */
var level = 0;
var past_level = 0;
var hList = `
<div id='toc' class='sidebar-section-box overflow-hidden flex flex-col gap-2'>
<div class="sidebar-section-title cursor-pointer text-lg user-select border-b element-toggle" data-animate="collapse" data-target="#sidebar-toc-list" onclick="this.classList.toggle('animate-collapsed'); document.getElementById('sidebar-toc-list').classList.toggle('hidden');">
<i class="fa-solid fa-chevron-up icon-show" aria-hidden="true"></i>
<i class="fa-solid fa-chevron-down icon-hide" aria-hidden="true"></i>
Table of contents
</div>
<div class="sidebar-elements overflow-hidden" id="sidebar-toc-list">
<div class="flex flex-col gap-2 text-xs">
<ul id='tableofcontents'>
<li class='toc-level-0'><a href='#toc-entry'>Entry</a></li>
`;
/* Create sublists to reflect heading level */
for( var i = 0; i < headings.length; i++ ) {
// "Entry" and post titles act as level-0 headers;
level = (headings[i].classList.contains("post-title")) ? 0 : headings[i].nodeName.substr(1);
if (level > past_level) { // Go down a level
for(var j = 0; j < level - past_level; j++) {
hList += "<li><ul>";
}
}
else if (level < past_level) { // Go up a level
for(var j = 0; j < past_level - level; j++) {
hList += "</ul></li>";
}
}
/* Handle heading text (it gets complicated with Timeline elements and inline tags) */
var headingText = headings[i],
child = headingText.firstChild,
texts = [];
// Iterate through heading nodes
while (child) {
// Not a tag (text node)
if (!child.tagName) {
texts.push(child.data);
console.log("1: " + child.data); // Why am I getting so many empty text nodes?
}
// Identify and manage HTML tags
else {
// Text-muted tag, i.e. a Timeline date ;; no longer relevant but keeping for reference
/*
if ($(child).hasClass("text-muted")) {
//texts.push('<span class="text-muted">' + child.innerText + '</span>');
texts.push(child.innerText);
console.log("2: " + child.innerText);
}
*/
// Screenreader prompt
if ($(child).hasClass("sr-only")) {
// exclude
}
else {
texts.push(child.innerText);
console.log("3: " + child.innerText);
}
}
child = child.nextSibling;
}
headingText = texts.join("");
/* Add an ID to the entry box */
document.querySelector(".box-entity-entry").id = "toc-entry";
/* Check if heading already has an ID, else create one */
if (headings[i].id.length < 1) {
headings[i].id = "h" + i + "-" + headingText.trim().replace(/\s+/g, "-").replace(/^[^\p{L}]+|[^\p{L}\p{N}:.-]+/gu, "");
// We indicate a unique ID to acccount for duplicate titles
}
/* Create link in TOC */
hList += "<li class='toc-level-" + level + "'><a href='#" + headings[i].id + "' parent-post='" + $(headings[i]).closest('article:is(.box-entity-entry, .post-block)').attr('id') + "'>" + headingText + "</a></li>";
/* Add "toc" link to non-box headings */
if (addTopLink && level > 0 && $(headings[i]).parent('a.entity-mention').length == 0) { // That last condition is to omit Extraordinary Tooltips and other transclusions
headings[i].insertAdjacentHTML("beforeend", "<a class='to-top' href='#toc' title='Back to table of contents'> ^ " + addTopLink + "</a>");
}
/* Update past_level */
past_level = level;
}
/* Close sublists per current level */
for(var k = 0; k < past_level; k++) {
hList += "</li></ul>";
}
/* Close TOC */
hList += "</div></div></div>";
/* Insert element after Pins (and entity links) */
/* Calendars use only one sidebar */
if (document.getElementById('app').parentNode.classList.contains("kanka-entity-calendar")) {
document.getElementsByClassName('entity-submenu')[0].insertAdjacentHTML("beforeend", hList);
}
/* Everything else */
else {
document.getElementsByClassName('entity-sidebar')[0].insertAdjacentHTML("beforeend", hList);
}
/* Listener: If the target heading is in a collapsed post, expand it first */
// For headings within posts, we need to find the parent to open, then scroll to the targeted heading once rendered
$("#tableofcontents :not(.toc-level-0) a").click(function() {
var targetPost = $(this).attr('parent-post');
if (targetPost != "toc-entry" && $("#" + targetPost + " .element-toggle")[0].classList.contains("animate-collapsed")) {
$("#" + targetPost + " .element-toggle")[0].click();
// Wait a bit for rendering and scroll to appropriate heading
let targetHeading = document.querySelector($(this).attr('href'));
setTimeout(function(){ targetHeading.scrollIntoView(); }, 300);
}
});
// For posts, just pop them open as we go
$("#tableofcontents .toc-level-0 a").click(function() {
var targetPost = $(this).attr('parent-post');
if (targetPost != "undefined" && $("#" + targetPost + " .element-toggle")[0].classList.contains("animate-collapsed")) {
$("#" + targetPost + " .element-toggle")[0].click();
}
});
GM_addStyle(`
.entity-links {
margin-bottom: 30px; /* For consistency with other boxes */
}
#tableofcontents, #tableofcontents ul {
list-style: none;
padding: 0;
text-indent: -5px;
}
#tableofcontents {
padding: 5px 10px;
margin: 0;
overflow: hidden;
word-wrap: anywhere;
}
#tableofcontents ul {
padding-left: 10px;
}
#tableofcontents a {
font-size: 13px;
}
#tableofcontents li.toc-level-0 a {
font-weight: bold;
}
.to-top {
vertical-align: super;
font-variant: all-petite-caps;
font-size: 10px;
}
`);
}