Kanka Automatic Table of Contents

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

当前为 2021-12-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Kanka Automatic Table of Contents
  3. // @namespace http://tampermonkey.net/
  4. // @version 7.2
  5. // @description Automatically adds a table of contents to Kanka entity pages under the Pins sidebar.
  6. // @author Salvatos
  7. // @match https://kanka.io/*
  8. // @exclude */html-export
  9. // @icon https://www.google.com/s2/favicons?domain=kanka.io
  10. // @grant GM_addStyle
  11. // ==/UserScript==
  12.  
  13. // Run only on entity Story pages
  14. if (document.getElementById('app').parentNode.classList.contains("entity-story")) {
  15. GM_addStyle(`
  16. .entity-links {
  17. margin-bottom: 30px; /* For consistency with other boxes */
  18. }
  19. #tableofcontents, #tableofcontents ul {
  20. list-style: none;
  21. padding: 0;
  22. text-indent: -5px;
  23. }
  24. #tableofcontents {
  25. padding: 0 10px;
  26. margin: 0;
  27. overflow: hidden;
  28. word-wrap: anywhere;
  29. }
  30. #tableofcontents ul {
  31. padding-left: 10px;
  32. }
  33. #tableofcontents a {
  34. font-size: 14px;
  35. }
  36. #tableofcontents li.toc-level-0 a {
  37. font-weight: bold;
  38. }
  39. #tableofcontents .text-muted {
  40. display: none;
  41. }
  42. .to-top {
  43. vertical-align: super;
  44. font-variant: all-petite-caps;
  45. font-size: 10px;
  46. }
  47. `);
  48.  
  49. /* Set arrays and prefs */
  50. var headings = [];
  51. var tag_names = { h1:1, h2:1, h3:1, h4:1, h5:1, h6:1 };
  52. const addTopLink = true;
  53.  
  54. /* 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 */
  55. $('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();
  56.  
  57. /* Walks through DOM looking for selected elements */
  58. function walk( root ) {
  59. if( root.nodeType === 1 && root.nodeName !== 'script' && !root.classList.contains("calendar") && !root.classList.contains("modal") ) { // Added a check to exclude modals and calendar dates
  60. if( tag_names.hasOwnProperty(root.nodeName.toLowerCase()) ) {
  61. headings.push( root );
  62. } else {
  63. for( var i = 0; i < root.childNodes.length; i++ ) {
  64. walk( root.childNodes[i] );
  65. }
  66. }
  67. }
  68. }
  69. // Find and walk through the main content block, based on entity type
  70. if (document.getElementById('app').parentNode.classList.contains("kanka-entity-calendar")) {
  71. walk( document.getElementsByClassName('entity-main-block')[0] );
  72. }
  73. else {
  74. walk( document.getElementsByClassName('entity-story-block')[0] );
  75. }
  76.  
  77. /* Start main list */
  78. var level = 0;
  79. var past_level = 0;
  80. var hList = `
  81. <div id='toc' class='sidebar-section-box'>
  82. <div class="sidebar-section-title cursor" data-toggle="collapse" data-target="#sidebar-toc-list">
  83. <i class="fa fa-chevron-right" style="display: none" aria-hidden="true"></i>
  84. <i class="fa fa-chevron-down" aria-hidden="true"></i>
  85. Table of contents
  86. </div>
  87. <div class="sidebar-elements collapse in" id="sidebar-toc-list">
  88. <ul id='tableofcontents'>
  89. `;
  90.  
  91. /* Create sublists to reflect heading level */
  92. for( var i = 0; i < headings.length; i++ ) {
  93. // "Entry", Mentions and post titles act as level-0 headers
  94. level = (headings[i].classList.contains("box-title")) ? 0 : headings[i].nodeName.substr(1);
  95.  
  96. if (level > past_level) { // Go down a level
  97. for(var j = 0; j < level - past_level; j++) {
  98. hList += "<li><ul>";
  99. }
  100. }
  101. else if (level < past_level) { // Go up a level
  102. for(var j = 0; j < past_level - level; j++) {
  103. hList += "</ul></li>";
  104. }
  105. }
  106.  
  107. /* Handle heading text (it gets complicated with Timeline elements and inline tags) */
  108. var headingText = headings[i],
  109. child = headingText.firstChild,
  110. texts = [];
  111. // Iterate through heading nodes
  112. while (child) {
  113. // Not a tag (text node)
  114. if (!child.tagName) {
  115. texts.push(child.data);
  116. }
  117. // Identify and manage HTML tags
  118. else {
  119. // Text-muted tag, i.e. a Timeline date
  120. if ($(child).hasClass("text-muted")) {
  121. texts.push('<span class="text-muted">' + child.innerText + '</span>');
  122. }
  123. // Screenreader prompt
  124. else if ($(child).hasClass("sr-only")) {
  125. // exclude
  126. }
  127. else {
  128. texts.push(child.innerText);
  129. }
  130. }
  131. child = child.nextSibling;
  132. }
  133.  
  134. headingText = texts.join("");
  135.  
  136. /* Create ID anchor on heading */
  137. headings[i].id = "h" + level + "-" + headingText.replace(/\s/g, "");
  138.  
  139. /* Create link in TOC */
  140. hList += "<li class='toc-level-" + level + "'><a href='#" + headings[i].id + "' parent-post='" + $(headings[i]).parent().parent().attr('id') + "'>" + headingText + "</a></li>";
  141.  
  142. /* Add "toc" link to non-box headings */
  143. if (addTopLink && level > 0) {
  144. headings[i].insertAdjacentHTML("beforeend", "<a class='to-top' href='#toc' title='Back to table of contents'>&nbsp;^&nbsp;toc</a>");
  145. }
  146.  
  147. /* Update past_level */
  148. past_level = level;
  149. }
  150.  
  151. /* Close sublists per current level */
  152. for(var k = 0; k < past_level; k++) {
  153. hList += "</li></ul>";
  154. }
  155. /* Close TOC */
  156. hList += "</div></div>";
  157.  
  158. /* Insert element after Pins (and entity links) */
  159. /* Calendars use only one sidebar */
  160. if (document.getElementById('app').parentNode.classList.contains("kanka-entity-calendar")) {
  161. document.getElementsByClassName('entity-submenu')[0].insertAdjacentHTML("beforeend", hList);
  162. }
  163. /* Everything else */
  164. else {
  165. document.getElementsByClassName('entity-sidebar')[0].insertAdjacentHTML("beforeend", hList);
  166. }
  167.  
  168. /* Listener: If the target heading is in a collapsed post, expand it first */
  169. $("#tableofcontents :not(.toc-level-0) a").click(function() {
  170. var targetPost = $(this).attr('parent-post');
  171. if (!$("#" + targetPost)[0].classList.contains("in")) {
  172. $("h3[data-target='#" + targetPost + "']").click();
  173. }
  174. });
  175. $("#tableofcontents .toc-level-0 a").click(function() {
  176. var targetPost = $(this).attr('parent-post');
  177. if (targetPost != "undefined" && !$("#" + targetPost + " > .box-header > h3")[0].classList.contains("in")) {
  178. $("#" + targetPost + " > .box-header > h3").click();
  179. }
  180. });
  181. }