GitHub Collapse Markdown

A userscript that collapses markdown headers

当前为 2016-06-27 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Collapse Markdown
  3. // @version 1.0.0
  4. // @description A userscript that collapses markdown headers
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @namespace https://github.com/Mottie
  7. // @include https://github.com/*
  8. // @include https://gist.github.com/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_registerMenuCommand
  14. // @author Rob Garrison
  15. // ==/UserScript==
  16. /* global GM_addStyle, GM_getValue, GM_setValue, GM_registerMenuCommand */
  17. /* jshint esnext:true, unused:true */
  18. (() => {
  19. "use strict";
  20.  
  21. let startCollapsed = GM_getValue("ghcm-collapsed", false),
  22. // palette generated by http://tools.medialab.sciences-po.fr/iwanthue/ (colorblind friendly, soft)
  23. defaultColors = ["#6778d0", "#ac9c3d", "#b94a73", "#56ae6c", "#9750a1", "#ba543d"],
  24. colors = GM_getValue("ghcm-colors", defaultColors),
  25.  
  26. headers = "H1 H2 H3 H4 H5 H6".split(" "),
  27. // markdown wrapper class name
  28. markdown = "markdown-body",
  29. collapsed = "ghcm-collapsed",
  30.  
  31. arrowColors = document.createElement("style");
  32.  
  33. GM_addStyle(`
  34. .${markdown} h1, .${markdown} h2, .${markdown} h3, .${markdown} h4,
  35. .${markdown} h5, .${markdown} h6 {
  36. position:relative;
  37. padding-right:.8em;
  38. cursor:pointer;
  39. }
  40. .${markdown} h1:after, .${markdown} h2:after, .${markdown} h3:after,
  41. .${markdown} h4:after, .${markdown} h5:after, .${markdown} h6:after {
  42. display:inline-block;
  43. position:absolute;
  44. right:0;
  45. top:calc(50% - .5em);
  46. font-size:.8em;
  47. content:"\u25bc";
  48. }
  49. .${markdown} .${collapsed}:after {
  50. content:"\u25C4";
  51. }
  52. /* clicking on header link won't pass svg as the event.target */
  53. .octicon-link {
  54. pointer-events:none;
  55. }
  56. .ghcm-hidden {
  57. display:none !important;
  58. }
  59. `);
  60.  
  61. function addColors() {
  62. arrowColors.textContent = `
  63. .${markdown} h1:after { color:${colors[0]} }
  64. .${markdown} h2:after { color:${colors[1]} }
  65. .${markdown} h3:after { color:${colors[2]} }
  66. .${markdown} h4:after { color:${colors[3]} }
  67. .${markdown} h5:after { color:${colors[4]} }
  68. .${markdown} h6:after { color:${colors[5]} }
  69. `;
  70. }
  71.  
  72. function toggle(el, shifted) {
  73. if (el) {
  74. el.classList.toggle(collapsed);
  75. let els,
  76. name = el.nodeName || "",
  77. level = parseInt(name.replace(/[^\d]/, ""), 10),
  78. isCollapsed = el.classList.contains(collapsed);
  79. if (shifted) {
  80. // collapse all same level anchors
  81. els = $$(`.${markdown} ${name}`);
  82. for (el of els) {
  83. nextHeader(el, level, isCollapsed);
  84. }
  85. } else {
  86. nextHeader(el, level, isCollapsed);
  87. }
  88. removeSelection();
  89. }
  90. }
  91.  
  92. function nextHeader(el, level, isCollapsed) {
  93. el.classList[isCollapsed ? "add" : "remove"](collapsed);
  94. let selector = headers.slice(0, level).join(","),
  95. els = [];
  96. el = el.nextElementSibling;
  97. while (el && !el.matches(selector)) {
  98. els[els.length] = el;
  99. el = el.nextElementSibling;
  100. }
  101. if (els.length) {
  102. if (isCollapsed) {
  103. addClass(els, "ghcm-hidden");
  104. } else {
  105. removeClass(els, collapsed + " ghcm-hidden");
  106. }
  107. }
  108. }
  109.  
  110. // show siblings of hash target
  111. function siblings(target) {
  112. let level = parseInt((target.nodeName || "").replace(/[^\d]/, ""), 10),
  113. el = target.nextElementSibling,
  114. selector = headers.slice(0, level - 1).join(","),
  115. els = [target];
  116. while (el && !el.matches(selector)) {
  117. els[els.length] = el;
  118. el = el.nextElementSibling;
  119. }
  120. el = target.previousElementSibling;
  121. while (el && !el.matches(selector)) {
  122. els[els.length] = el;
  123. el = el.previousElementSibling;
  124. }
  125. if (els.length) {
  126. els = els.filter(el => {
  127. return el.nodeName === target.nodeName;
  128. });
  129. removeClass(els, "ghcm-hidden");
  130. }
  131. nextHeader(target, level, false);
  132. }
  133.  
  134. function removeSelection() {
  135. // remove text selection - http://stackoverflow.com/a/3171348/145346
  136. var sel = window.getSelection ? window.getSelection() : document.selection;
  137. if (sel) {
  138. if (sel.removeAllRanges) {
  139. sel.removeAllRanges();
  140. } else if (sel.empty) {
  141. sel.empty();
  142. }
  143. }
  144. }
  145.  
  146. function addBinding() {
  147. document.addEventListener("click", event => {
  148. if (event.target.classList.contains("anchor")) {
  149. return;
  150. }
  151. // check if element is inside a header
  152. let target = closest(event.target, headers.join(","));
  153. if (target && headers.indexOf(target.nodeName || "") > -1) {
  154. // make sure the header is inside of markdown
  155. if (closest(target, `.${markdown}`)) {
  156. toggle(target, event.shiftKey);
  157. }
  158. }
  159. });
  160. }
  161.  
  162. function checkHash() {
  163. let el, els, md, tmp,
  164. mds = $$(`.${markdown}`);
  165. for (md of mds) {
  166. els = $$(headers.join(","), md);
  167. if (els.length > 1) {
  168. for (el of els) {
  169. if (el && !el.classList.contains(collapsed)) {
  170. toggle(el, true);
  171. }
  172. }
  173. }
  174. }
  175. // open up
  176. tmp = (window.location.hash || "").replace(/#/, "");
  177. if (tmp) {
  178. els = $(`#user-content-${tmp}`);
  179. if (els && els.classList.contains("anchor")) {
  180. el = els.parentNode;
  181. if (el.matches(headers.join(","))) {
  182. siblings(el);
  183. document.documentElement.scrollTop = el.offsetTop;
  184. // set scrollTop a second time, in case of browser lag
  185. setTimeout(() => {
  186. document.documentElement.scrollTop = el.offsetTop;
  187. }, 500);
  188. }
  189. }
  190. }
  191. }
  192.  
  193. function checkColors() {
  194. if (!colors || colors.length !== 6) {
  195. colors = [].concat(defaultColors);
  196. }
  197. }
  198.  
  199. function init() {
  200. document.querySelector("head").appendChild(arrowColors);
  201. checkColors();
  202. addColors();
  203. addBinding();
  204. if (startCollapsed) {
  205. checkHash();
  206. }
  207. }
  208.  
  209. function $(selector, el) {
  210. return (el || document).querySelector(selector);
  211. }
  212. function $$(selectors, el) {
  213. return Array.from((el || document).querySelectorAll(selectors));
  214. }
  215. function addClass(els, name) {
  216. for (let el of els) {
  217. el.classList.add(name);
  218. }
  219. }
  220. function removeClass(els, name) {
  221. name = (name || "").split(" ");
  222. for (let el of els) {
  223. el.classList.remove(...name);
  224. }
  225. }
  226. function closest(el, selector) {
  227. while (el && el.nodeName !== "BODY" && !el.matches(selector)) {
  228. el = el.parentNode;
  229. }
  230. return el && el.matches(selector) ? el : null;
  231. }
  232.  
  233. // Add GM options
  234. GM_registerMenuCommand("Set collapse markdown state", () => {
  235. let val = prompt("Initially collapse headers:", !startCollapsed);
  236. if (val !== null) {
  237. startCollapsed = /^t/.test(val);
  238. GM_setValue("ghcm-collapsed", startCollapsed);
  239. console.log(`GitHub Collapse Markdown: Headers will ${startCollapsed ? "be" : "not be"} initially collapsed`);
  240. }
  241. });
  242. GM_registerMenuCommand("Set collapse markdown colors", () => {
  243. let val = prompt("Set header arrow colors:", JSON.stringify(colors));
  244. if (val !== null) {
  245. // allow pasting in a JSON format
  246. try {
  247. val = JSON.parse(val);
  248. if (val && val.length === 6) {
  249. colors = val;
  250. GM_setValue("ghcm-colors", colors);
  251. console.log("GitHub Collapse Markdown: colors set to", colors);
  252. addColors();
  253. return;
  254. } else {
  255. console.error("GitHub Collapse Markdown: invalid color definition (6 colors)", val);
  256. // reset colors to default (in case colors variable is corrupted)
  257. checkColors();
  258. }
  259. } catch(err) {
  260. console.error("GitHub Collapse Markdown: invalid JSON");
  261. }
  262. }
  263. });
  264.  
  265. init();
  266.  
  267. })();