GitLab Collapse Markdown

A userscript that collapses markdown headers

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        GitLab Collapse Markdown
// @version     0.1.0
// @description A userscript that collapses markdown headers
// @license     MIT
// @author      Rob Garrison
// @namespace   https://gitlab.com/Mottie
// @include     https://gitlab.com/*
// @include     https://about.gitlab.com/*
// @run-at      document-idle
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @icon        https://gitlab.com/assets/gitlab_logo-7ae504fe4f68fdebb3c2034e36621930cd36ea87924c11ff65dbcb8ed50dca58.png
// ==/UserScript==
(() => {
	"use strict";

	const defaultColors = [
			// palette generated by http://tools.medialab.sciences-po.fr/iwanthue/
			// (colorblind friendly, soft)
			"#6778d0", "#ac9c3d", "#b94a73", "#56ae6c", "#9750a1", "#ba543d"
		],
		blocks = [
			".wiki", // gitlab.com/:user/:repo/wikis
			".note-text.md", // issue comments
			".md-preview", // issue preview
			".documentation-index", // gitlab.com/help/
			".md-page", // about.gitlab.com
			"" // leave empty string at the end
		],

		headers = "H1 H2 H3 H4 H5 H6".split(" "),
		// toggled class name
		collapsed = "glcm-collapsed",
		arrowColors = document.createElement("style");

	let startCollapsed = GM_getValue("glcm-collapsed", false),
		colors = GM_getValue("glcm-colors", defaultColors);

	GM_addStyle(`
		${blocks.join(" h1,")} ${blocks.join(" h2,")}
		${blocks.join(" h3,")} ${blocks.join(" h4,")}
		${blocks.join(" h5,")} ${blocks.join(" h6,").slice(0, -1)} {
			position:relative;
			padding-right:.8em;
			cursor:pointer;
			width:calc(100% - 5px);
		}
		${blocks.join(" h1:after,")} ${blocks.join(" h2:after,")}
		${blocks.join(" h3:after,")} ${blocks.join(" h4:after,")}
		${blocks.join(" h5:after,")} ${blocks.join(" h6:after,").slice(0, -1)} {
			display:inline-block;
			position:absolute;
			right:0;
			top:calc(50% - .5em);
			font-size:.8em;
			content:"\u25bc";
		}
		${blocks.join(" ." + collapsed + ":after,").slice(0, -1)} {
			transform:rotate(90deg);
		}
		.glcm-hidden {
			display:none !important;
		}
	`);

	function addColors() {
		arrowColors.textContent = `
			${blocks.join(" h1:after,").slice(0, -1)} { color:${colors[0]} }
			${blocks.join(" h2:after,").slice(0, -1)} { color:${colors[1]} }
			${blocks.join(" h3:after,").slice(0, -1)} { color:${colors[2]} }
			${blocks.join(" h4:after,").slice(0, -1)} { color:${colors[3]} }
			${blocks.join(" h5:after,").slice(0, -1)} { color:${colors[4]} }
			${blocks.join(" h6:after,").slice(0, -1)} { color:${colors[5]} }
		`;
	}

	function toggle(el, shifted) {
		if (el) {
			el.classList.toggle(collapsed);
			let els;
			const name = el.nodeName || "",
				level = parseInt(name.replace(/[^\d]/, ""), 10),
				isCollapsed = el.classList.contains(collapsed);
			if (shifted) {
				// collapse all same level anchors
				els = $$(`${blocks.join(" " + name + ",").slice(0, -1)}`);
				for (el of els) {
					nextHeader(el, level, isCollapsed);
				}
			} else {
				nextHeader(el, level, isCollapsed);
			}
			removeSelection();
		}
	}

	function nextHeader(el, level, isCollapsed) {
		el.classList.toggle(collapsed, isCollapsed);
		const selector = headers.slice(0, level).join(","),
			name = [collapsed, "glcm-hidden"],
			els = [];
		el = el.nextElementSibling;
		while (el && !el.matches(selector)) {
			els[els.length] = el;
			el = el.nextElementSibling;
		}
		if (els.length) {
			if (isCollapsed) {
				els.forEach(el => {
					el.classList.add("glcm-hidden");
				});
			} else {
				els.forEach(el => {
					el.classList.remove(...name);
				});
			}
		}
	}

	// show siblings of hash target
	function siblings(target) {
		let el = target.nextElementSibling,
			els = [target];
		const level = parseInt((target.nodeName || "").replace(/[^\d]/, ""), 10),
			selector = headers.slice(0, level - 1).join(",");
		while (el && !el.matches(selector)) {
			els[els.length] = el;
			el = el.nextElementSibling;
		}
		el = target.previousElementSibling;
		while (el && !el.matches(selector)) {
			els[els.length] = el;
			el = el.previousElementSibling;
		}
		if (els.length) {
			els = els.filter(el => {
				return el.nodeName === target.nodeName;
			});
			els.forEach(el => {
				el.classList.remove("glcm-hidden");
			});
		}
		nextHeader(target, level, false);
	}

	function removeSelection() {
		// remove text selection - https://stackoverflow.com/a/3171348/145346
		const sel = window.getSelection ?
			window.getSelection() :
			document.selection;
		if (sel) {
			if (sel.removeAllRanges) {
				sel.removeAllRanges();
			} else if (sel.empty) {
				sel.empty();
			}
		}
	}

	function addBinding() {
		document.addEventListener("click", event => {
			let target = event.target;
			const name = (target && (target.nodeName || "")).toLowerCase();
			if (name === "path") {
				target = closest("svg", target);
			}
			// check if element is inside a header
			target = closest(headers.join(","), event.target);
			if (target && headers.indexOf(target.nodeName || "") > -1) {
				// make sure the header is inside of markdown
				if (closest(`${blocks.join(",").slice(0, -1)}`, target)) {
					toggle(target, event.shiftKey);
				}
			}
		});
		window.addEventListener("hashchange", () => {
			if (startCollapsed) {
				checkHash();
			}
		});
	}

	function checkHash() {
		let el, els, md;
		// don't collapse H1 blocks
		const mds = $$(`${blocks.slice(1).join(",").slice(0, -1)}`),
			tmp = (window.location.hash || "").replace(/#/, "");
		for (md of mds) {
			els = $$(headers.slice(1).join(","), md);
			if (els.length > 1) {
				for (el of els) {
					if (el && !el.classList.contains(collapsed)) {
						toggle(el, true);
					}
				}
			}
		}
		// open up
		if (tmp) {
			els = $(`#${tmp}`);
			if (els && els.classList.contains("anchor")) {
				el = els.parentNode;
				if (el.matches(headers.join(","))) {
					siblings(el);
					document.documentElement.scrollTop = el.offsetTop;
					// set scrollTop a second time, in case of browser lag
					setTimeout(() => {
						document.documentElement.scrollTop = el.offsetTop;
					}, 500);
				}
			}
		}
	}

	function checkColors() {
		if (!colors || colors.length !== 6) {
			colors = [].concat(defaultColors);
		}
	}

	function init() {
		document.querySelector("head").appendChild(arrowColors);
		checkColors();
		addColors();
		addBinding();
		if (startCollapsed) {
			checkHash();
		}
	}

	function $(selector, el) {
		return (el || document).querySelector(selector);
	}

	function $$(selectors, el) {
		return Array.from((el || document).querySelectorAll(selectors));
	}

	function closest(selector, el) {
		while (el && el.nodeType === 1) {
			if (el.matches(selector)) {
				return el;
			}
			el = el.parentNode;
		}
		return null;
	}

	// Add GM options
	GM_registerMenuCommand("Set GitLab collapse markdown state", () => {
		const val = prompt(
			"Set initial state to (c)ollapsed or (e)xpanded (first letter only):",
			startCollapsed ? "collapsed" : "expanded"
		);
		if (val !== null) {
			startCollapsed = /^c/i.test(val);
			GM_setValue("glcm-collapsed", startCollapsed);
			console.log(
				`GitLab Collapse Markdown: Headers will` +
				`${startCollapsed ? "be" : "not be"} initially collapsed`
			);
		}
	});

	GM_registerMenuCommand("Set GitLab collapse markdown colors", () => {
		let val = prompt("Set header arrow colors:", JSON.stringify(colors));
		if (val !== null) {
			// allow pasting in a JSON format
			try {
				val = JSON.parse(val);
				if (val && val.length === 6) {
					colors = val;
					GM_setValue("glcm-colors", colors);
					console.log("GitLab Collapse Markdown: colors set to", colors);
					addColors();
					return;
				}
				console.error(
					"GitLab Collapse Markdown: invalid color definition (6 colors)",
					val
				);
				// reset colors to default (in case colors variable is corrupted)
				checkColors();
			} catch (err) {
				console.error("GitLab Collapse Markdown: invalid JSON");
			}
		}
	});

	init();

})();