GitHub Collapse Markdown

A userscript that collapses markdown headers

目前為 2023-07-01 提交的版本,檢視 最新版本

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

QingJ © 2025

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