GitHub Collapse Markdown

A userscript that collapses markdown headers

目前为 2016-10-03 提交的版本,查看 最新版本

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