GitHub Collapse In Comment

A userscript that adds a header that can toggle long code and quote blocks in comments

// ==UserScript==
// @name        GitHub Collapse In Comment
// @version     1.0.23
// @description A userscript that adds a header that can toggle long code and quote blocks in comments
// @license     MIT
// @author      Rob Garrison
// @namespace   https://github.com/Mottie
// @match       https://github.com/*
// @match       https://gist.github.com/*
// @run-at      document-idle
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @require     https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
// @icon        https://github.githubassets.com/pinned-octocat.svg
// @supportURL  https://github.com/Mottie/GitHub-userscripts/issues
// ==/UserScript==

(() => {
	"use strict";
	/*
	Idea from: https://github.com/dear-github/dear-github/issues/166 &
		https://github.com/isaacs/github/issues/208
	examples:
		https://github.com/Mottie/tablesorter/issues/569
		https://github.com/jquery/jquery/issues/3195
	*/
	// hide code/quotes longer than this number of lines
	let minLines = GM_getValue("gcic-max-lines", 10),
		startCollapsed = GM_getValue("gcic-start-collapsed", true);
	// extract syntax type from class name
	const regex = /highlight(?:-[^\s]+)+/;

	// syntax highlight class name lookup table
	const syntaxClass = {
		basic: "HTML",
		cs: "C#",
		fsharp: "F#",
		gfm: "Markdown",
		jq: "JSONiq",
		shell: "Bash (shell)",
		tcl: "Glyph",
		tex: "LaTex"
	};

	GM_addStyle(`
		.gcic-block {
			border:#eee 1px solid;
			padding:2px 8px 2px 10px;
			border-radius:5px 5px 0 0;
			position:relative;
			top:1px;
			cursor:pointer;
			font-weight:bold;
			display:block;
		}
		.gcic-block + .highlight {
			border-top:none;
		}
		.gcic-block + .email-signature-reply {
			margin-top:0;
		}
		.gcic-block:after {
			content:"\u25bc ";
			float:right;
		}
		.gcic-block-closed {
			border-radius:5px;
			margin-bottom:10px;
		}
		.gcic-block-closed:after {
			transform: rotate(90deg);
		}
		.gcic-block-closed + .highlight, .gcic-block-closed + .email-signature-reply,
		.gcic-block-closed + pre {
			display:none;
		}
	`);

	function makeToggle(name, lines) {
		/* full list of class names from (look at "tm_scope" value)
		https://github.com/github/linguist/blob/master/lib/linguist/languages.yml
		here are some example syntax highlighted class names:
			highlight-text-html-markdown-source-gfm-apib
			highlight-text-html-basic
			highlight-source-fortran-modern
			highlight-text-tex
		*/
		let n = (name || "").match(regex);
		if (n && n[0]) {
			n = n[0].replace(
				/(highlight[-\s]|(source-)|(text-)|(html-)|(markdown-)|(-modern))/g, ""
			);
			n = (syntaxClass[n] || n).toUpperCase().trim();
		}
		return `${n || "Block"} (${lines} lines)`;
	}

	function addToggles() {
		// issue comments
		if ($("#discussion_bucket")) {
			let indx = 0;
			const block = document.createElement("a"),
				els = $$(".markdown-body pre, .email-signature-reply"),
				len = els.length;

			// "flash" = blue box styling
			block.className = `gcic-block border flash${
				startCollapsed ? " gcic-block-closed" : ""
			}`;
			block.href = "#";

			// loop with delay to allow user interaction
			const loop = () => {
				let el, wrap, node, syntaxClass, numberOfLines,
					// max number of DOM insertions per loop
					max = 0;
				while (max < 20 && indx < len) {
					if (indx >= len) {
						return;
					}
					el = els[indx];
					if (el && !el.classList.contains("gcic-has-toggle")) {
						numberOfLines = el.innerHTML.split("\n").length;
						if (numberOfLines > minLines) {
							syntaxClass = "";
							wrap = closest(".highlight", el);
							if (wrap && wrap.classList.contains("highlight")) {
								syntaxClass = wrap.className;
							} else {
								// no syntax highlighter defined (not wrapped)
								wrap = el;
							}
							node = block.cloneNode();
							node.innerHTML = makeToggle(syntaxClass, numberOfLines);
							wrap.parentNode.insertBefore(node, wrap);
							el.classList.add("gcic-has-toggle");
							if (startCollapsed) {
								el.display = "none";
							}
							max++;
						}
					}
					indx++;
				}
				if (indx < len) {
					setTimeout(() => {
						loop();
					}, 200);
				}
			};
			loop();
		}
	}

	function addBindings() {
		document.addEventListener("click", event => {
			let els, indx, flag;
			const el = event.target;
			if (el && el.classList.contains("gcic-block")) {
				event.preventDefault();
				// shift + click = toggle all blocks in a single comment
				// shift + ctrl + click = toggle all blocks on page
				if (event.shiftKey) {
					els = $$(
						".gcic-block",
						event.ctrlKey || event.metaKey ? "" : closest(".markdown-body", el)
					);
					indx = els.length;
					flag = el.classList.contains("gcic-block-closed");
					while (indx--) {
						els[indx].classList.toggle("gcic-block-closed", !flag);
					}
				} else {
					el.classList.toggle("gcic-block-closed");
				}
				removeSelection();
			}
		});
	}

	function update() {
		let toggles = $$(".gcic-block"),
			indx = toggles.length;
		while (indx--) {
			toggles[indx].parentNode.removeChild(toggles[indx]);
		}
		toggles = $$(".gcic-has-toggle");
		indx = toggles.length;
		while (indx--) {
			toggles[indx].classList.remove("gcic-has-toggle");
		}
		addToggles();
	}

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

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

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

	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();
			}
		}
	}

	GM_registerMenuCommand("Set GitHub Collapse In Comment Max Lines", () => {
		let val = prompt("Minimum number of lines before adding a toggle:",
			minLines);
		val = parseInt(val, 10);
		if (val) {
			minLines = val;
			GM_setValue("gcic-max-lines", val);
			update();
		}
	});

	GM_registerMenuCommand("Set GitHub Collapse In Comment Initial State", () => {
		let val = prompt(
			"Start with blocks (c)ollapsed or (e)xpanded (first letter necessary):",
			startCollapsed ? "collapsed" : "expanded"
		);
		if (val) {
			val = /^c/.test(val || "");
			startCollapsed = val;
			GM_setValue("gcic-start-collapsed", val);
			update();
		}
	});

	document.addEventListener("ghmo:container", addToggles);
	document.addEventListener("ghmo:preview", addToggles);
	addBindings();
	addToggles();

})();