Kanka Automatic Table of Contents

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

目前为 2021-11-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Kanka Automatic Table of Contents
  3. // @namespace http://tampermonkey.net/
  4. // @version 5
  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: 20px;
  18. }
  19. div#toc h3 {
  20. text-align: center;
  21. margin: 0 0 10px;
  22. padding-bottom: 10px;
  23. font-family: Source Sans Pro,Helvetica Neue,Helvetica,Arial,sans-serif;
  24. font-weight: bold;
  25. font-size: 17px;
  26. color: var(--header-text);
  27. border-bottom: 1px solid var(--box-with-border-bottom-border);
  28. }
  29. #tableofcontent, #tableofcontent ul {
  30. list-style: none;
  31. padding: 0;
  32. text-indent: -5px;
  33. }
  34. #tableofcontent {
  35. padding: 0 10px;
  36. overflow: hidden;
  37. word-wrap: anywhere;
  38. }
  39. #tableofcontent ul {
  40. padding-left: 10px;
  41. }
  42. #tableofcontent a {
  43. font-size: 14px;
  44. }
  45. #tableofcontent li.toc-level-0 a {
  46. font-weight: bold;
  47. }
  48. #tableofcontent .text-muted {
  49. display: none;
  50. }
  51. .to-top {
  52. vertical-align: super;
  53. font-variant: all-petite-caps;
  54. font-size: 10px;
  55. }
  56. `);
  57.  
  58. /* Set arrays and prefs */
  59. var headings = [];
  60. var tag_names = { h1:1, h2:1, h3:1, h4:1, h5:1, h6:1 };
  61. const addTopLink = false;
  62.  
  63. /* Pre-cleaning: remove stray line breaks left by Summernote at the end of headings so our TOP link doesn't get pushed to a new line */
  64. $('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();
  65.  
  66. /* Walks through DOM looking for selected elements */
  67. function walk( root ) {
  68. if( root.nodeType === 1 && root.nodeName !== 'script' && !root.classList.contains("calendar") && !root.classList.contains("modal") ) { // Added a check to exclude modals and calendar dates
  69. if( tag_names.hasOwnProperty(root.nodeName.toLowerCase()) ) {
  70. headings.push( root );
  71. } else {
  72. for( var i = 0; i < root.childNodes.length; i++ ) {
  73. walk( root.childNodes[i] );
  74. }
  75. }
  76. }
  77. }
  78. walk( document.getElementsByClassName('entity-story-block')[0] );
  79.  
  80. /* Start main list */
  81. var level =0;
  82. var past_level = 0;
  83. var hList= `
  84. <div id='toc' class='box box-solid' style='padding:10px;'>
  85. <h3>Table of contents</h3>
  86. <ul id='tableofcontent'>
  87. `;
  88.  
  89. /* Create sublists to reflect heading level */
  90. for( var i = 0; i < headings.length; i++ ) {
  91. // "Entry", Mentions and post titles act as level-0 headers
  92. level = (headings[i].classList.contains("box-title")) ? 0 : headings[i].nodeName.substr(1);
  93.  
  94. if (level > past_level) { // Go down a level
  95. for(var j = 0; j < level - past_level; j++) {
  96. hList += "<li><ul>";
  97. }
  98. }
  99. else if (level < past_level) { // Go up a level
  100. for(var j = 0; j < past_level - level; j++) {
  101. hList += "</ul></li>";
  102. }
  103. }
  104.  
  105. /* Handle heading text (it gets complicated with Timeline elements and inline tags) */
  106. var headingText = headings[i],
  107. child = headingText.firstChild,
  108. texts = [];
  109. // Iterate through heading nodes
  110. while (child) {
  111. // Not a tag (text node)
  112. if (!child.tagName) {
  113. texts.push(child.data);
  114. }
  115. // Tag but not a Timeline date
  116. else if (!$(child).hasClass("text-muted")) {
  117. texts.push(child.innerText);
  118. }
  119. // Text-muted tag, i.e. a Timeline date
  120. else {
  121. texts.push('<span class="text-muted">' + child.innerText + '</span>');
  122. }
  123. child = child.nextSibling;
  124. }
  125.  
  126. headingText = texts.join("");
  127.  
  128. /* Create ID anchor on heading */
  129. headings[i].id = "h" + level + "-" + headingText.replace(/\s/g, "");
  130.  
  131. /* Create link in TOC */
  132. hList += "<li class='toc-level-" + level + "'><a href='#" + headings[i].id + "'>" + headingText + "</a></li>";
  133.  
  134. /* Add "top" link to non-box headings */
  135. if (addTopLink && level > 0) {
  136. headings[i].insertAdjacentHTML("beforeend", "<a class='to-top' href='#toc'>&nbsp;^&nbsp;top</a>");
  137. }
  138.  
  139. /* Update past_level */
  140. past_level = level;
  141. }
  142.  
  143. /* Close sublists per current level */
  144. for(var k = 0; k < past_level; k++) {
  145. hList += "</li></ul>";
  146. }
  147. /* Close TOC */
  148. hList += "</div>";
  149.  
  150. /* Insert element after Pins (and entity links) */
  151. /* Calendars use only one sidebar */
  152. if (document.getElementById('app').parentNode.classList.contains("kanka-entity-calendar")) {
  153. document.getElementsByClassName('entity-sidebar-submenu')[0].insertAdjacentHTML("beforeend", hList);
  154. }
  155. /* Everything else */
  156. else {
  157. document.getElementsByClassName('entity-sidebar-pins')[0].insertAdjacentHTML("beforeend", hList);
  158. }
  159. }