GitHub Collapse Markdown

A userscript that collapses markdown headers

目前为 2016-07-30 提交的版本,查看 最新版本

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