GitLab Collapse Markdown

A userscript that collapses markdown headers

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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();

})();