Kanka Automatic Table of Contents

Automatically adds a table of contents to Kanka entity pages under the Pins sidebar.

当前为 2024-09-29 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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'>&nbsp;^&nbsp;" + 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;
	}
	`);
}