Kanka Automatic Table of Contents

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

当前为 2021-08-23 提交的版本,查看 最新版本

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