Kanka Automatic Table of Contents

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

当前为 2023-04-25 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Kanka Automatic Table of Contents
// @namespace    http://tampermonkey.net/
// @version      10
// @description  Automatically adds a table of contents to Kanka entity pages under the Pins sidebar.
// @author       Salvatos
// @match        https://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, based on entity type
    if (document.getElementById('app').parentNode.classList.contains("kanka-entity-calendar")) {
        walk( document.getElementsByClassName('entity-main-block')[0] );
    }
    else {
        walk( document.getElementsByClassName('entity-story-block')[0] );
    }

	/* Start main list */
	var level = 0;
	var past_level = 0;
	var hList = `
		<div id='toc' class='sidebar-section-box'>
        	<div class="sidebar-section-title cursor-pointer" data-toggle="collapse" data-target="#sidebar-toc-list">
        		<i class="fa fa-chevron-right" style="display: none" aria-hidden="true"></i>
        		<i class="fa fa-chevron-down" aria-hidden="true"></i>
				Table of contents
            </div>
        	<div class="sidebar-elements collapse in !visible" id="sidebar-toc-list">
				<ul id='tableofcontents'>
	`;

	/* Create sublists to reflect heading level */
	for( var i = 0; i < headings.length; i++ ) {
	    // "Entry", Mentions and post titles act as level-0 headers
	    level = (headings[i].classList.contains("box-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 ;; TODO: that doesn’t seem to be working, I’m getting ridiculous IDs; wait why am I ADDING a span and then hiding it in CSS?
                /*
                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("");

		/* 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]).parent().parent().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
            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>";

	/* 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)[0] || !$("#" + targetPost)[0].classList.contains("in")) {
            $("h3[data-target='#" + targetPost + "']").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 + " > .entity-content")[0].classList.contains("in")) {
            $("#" + targetPost + " > .box-header > h3").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: 0 10px;
        margin: 0;
	    overflow: hidden;
	    word-wrap: anywhere;
	}
	#tableofcontents ul {
	    padding-left: 10px;
	}
	#tableofcontents a {
	    font-size: 14px;
	}
	#tableofcontents li.toc-level-0 a {
	    font-weight: bold;
	}
	.to-top {
	    vertical-align: super;
	    font-variant: all-petite-caps;
	    font-size: 10px;
	}
	`);
}