Kanka Automatic Table of Contents

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

目前為 2021-11-22 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Kanka Automatic Table of Contents
// @namespace    http://tampermonkey.net/
// @version      7.1
// @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")) {
    GM_addStyle(`
	.entity-links {
		margin-bottom: 20px;
	}
	div#toc h3 {
	    text-align: center;
	    margin: 0 0 10px;
        padding-bottom: 10px;
	    font-family: Source Sans Pro,Helvetica Neue,Helvetica,Arial,sans-serif;
	    font-weight: bold;
	    font-size: 17px;
	    color: var(--header-text);
        border-bottom: 1px solid var(--box-with-border-bottom-border);
	}
	#tableofcontents, #tableofcontents ul {
	    list-style: none;
	    padding: 0;
	    text-indent: -5px;
	}
	#tableofcontents {
	    padding: 0 10px;
	    overflow: hidden;
	    word-wrap: anywhere;
	}
	#tableofcontents ul {
	    padding-left: 10px;
	}
	#tableofcontents a {
	    font-size: 14px;
	}
	#tableofcontents li.toc-level-0 a {
	    font-weight: bold;
	}
    #tableofcontents .text-muted {
        display: none;
    }
	.to-top {
	    vertical-align: super;
	    font-variant: all-petite-caps;
	    font-size: 10px;
	}
	`);

	/* Set arrays and prefs */
	var headings = [];
	var tag_names = { h1:1, h2:1, h3:1, h4:1, h5:1, h6:1 };
    const addTopLink = true;

    /* 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='box box-solid' style='padding:10px;'>
		<h3>Table of contents</h3>
		<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);
            }
            // Identify and manage HTML tags
            else {
                // Text-muted tag, i.e. a Timeline date
                if ($(child).hasClass("text-muted")) {
                    texts.push('<span class="text-muted">' + child.innerText + '</span>');
                }
                // Screenreader prompt
                else if ($(child).hasClass("sr-only")) {
                    // exclude
                }
                else {
                    texts.push(child.innerText);
                }
            }
            child = child.nextSibling;
        }

        headingText = texts.join("");

	    /* Create ID anchor on heading */
	    headings[i].id = "h" + level + "-" + headingText.replace(/\s/g, "");

        /* 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].insertAdjacentHTML("beforeend", "<a class='to-top' href='#toc' title='Back to table of contents'>&nbsp;^&nbsp;toc</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>";

	/* 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 */
    $("#tableofcontents :not(.toc-level-0) a").click(function() {
        var targetPost = $(this).attr('parent-post');
        if (!$("#" + targetPost)[0].classList.contains("in")) {
            $("h3[data-target='#" + targetPost + "']").click();
        }
    });
    $("#tableofcontents .toc-level-0 a").click(function() {
        var targetPost = $(this).attr('parent-post');
        if (targetPost != "undefined" && !$("#" + targetPost + " > .box-header > h3")[0].classList.contains("in")) {
            $("#" + targetPost + " > .box-header > h3").click();
        }
    });
}