GitHub Collapse Markdown

A userscript that collapses markdown headers

目前为 2017-03-25 提交的版本。查看 最新版本

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

QingJ © 2025

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