GitHub Table of Contents

A userscript that adds a table of contents to readme & wiki pages

当前为 2019-04-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Table of Contents
  3. // @version 1.3.4
  4. // @description A userscript that adds a table of contents to readme & wiki pages
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @include https://gist.github.com/*
  10. // @run-at document-idle
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_getValue
  13. // @grant GM.getValue
  14. // @grant GM_setValue
  15. // @grant GM.setValue
  16. // @grant GM_addStyle
  17. // @grant GM.addStyle
  18. // @require https://gf.qytechs.cn/scripts/28721-mutations/code/mutations.js?version=666427
  19. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103
  20. // @icon https://github.githubassets.com/pinned-octocat.svg
  21. // ==/UserScript==
  22. (async () => {
  23. "use strict";
  24.  
  25. GM.addStyle(`
  26. /* z-index > 1000 to be above the */
  27. .ghus-toc { position:fixed; z-index:1001; min-width:200px; top:64px; right:10px; }
  28. .ghus-toc h3 { cursor:move; }
  29. .ghus-toc-title { padding-left:20px; }
  30. /* icon toggles TOC container & subgroups */
  31. .ghus-toc .ghus-toc-icon { vertical-align:baseline; }
  32. .ghus-toc h3 .ghus-toc-icon, .ghus-toc li.collapsible .ghus-toc-icon { cursor:pointer; }
  33. .ghus-toc .ghus-toc-toggle { position:absolute; width:28px; height:38px; top:0px; left:0px; }
  34. .ghus-toc .ghus-toc-toggle svg { margin-top:10px; margin-left:9px; }
  35. .ghus-toc .ghus-toc-docs { float:right; }
  36. /* move collapsed TOC to top right corner */
  37. .ghus-toc.collapsed {
  38. width:30px; height:30px; min-width:auto; overflow:hidden; top:16px !important; left:auto !important;
  39. right:10px !important; border:1px solid rgba(128, 128, 128, 0.5); border-radius:3px;
  40. }
  41. .ghus-toc.collapsed > h3 { cursor:pointer; padding-top:5px; border:none; background:#222; color:#ddd; }
  42. .ghus-toc.collapsed .ghus-toc-docs { display:none; }
  43. .ghus-toc:not(.ghus-toc-hidden).collapsed ~ .js-header-wrapper .Header { padding-right: 48px !important; }
  44. /* move header text out-of-view when collapsed */
  45. .ghus-toc.collapsed > h3 svg { margin-top:6px; }
  46. .ghus-toc-hidden, .ghus-toc.collapsed .boxed-group-inner,
  47. .ghus-toc li:not(.collapsible) .ghus-toc-icon { display:none; }
  48. .ghus-toc .boxed-group-inner { max-width:250px; max-height:400px; overflow-y:auto; overflow-x:hidden; }
  49. .ghus-toc ul { list-style:none; }
  50. .ghus-toc li { max-width:230px; white-space:nowrap; overflow-x:hidden; text-overflow:ellipsis; }
  51. .ghus-toc .ghus-toc-h1 { padding-left:15px; }
  52. .ghus-toc .ghus-toc-h2 { padding-left:30px; }
  53. .ghus-toc .ghus-toc-h3 { padding-left:45px; }
  54. .ghus-toc .ghus-toc-h4 { padding-left:60px; }
  55. .ghus-toc .ghus-toc-h5 { padding-left:75px; }
  56. .ghus-toc .ghus-toc-h6 { padding-left:90px; }
  57. /* anchor collapsible icon */
  58. .ghus-toc li.collapsible .ghus-toc-icon {
  59. width:16px; height:10px; display:inline-block; margin-left:-16px;
  60. background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSdvY3RpY29uJyBoZWlnaHQ9JzE0JyB2aWV3Qm94PScwIDAgMTIgMTYnPjxwYXRoIGQ9J00wIDVsNiA2IDYtNkgweic+PC9wYXRoPjwvc3ZnPg==) left center no-repeat;
  61. }
  62. /* on rotate, height becomes width, so this is keeping things lined up */
  63. .ghus-toc li.collapsible.collapsed .ghus-toc-icon { -webkit-transform:rotate(-90deg); transform:rotate(-90deg); height:10px; width:12px; margin-right:2px; }
  64. .ghus-toc-icon svg, .ghus-toc-docs svg { pointer-events:none; }
  65. .ghus-toc-no-selection { -webkit-user-select:none !important; -moz-user-select:none !important; user-select:none !important; }
  66. `);
  67.  
  68. let tocInit = false;
  69.  
  70. // modifiable title
  71. let title = await GM.getValue("github-toc-title", "Table of Contents");
  72.  
  73. const container = document.createElement("div");
  74. const useClient = !!document.all;
  75.  
  76. // keyboard shortcuts
  77. const keyboard = {
  78. toggle : "g+t",
  79. restore : "g+r",
  80. timer : null,
  81. lastKey : null,
  82. delay : 1000 // ms between keyboard shortcuts
  83. };
  84.  
  85. // drag variables
  86. const drag = {
  87. el : null,
  88. elmX : 0,
  89. elmY : 0,
  90. time : 0,
  91. unsel : null
  92. };
  93.  
  94. const stopPropag = event => {
  95. event.preventDefault();
  96. event.stopPropagation();
  97. };
  98.  
  99. // drag code adapted from http://jsfiddle.net/tovic/Xcb8d/light/
  100. function dragInit(event) {
  101. if (!container.classList.contains("collapsed")) {
  102. const x = useClient ? window.event.clientX : event.pageX;
  103. const y = useClient ? window.event.clientY : event.pageY;
  104. drag.el = container;
  105. drag.elmX = x - drag.el.offsetLeft;
  106. drag.elmY = y - drag.el.offsetTop;
  107. selectionToggle(true);
  108. } else {
  109. drag.el = null;
  110. }
  111. drag.time = new Date().getTime() + 500;
  112. }
  113.  
  114. function dragMove(event) {
  115. if (drag.el !== null) {
  116. const x = useClient ? window.event.clientX : event.pageX;
  117. const y = useClient ? window.event.clientY : event.pageY;
  118. drag.el.style.left = (x - drag.elmX) + "px";
  119. drag.el.style.top = (y - drag.elmY) + "px";
  120. drag.el.style.right = "auto";
  121. }
  122. }
  123.  
  124. function dragStop() {
  125. if (drag.el !== null) {
  126. dragSave();
  127. selectionToggle();
  128. }
  129. drag.el = null;
  130. }
  131.  
  132. async function dragSave(clear) {
  133. let val = clear ? null : [container.style.left, container.style.top];
  134. await GM.setValue("github-toc-location", val);
  135. }
  136.  
  137. // stop text selection while dragging
  138. function selectionToggle(disable) {
  139. const body = $("body");
  140. if (disable) {
  141. // save current "unselectable" value
  142. drag.unsel = body.getAttribute("unselectable");
  143. body.setAttribute("unselectable", "on");
  144. body.classList.add("ghus-toc-no-selection");
  145. on(body, "onselectstart", stopPropag);
  146. } else {
  147. if (drag.unsel) {
  148. body.setAttribute("unselectable", drag.unsel);
  149. }
  150. body.classList.remove("ghus-toc-no-selection");
  151. body.removeEventListener("onselectstart", stopPropag);
  152. }
  153. removeSelection();
  154. }
  155.  
  156. function removeSelection() {
  157. // remove text selection - http://stackoverflow.com/a/3171348/145346
  158. const sel = window.getSelection ? window.getSelection() : document.selection;
  159. if (sel) {
  160. if (sel.removeAllRanges) {
  161. sel.removeAllRanges();
  162. } else if (sel.empty) {
  163. sel.empty();
  164. }
  165. }
  166. }
  167.  
  168. async function tocShow() {
  169. container.classList.remove("collapsed");
  170. await GM.setValue("github-toc-hidden", false);
  171. }
  172.  
  173. async function tocHide() {
  174. container.classList.add("collapsed");
  175. await GM.setValue("github-toc-hidden", true);
  176. }
  177.  
  178. function tocToggle() {
  179. // don't toggle content on long clicks
  180. if (drag.time > new Date().getTime()) {
  181. if (container.classList.contains("collapsed")) {
  182. tocShow();
  183. } else {
  184. tocHide();
  185. }
  186. }
  187. }
  188. // hide TOC entirely, if no rendered markdown detected
  189. function tocView(isVisible) {
  190. const toc = $(".ghus-toc");
  191. if (toc) {
  192. toc.classList.toggle("ghus-toc-hidden", !isVisible);
  193. }
  194. }
  195.  
  196. function tocAdd() {
  197. if (!tocInit) {
  198. return;
  199. }
  200. if ($("#wiki-content, #readme")) {
  201. let indx, header, anchor, txt;
  202. let content = "<ul>";
  203. const anchors = $$(".markdown-body .anchor");
  204. const len = anchors.length;
  205. if (len > 1) {
  206. for (indx = 0; indx < len; indx++) {
  207. anchor = anchors[indx];
  208. if (anchor.parentNode) {
  209. header = anchor.parentNode;
  210. // replace single & double quotes with right angled quotes
  211. txt = header.textContent.trim().replace(/'/g, "&#8217;").replace(/"/g, "&#8221;");
  212. content += `
  213. <li class="ghus-toc-${header.nodeName.toLowerCase()}">
  214. <span class="ghus-toc-icon octicon ghd-invert"></span>
  215. <a href="${anchor.hash}" title="${txt}">${txt}</a>
  216. </li>
  217. `;
  218. }
  219. }
  220. $(".boxed-group-inner", container).innerHTML = content + "</ul>";
  221. tocView(true);
  222. listCollapsible();
  223. } else {
  224. tocView();
  225. }
  226. } else {
  227. tocView();
  228. }
  229. }
  230.  
  231. function listCollapsible() {
  232. let indx, el, next, count, num, group;
  233. const els = $$("li", container);
  234. const len = els.length;
  235. for (indx = 0; indx < len; indx++) {
  236. count = 0;
  237. group = [];
  238. el = els[indx];
  239. next = el && el.nextElementSibling;
  240. if (next) {
  241. num = el.className.match(/\d/)[0];
  242. while (next && !next.classList.contains("ghus-toc-h" + num)) {
  243. if (next.className.match(/\d/)[0] > num) {
  244. count++;
  245. group[group.length] = next;
  246. }
  247. next = next.nextElementSibling;
  248. }
  249. if (count > 0) {
  250. el.className += " collapsible collapsible-" + indx;
  251. addClass(group, "ghus-toc-childof-" + indx);
  252. }
  253. }
  254. }
  255. group = [];
  256. on(container, "click", event => {
  257. // Allow doc link to work
  258. if (event.target.nodeName.toLowerCase() === "a") {
  259. return;
  260. }
  261. stopPropag(event);
  262. // click on icon, then target LI parent
  263. let els, name, indx;
  264. const el = event.target.parentNode;
  265. const collapse = el.classList.contains("collapsed");
  266. if (event.target.classList.contains("ghus-toc-icon")) {
  267. if (event.shiftKey) {
  268. name = el.className.match(/ghus-toc-h\d/);
  269. els = name ? $$("." + name, container) : [];
  270. indx = els.length;
  271. while (indx--) {
  272. collapseChildren(els[indx], collapse);
  273. }
  274. } else {
  275. collapseChildren(el, collapse);
  276. }
  277. removeSelection();
  278. }
  279. });
  280. }
  281.  
  282. function collapseChildren(el, collapse) {
  283. const name = el && el.className.match(/collapsible-(\d+)/);
  284. const children = name ? $$(".ghus-toc-childof-" + name[1], container) : null;
  285. if (children) {
  286. if (collapse) {
  287. el.classList.remove("collapsed");
  288. removeClass(children, "ghus-toc-hidden");
  289. } else {
  290. el.classList.add("collapsed");
  291. addClass(children, "ghus-toc-hidden");
  292. }
  293. }
  294. }
  295.  
  296. // keyboard shortcuts
  297. // GitHub hotkeys are set up to only go to a url, so rolling our own
  298. function keyboardCheck(event) {
  299. clearTimeout(keyboard.timer);
  300. // use "g+t" to toggle the panel; "g+r" to reset the position
  301. // keypress may be needed for non-alphanumeric keys
  302. const tocToggleKeys = keyboard.toggle.split("+");
  303. const tocReset = keyboard.restore.split("+");
  304. const key = String.fromCharCode(event.which).toLowerCase();
  305. const panelHidden = container.classList.contains("collapsed");
  306.  
  307. // press escape to close the panel
  308. if (event.which === 27 && !panelHidden) {
  309. tocHide();
  310. return;
  311. }
  312. // prevent opening panel while typing in comments
  313. if (/(input|textarea)/i.test(document.activeElement.nodeName)) {
  314. return;
  315. }
  316. // toggle TOC (g+t)
  317. if (keyboard.lastKey === tocToggleKeys[0] && key === tocToggleKeys[1]) {
  318. if (panelHidden) {
  319. tocShow();
  320. } else {
  321. tocHide();
  322. }
  323. }
  324. // reset TOC window position (g+r)
  325. if (keyboard.lastKey === tocReset[0] && key === tocReset[1]) {
  326. container.setAttribute("style", "");
  327. dragSave(true);
  328. }
  329. keyboard.lastKey = key;
  330. keyboard.timer = setTimeout(() => {
  331. keyboard.lastKey = null;
  332. }, keyboard.delay);
  333. }
  334.  
  335. async function init() {
  336. // there is no ".header" on github.com/contact; and some other pages
  337. if (!$(".header, .Header, .js-header-wrapper") || tocInit) {
  338. return;
  339. }
  340. // insert TOC after header
  341. const location = await GM.getValue("github-toc-location", null);
  342. // restore last position
  343. if (location) {
  344. container.style.left = location[0];
  345. container.style.top = location[1];
  346. container.style.right = "auto";
  347. }
  348.  
  349. // TOC saved state
  350. const hidden = await GM.getValue("github-toc-hidden", false);
  351. container.className = "ghus-toc boxed-group wiki-pages-box readability-sidebar" + (hidden ? " collapsed" : "");
  352. container.setAttribute("role", "navigation");
  353. container.setAttribute("unselectable", "on");
  354. container.innerHTML = `
  355. <h3 class="js-wiki-toggle-collapse wiki-auxiliary-content" data-hotkey="g t">
  356. <span class="ghus-toc-toggle ghus-toc-icon">
  357. <svg class="octicon" height="14" width="14" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 12">
  358. <path d="M2 13c0 .6 0 1-.6 1H.6c-.6 0-.6-.4-.6-1s0-1 .6-1h.8c.6 0 .6.4.6 1zm2.6-9h6.8c.6 0 .6-.4.6-1s0-1-.6-1H4.6C4 2 4 2.4 4 3s0 1 .6 1zM1.4 7H.6C0 7 0 7.4 0 8s0 1 .6 1h.8C2 9 2 8.6 2 8s0-1-.6-1zm0-5H.6C0 2 0 2.4 0 3s0 1 .6 1h.8C2 4 2 3.6 2 3s0-1-.6-1zm10 5H4.6C4 7 4 7.4 4 8s0 1 .6 1h6.8c.6 0 .6-.4.6-1s0-1-.6-1zm0 5H4.6c-.6 0-.6.4-.6 1s0 1 .6 1h6.8c.6 0 .6-.4.6-1s0-1-.6-1z"/>
  359. </svg>
  360. </span>
  361. <span class="ghus-toc-title">${title}</span>
  362. <a class="ghus-toc-docs tooltipped tooltipped-w" aria-label="Go to documentation" href="https://github.com/Mottie/GitHub-userscripts/wiki/GitHub-table-of-contents">
  363. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 16 14">
  364. <path d="M6 10h2v2H6V10z m4-3.5c0 2.14-2 2.5-2 2.5H6c0-0.55 0.45-1 1-1h0.5c0.28 0 0.5-0.22 0.5-0.5v-1c0-0.28-0.22-0.5-0.5-0.5h-1c-0.28 0-0.5 0.22-0.5 0.5v0.5H4c0-1.5 1.5-3 3-3s3 1 3 2.5zM7 2.3c3.14 0 5.7 2.56 5.7 5.7S10.14 13.7 7 13.7 1.3 11.14 1.3 8s2.56-5.7 5.7-5.7m0-1.3C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7S10.86 1 7 1z" />
  365. </svg>
  366. </a>
  367. </h3>
  368. <div class="boxed-group-inner wiki-auxiliary-content wiki-auxiliary-content-no-bg"></div>
  369. `;
  370.  
  371. // add container
  372. const el = $(".header, .js-header-wrapper");
  373. el.parentNode.insertBefore(container, el);
  374.  
  375. // make draggable
  376. on($("h3", container), "mousedown", dragInit);
  377. on(document, "mousemove", dragMove);
  378. on(document, "mouseup", dragStop);
  379. // toggle TOC
  380. on($(".ghus-toc-icon", container), "mouseup", tocToggle);
  381. // prevent container content selection
  382. on(container, "onselectstart", stopPropag);
  383. // keyboard shortcuts
  384. on(document, "keydown", keyboardCheck);
  385.  
  386. tocInit = true;
  387. tocAdd();
  388. }
  389.  
  390. function $(str, el) {
  391. return (el || document).querySelector(str);
  392. }
  393.  
  394. function $$(str, el) {
  395. return Array.from((el || document).querySelectorAll(str));
  396. }
  397.  
  398. function on(el, name, handler) {
  399. el.addEventListener(name, handler);
  400. }
  401.  
  402. function addClass(els, name) {
  403. let indx;
  404. const len = els.length;
  405. for (indx = 0; indx < len; indx++) {
  406. els[indx].classList.add(name);
  407. }
  408. }
  409.  
  410. function removeClass(els, name) {
  411. let indx;
  412. const len = els.length;
  413. for (indx = 0; indx < len; indx++) {
  414. els[indx].classList.remove(name);
  415. }
  416. }
  417.  
  418. // Add GM options
  419. GM.registerMenuCommand("Set Table of Contents Title", async () => {
  420. title = prompt("Table of Content Title:", title);
  421. await GM.setValue("github-toc-title", title);
  422. $("h3 span", container).textContent = title;
  423. });
  424.  
  425. on(document, "ghmo:container", tocAdd);
  426. on(document, "ghmo:preview", tocAdd);
  427. init();
  428.  
  429. })();

QingJ © 2025

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