Table of Contents Everywhere

On pages which do not have a Table of Contents, but should do, create one! (I actually use this as a bookmarklet, so I can load it onto the current page only when I want it.)

目前为 2018-05-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Table of Contents Everywhere
  3. // @description On pages which do not have a Table of Contents, but should do, create one! (I actually use this as a bookmarklet, so I can load it onto the current page only when I want it.)
  4. // @downstreamURL http://userscripts.org/scripts/source/123255.user.js
  5. // @license ISC
  6. // @version 1.0.4
  7. // @include http://*/*
  8. // @include https://*/*
  9. // @include file://*
  10. // @grant none
  11. // @namespace https://gf.qytechs.cn/users/8615
  12. // ==/UserScript==
  13.  
  14. var minimumItems = 4; // Don't display a TOC for fewer than this number of entries.
  15. var maximumItems = 800; // Don't display a TOC for more than this number of entries.
  16. var delayBeforeRunning = 1600;
  17. var showAnchors = true;
  18. var pushAnchorsToBottom = true; // They can look messy interspersed amongst TOC tree
  19. var startRolledUp = false;
  20.  
  21. // 2015-05-12 Improved shadow styling
  22. // 2015-01-02 Improved styling
  23. // 2012-02-19 Removed verbose log. Added showAnchors. Added https since everyone is forcing that now (e.g. github).
  24. // 2012-02-18 Fixed sorting of TOC elements. Added anchor unicode.
  25. // 2012-01-30 Implemented GM_log and GM_addStyle so this script can be included on any web page.
  26.  
  27. // TODO: derbyjs.com is an example of a site with a <div id=toc> that has no
  28. // hide or close buttons. Perhaps we should add close and rollup buttons if we
  29. // cannot find any recognisable buttons. (Medawiki tocs for example, do have a
  30. // show/hide button, so we don't want to add to them!)
  31.  
  32. // TODO: whatwg.org presents its own TOC but with no title. Our buttons appear in the wrong place!
  33.  
  34. // BUG: Displays links for elements which may be invisible due to CSS. (e.g. see github markdown pages)
  35.  
  36. // TODO CONSIDER: TOC hijacking _whitelist_ to avoid creeping fixes for per-site issues. Different problems are appearing on a small proportion of websites when we try to consume/hijack their existing TOC. It would be better to create our own *separate* TOC as standard, and only hijack *known* friendly TOCs such as WikiMedia's / Wikia's.
  37. // (We might offer a tiny button "Try to Use Page TOC" allowing us to test hijack before adding the site to the whitelist.)
  38.  
  39. setTimeout(function(){
  40.  
  41.  
  42.  
  43. // Implementing these two means we can run as a stand-alone script on any page.
  44. if (typeof GM_log == "undefined") {
  45. GM_log = function() {
  46. // Firefox's console.log does not have apply or call functions!
  47. var txt = Array.prototype.join.call(arguments," ");
  48. console.log(txt);
  49. };
  50. }
  51. if (typeof GM_addStyle == "undefined") {
  52. this.GM_addStyle = function(css) {
  53. var s = document.createElement("style");
  54. s.type = 'text/css';
  55. s.innerHTML = css;
  56. document.getElementsByTagName("head")[0].appendChild(s);
  57. };
  58. }
  59.  
  60. // Implementing these allows us to remember toggled state. (Chrome's set/getValue don't work.)
  61. if (typeof GM_setValue == 'undefined' || window.navigator.vendor.match(/Google/)) {
  62. GM_log("[TOCE] Adding fallback implementation of GM_set/getValue");
  63.  
  64. if (typeof localStorage == 'undefined') {
  65.  
  66. GM_getValue = function(name, defaultValue) {
  67. return defaultValue;
  68. };
  69.  
  70. } else {
  71.  
  72. GM_setValue = function(name, value) {
  73. value = (typeof value)[0] + value;
  74. localStorage.setItem(name, value);
  75. };
  76.  
  77. GM_getValue = function(name, defaultValue) {
  78. var value = localStorage.getItem(name);
  79. if (!value)
  80. return defaultValue;
  81. var type = value[0];
  82. value = value.substring(1);
  83. switch (type) {
  84. case 'b':
  85. return value == 'true';
  86. case 'n':
  87. return Number(value);
  88. default:
  89. return value;
  90. }
  91. };
  92.  
  93. }
  94.  
  95. }
  96.  
  97. function loadScript(url,thenCallFn) {
  98. GM_log("[TOCE] Loading fallback: "+url);
  99. var scr = document.createElement("script");
  100. scr.src = url;
  101. scr.type = "text/javascript"; // Konqueror 3.5 needs this!
  102. if (thenCallFn) {
  103. var called = false;
  104. function onceOnlyCallback(evt) {
  105. if (!called) {
  106. called = true;
  107. thenCallFn(evt);
  108. }
  109. }
  110. function errorCallback(evt) {
  111. GM_log("[TOCE] Failed to load: "+url,evt);
  112. onceOnlyCallback(evt);
  113. }
  114. scr.addEventListener('load',onceOnlyCallback,false);
  115. scr.addEventListener('error',errorCallback,false);
  116. // Fallback in case above events unsupported by browser (e.g. Konq 3.5)
  117. setTimeout(onceOnlyCallback,5000);
  118. }
  119. document.body.appendChild(scr);
  120. }
  121.  
  122. // Modified for this script's needs.
  123. // Returns e.g. "/*[2]/*[4]/*[9]"
  124. function getXPath(node) {
  125. var parent = node.parentNode;
  126. if (!parent) {
  127. return '';
  128. }
  129. var siblings = parent.childNodes;
  130. var totalCount = 0;
  131. var thisCount = -1;
  132. for (var i=0;i<siblings.length;i++) {
  133. var sibling = siblings[i];
  134. if (true /*sibling.nodeType == node.nodeType*/) {
  135. totalCount++;
  136. }
  137. if (sibling == node) {
  138. thisCount = totalCount;
  139. break;
  140. }
  141. }
  142. // return getXPath(parent) + '/*' /*node.nodeName.toLowerCase()*/ + (totalCount>1 ? '[' + thisCount + ']' : '' );
  143. // Remain consistent:
  144. return getXPath(parent) + '/*' + '[' + thisCount + ']';
  145. }
  146.  
  147. // Konqueror 3.5 lacks some things!
  148. if (!Array.prototype.map) {
  149. Array.prototype.map = function(fn) {
  150. var l = [];
  151. for (var i=0;i<this.length;i++) {
  152. l.push(fn(this[i]));
  153. }
  154. return l;
  155. };
  156. }
  157. if (!String.prototype.trim) {
  158. String.prototype.trim = function() {
  159. return this.replace(/^[ \t]+/,'').replace(/[ \t]+$/,'');
  160. };
  161. }
  162.  
  163.  
  164.  
  165. // The following block is mirrored in wikiindent.user.js
  166.  
  167. // See also: resetProps
  168. function clearStyle(elem) {
  169. // We set some crucial defaults, so we don't inherit CSS from the page:
  170. elem.style.display = 'inline';
  171. elem.style.position = 'static';
  172. elem.style.top = 'auto';
  173. elem.style.right = 'auto';
  174. elem.style.bottom = 'auto';
  175. elem.style.left = 'auto';
  176. elem.style.color = 'black';
  177. elem.style.backgroundColor = '#f4f4f4';
  178. elem.style.border = '0px solid magenta';
  179. elem.style.padding = '0px';
  180. elem.style.margin = '1px';
  181. return elem;
  182. }
  183.  
  184. function newNode(tag,data) {
  185. var elem = document.createElement(tag);
  186. if (data) {
  187. for (var prop in data) {
  188. elem[prop] = data[prop];
  189. }
  190. }
  191. return elem;
  192. }
  193.  
  194. function newSpan(text) {
  195. return clearStyle(newNode("span",{textContent:text}));
  196. }
  197.  
  198. function addCloseButtonTo(where, toc) {
  199. var closeButton = newSpan("[X]");
  200. // closeButton.style.float = 'right';
  201. // closeButton.style.cssFloat = 'right'; // Firefox
  202. // closeButton.style.styleFloat = 'right'; // IE7
  203. closeButton.style.cursor = 'pointer';
  204. closeButton.style.paddingLeft = '5px';
  205. closeButton.onclick = function() { toc.parentNode.removeChild(toc); };
  206. closeButton.id = "closeTOC";
  207. where.appendChild(closeButton);
  208. }
  209.  
  210. function addHideButtonTo(toc, tocInner) {
  211. var rollupButton = newSpan("[-]");
  212. // rollupButton.style.float = 'right';
  213. // rollupButton.style.cssFloat = 'right'; // Firefox
  214. // rollupButton.style.styleFloat = 'right'; // IE7
  215. rollupButton.style.cursor = 'pointer';
  216. rollupButton.style.paddingLeft = '10px';
  217. function toggleRollUp() {
  218. if (tocInner.style.display == 'none') {
  219. tocInner.style.display = '';
  220. rollupButton.textContent = "[-]";
  221. } else {
  222. tocInner.style.display = 'none';
  223. rollupButton.textContent = "[+]";
  224. }
  225. setTimeout(function(){
  226. GM_setValue("TOCE_rolledUp", tocInner.style.display=='none');
  227. },5);
  228. }
  229. rollupButton.onclick = toggleRollUp;
  230. rollupButton.id = "togglelink";
  231. toc.appendChild(rollupButton);
  232. if (startRolledUp || GM_getValue("TOCE_rolledUp",false)) {
  233. toggleRollUp();
  234. }
  235. }
  236.  
  237. function addButtonsConditionally(toc) {
  238.  
  239. function verbosely(fn) {
  240. return function() {
  241. // GM_log("[WI] Calling: "+fn+" with ",arguments);
  242. return fn.apply(this,arguments);
  243. };
  244. };
  245.  
  246. // Provide a hide/show toggle button if the TOC does not already have one.
  247.  
  248. // Wikimedia's toc element is actually a table. We must put the
  249. // buttons in the title div, if we can find it!
  250.  
  251. var tocTitle = document.getElementById("toctitle"); // Wikipedia
  252. tocTitle = tocTitle || toc.getElementsByTagName("h2")[0]; // Mozdev
  253. // tocTitle = tocTitle || toc.getElementsByTagName("div")[0]; // Fingers crossed for general
  254. tocTitle = tocTitle || toc.firstChild; // Fingers crossed for general
  255.  
  256. // Sometimes Wikimedia does not add a hide/show button (if the TOC is small).
  257. // We cannot test this immediately, because it gets loaded in later!
  258. function addButtonsNow() {
  259.  
  260. var hideShowButton = document.getElementById("togglelink");
  261. if (!hideShowButton) {
  262. var tocInner = toc.getElementsByTagName("ol")[0]; // Mozdev (can't get them all!)
  263. tocInner = tocInner || toc.getElementsByTagName("ul")[0]; // Wikipedia
  264. tocInner = tocInner || toc.getElementsByTagName("div")[0]; // Our own
  265. if (tocInner) {
  266. verbosely(addHideButtonTo)(tocTitle || toc, tocInner);
  267. }
  268. }
  269.  
  270. // We do this later, to ensure it appears on the right of
  271. // any existing [hide/show] button.
  272. if (document.getElementById("closeTOC") == null) {
  273. verbosely(addCloseButtonTo)(tocTitle || toc, toc);
  274. }
  275.  
  276. }
  277.  
  278. // Sometimes Wikimedia does not add a hide/show button (if the TOC is small).
  279. // We cannot test this immediately, because it gets loaded in later!
  280. if (document.location.href.indexOf("wiki") >= 0) {
  281. setTimeout(addButtonsNow,2000);
  282. } else {
  283. addButtonsNow();
  284. }
  285.  
  286. }
  287.  
  288. // End mirror.
  289.  
  290.  
  291.  
  292. // == Main == //
  293.  
  294. function buildTableOfContents() {
  295.  
  296. // Can we make a TOC?
  297. var headers = "//h1 | //h2 | //h3 | //h4 | //h5 | //h6 | //h7 | //h8";
  298. var anchors = "//a[@name]";
  299. // For coffeescript.org:
  300. var elementsMarkedAsHeader = "//*[@class='header']";
  301. // However on many sites that might be the thing opposite the footer, and probably not of note.
  302.  
  303. var xpathQuery = headers+(showAnchors?"|"+anchors:"")+"|"+elementsMarkedAsHeader;
  304. var nodeSnapshot = document.evaluate(xpathQuery,document,null,6,null);
  305. //// Chrome needs lower-case 'h', Firefox needs upper-case 'H'!
  306. // var nodeSnapshot = document.evaluate("//*[starts-with(name(.),'h') and substring(name(.),2) = string(number(substring(name(.),2)))]",document,null,6,null);
  307. // var nodeSnapshot = document.evaluate("//*[starts-with(name(.),'H') and substring(name(.),2) = string(number(substring(name(.),2)))]",document,null,6,null);
  308.  
  309. if (nodeSnapshot.snapshotLength > maximumItems) {
  310. GM_log("[TOCE] Too many nodes for table (sanity): "+nodeSnapshot.snapshotLength);
  311. } else if (nodeSnapshot.snapshotLength >= minimumItems) {
  312.  
  313. GM_log("[TOCE] Making TOC with "+nodeSnapshot.snapshotLength+" nodes.");
  314.  
  315. var toc = newNode("div");
  316. toc.id = 'toc';
  317.  
  318. // var heading = newSpan("Table of Contents");
  319. var heading = clearStyle(newNode("h2",{textContent:"Table of Contents"}));
  320. heading.id = 'toctitle'; // Like Wikipedia
  321. heading.style.fontWeight = "bold";
  322. heading.style.fontSize = "100%";
  323. toc.appendChild(heading);
  324.  
  325. var table = newNode("div");
  326. // addHideButtonTo(toc,table);
  327. table.id = 'toctable'; // Our own
  328. toc.appendChild(table);
  329.  
  330. // We need to do this *after* adding the table.
  331. addButtonsConditionally(toc);
  332.  
  333. // The xpath query did not return the elements in page-order.
  334. // We sort them back into the order they appear in the document
  335. // Yep it's goofy code, but it works.
  336. var nodeArray = [];
  337. for (var i=0;i<nodeSnapshot.snapshotLength;i++) {
  338. var node = nodeSnapshot.snapshotItem(i);
  339. nodeArray.push(node);
  340. // We need to sort numerically, since with strings "24" < "4"
  341. node.magicPath = getXPath(node).substring(3).slice(0,-1).split("]/*[").map(Number);
  342. if (pushAnchorsToBottom && node.tagName==="A") {
  343. node.magicPath.unshift(+Infinity);
  344. }
  345. }
  346. nodeArray.sort(function(a,b){
  347. // GM_log("[TOCE] Comparing "+a.magicPath+" against "+b.magicPath);
  348. for (var i=0;i<a.magicPath.length;i++) {
  349. if (i >= b.magicPath.length) {
  350. return +1; // b wins (comes earlier)
  351. }
  352. if (a.magicPath[i] > b.magicPath[i]) {
  353. return +1; // b wins
  354. }
  355. if (a.magicPath[i] < b.magicPath[i]) {
  356. return -1; // a wins
  357. }
  358. }
  359. return -1; // assume b is longer, or they are equal
  360. });
  361.  
  362. for (var i=0;i<nodeArray.length;i++) {
  363. var node = nodeArray[i];
  364.  
  365. var level = (node.tagName.substring(1) | 0) - 1;
  366. if (level < 0) {
  367. level = 0;
  368. }
  369.  
  370. var linkText = node.textContent && node.textContent.trim() || node.name;
  371. if (!linkText) {
  372. continue; // skip things we cannot name
  373. }
  374.  
  375. var link = clearStyle(newNode("A"));
  376. if (linkText.length > 40) {
  377. link.title = linkText; // Show full title on hover
  378. linkText = linkText.substring(0,32)+"...";
  379. }
  380. link.textContent = linkText;
  381. /* Dirty hack for Wikimedia: */
  382. if (link.textContent.substring(0,7) == "[edit] ") {
  383. link.textContent = link.textContent.substring(7);
  384. }
  385. if (node.tagName == "A") {
  386. link.href = '#'+node.name;
  387. } else {
  388. (function(node){
  389. link.onclick = function(evt){
  390. node.scrollIntoView();
  391.  
  392. // Optional: CSS animation
  393. // NOT WORKING!
  394. /*
  395. node.id = "toc_current_hilight";
  396. ["","-moz-","-webkit-"].forEach(function(insMode){
  397. GM_addStyle("#toc_current_hilight { "+insMode+"animation: 'fadeHighlight 4s ease-in 1s alternate infinite'; }@"+insMode+"keyframes fadeHighlight { 0%: { background-color: yellow; } 100% { background-color: rgba(255,255,0,0); } }");
  398. });
  399. */
  400.  
  401. evt.preventDefault();
  402. return false;
  403. };
  404. })(node);
  405. link.href = '#';
  406. }
  407. table.appendChild(link);
  408.  
  409. // For better layout, we will now replace that link with a neater li.
  410. liType = "li";
  411. if (node.tagName == "A") {
  412. liType = "div";
  413. }
  414. var li = newNode(liType);
  415. // clearStyle(li); // display:inline; is bad on LIs!
  416. // li.style.display = 'list-item'; // not working on Github
  417. link.parentNode.replaceChild(li,link);
  418. if (node.tagName == "A") {
  419. li.appendChild(document.createTextNode("\u2693 "));
  420. }
  421. li.appendChild(link);
  422. li.style.paddingLeft = (1.5*level)+"em";
  423. li.style.fontSize = (100-6*(level+1))+"%";
  424. li.style.size = li.style.fontSize;
  425.  
  426. // Debugging:
  427. /*
  428. li.title = node.tagName;
  429. if (node.name)
  430. li.title += " (#"+node.name+")";
  431. li.title = getXPath(node);
  432. */
  433.  
  434. }
  435.  
  436. document.body.appendChild(toc);
  437.  
  438. // TODO scrollIntoView if newly matching 1.hash exists
  439.  
  440. postTOC(toc);
  441.  
  442. } else {
  443. GM_log("[TOCE] Not enough items found to create toc.");
  444. }
  445.  
  446. return toc;
  447.  
  448. }
  449.  
  450. function postTOC(toc) {
  451. if (toc) {
  452.  
  453. // We make the TOC float regardless whether we created it or it already existed.
  454. // Interestingly, the overflow settings seems to apply to all sub-elements.
  455. // E.g.: http://mewiki.project357.com/wiki/X264_Settings#Input.2FOutput
  456. // FIXED: Some of the sub-trees are so long that they also get scrollbars, which is a bit messy!
  457. // FIXED : max-width does not do what I want! To see, find a TOC with really wide section titles (long lines).
  458.  
  459. // Also in Related_Links_Pager.user.js
  460. // See also: clearStyle
  461. var resetProps = " width: auto; height: auto; max-width: none; max-height: none; ";
  462.  
  463. if (toc.id === "") {
  464. toc.id = "toc";
  465. }
  466. var tocID = toc.id;
  467. GM_addStyle("#"+tocID+" { position: fixed; top: 10%; right: 4%; background-color: #f4f4f4; color: black; font-weight: normal; padding: 5px; border: 1px solid grey; z-index: 9999999; "+resetProps+" }" // max-height: 80%; max-width: 32%; overflow: auto;
  468. + "#"+tocID+" { opacity: 0.4; }"
  469. + "#"+tocID+":hover { box-shadow: 0px 2px 10px 1px rgba(0,0,0,0.4); }"
  470. + "#"+tocID+":hover { -webkit-box-shadow: 0px 1px 4px 0px rgba(0,0,0,0.4); }"
  471. + "#"+tocID+":hover { opacity: 1.0; }"
  472. + "#"+tocID+" > * > * { opacity: 0.0; }"
  473. + "#"+tocID+":hover > * > * { opacity: 1.0; }"
  474. + "#"+tocID+" , #"+tocID+" > * > * { transition: opacity; transition-duration: 400ms; }"
  475. + "#"+tocID+" , #"+tocID+" > * > * { -webkit-transition: opacity; -webkit-transition-duration: 400ms; }"
  476. );
  477. GM_addStyle("#"+tocID+" > * { "+resetProps+" }");
  478.  
  479. var maxWidth = window.innerWidth * 0.40 | 0;
  480. var maxHeight = window.innerHeight * 0.80 | 0;
  481.  
  482. var table = document.getElementById("toctable");
  483. table = table || toc.getElementsByTagName("ul")[0]; // Wikipedia
  484. table = table || toc; // Give up, set for whole element
  485. table.style.overflow = 'auto';
  486. table.style.maxWidth = maxWidth+"px";
  487. table.style.maxHeight = maxHeight+"px";
  488.  
  489. }
  490. }
  491.  
  492. function searchForTOC() {
  493.  
  494. try {
  495.  
  496. var tocFound = document.getElementById("toc");
  497. // Konqueror 3.5 does NOT have document.getElementsByClassName(), so we check for it.
  498. tocFound = tocFound || (document.getElementsByClassName && document.getElementsByClassName("toc")[0]);
  499. tocFound = tocFound || document.getElementById("article-nav"); // developer.mozilla.org
  500. tocFound = tocFound || document.getElementById("page-toc"); // developer.mozilla.org
  501. tocFound = tocFound || (document.getElementsByClassName && document.getElementsByClassName("twikiToc")[0]); // TWiki
  502. tocFound = tocFound || document.getElementById("TOC"); // meteorpedia.com
  503. tocFound = tocFound || document.location.host==="developer.android.com" && document.getElementById("qv");
  504. if (document.location.host.indexOf("dartlang.org")>=0) {
  505. tocFound = null; // The toc they gives us contains top-level only. It's preferable to generate our own full tree.
  506. }
  507. // whatwg.org:
  508. /* if (document.getElementsByTagName("nav").length == 1) {
  509. GM_log("[TOCE] Using nav element.");
  510. tocFound = document.getElementsByTagName("nav")[0];
  511. } */
  512.  
  513. var toc = tocFound;
  514.  
  515. // With the obvious exception of Wikimedia sites, most found tocs do not contain a hide/close button.
  516. // TODO: If we are going to make the toc float, we should give it rollup/close buttons, unless it already has them.
  517. // The difficulty here is: where to add the buttons in the TOC, and which part of the TOC to hide, without hiding the buttons!
  518. // Presumably we need to identify the title element (first with textContent) and collect everything after that into a hideable block (or hide/unhide each individually when needed).
  519.  
  520. if (toc) {
  521.  
  522. postTOC(toc);
  523.  
  524. addButtonsConditionally(toc);
  525.  
  526. } else {
  527.  
  528. toc = buildTableOfContents();
  529.  
  530. }
  531.  
  532. } catch (e) {
  533. GM_log("[TOCE] Error! "+e);
  534. }
  535.  
  536. }
  537.  
  538. if (document.evaluate /*this.XPathResult*/) {
  539. searchForTOC();
  540. } else {
  541. loadScript("http://hwi.ath.cx/javascript/xpath.js", searchForTOC);
  542. }
  543.  
  544.  
  545.  
  546. },delayBeforeRunning);
  547. // We want it to run fairly soon but it can be quite heavy on large pages - big XPath search.
  548.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址