GitHub Code Folding

A userscript that adds code folding to GitHub files

Verzia zo dňa 29.12.2016. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name        GitHub Code Folding
// @version     0.1.0
// @description A userscript that adds code folding to GitHub files
// @license     https://opensource.org/licenses/MIT
// @namespace   http://github.com/Mottie
// @include     https://github.com/*
// @run-at      document-idle
// @grant       GM_addStyle
// @author      Rob Garrison
// ==/UserScript==
/* jshint esnext:true, unused:true */
/**
 * This userscript has been heavily modified from the "github-code-folding"
 * Chrome extension Copyright 2016 by Noam Lustiger; under an MIT license
 * https://github.com/noam3127/github-code-folding
 */
(() => {
	"use strict";

	GM_addStyle(`
		td.blob-code.blob-code-inner { padding-left:13px; }
		.collapser { position:absolute; left:2px; width:22px; opacity:.5;
			transition:.15s; cursor:pointer; }
		.collapser:after { content:"\u25bc"; }
		.collapser:hover { opacity:1; }
		.sideways { transform:rotate(-90deg); transform-origin:16% 49%; opacity:.8; }
		.hidden-line { display:none; }
		.ellipsis { padding:1px 2px; margin-left:2px; cursor:pointer;
			background:rgba(255,235,59,.4); }
		.ellipsis:hover { background:rgba(255,235,59,.7); }
	`);

	let busy = false;
	const pairs = new Map(),
		ellipsis = document.createElement("span"),
		triangle = document.createElement("span");

	triangle.className = "collapser";
	ellipsis.className = "pl-smi ellipsis";
	ellipsis.innerHTML = "…";

	function countInitialWhiteSpace(arr) {
		const getWhiteSpaceIndex = i => {
			if (arr[i] !== " " && arr[i] !== "\t") {
				return i;
			}
			i++;
			return getWhiteSpaceIndex(i);
		};
		return getWhiteSpaceIndex(0);
	}

	function getPreviousSpaces(map, lineNum) {
		let prev = map.get(lineNum - 1);
		return prev === -1 ?
			getPreviousSpaces(map, lineNum - 1) :
			{
				lineNum: lineNum - 1,
				count: prev
			};
	}

	function getLineNumber(el) {
		let elm = closest("td", el),
			index = elm ? elm.id : "";
		if (index) {
			return parseInt(index.slice(2), 10);
		}
		return "";
	}

	function toggleCode(action, index, depth) {
		busy = true;
		let els, lineNums;
		const codeLines = $$(".file table.highlight .blob-code-inner");
		// depth is a string containing a specific depth number to toggle
		if (depth) {
			els = $$(`.collapser[data-depth="${depth}"]`);
			lineNums = els.map(el => {
				el.classList[action === "hide" ? "add" : "remove"]("sideways");
				return getLineNumber(el);
			});
		} else {
			lineNums = [index];
		}

		if (action === "hide") {
			lineNums.forEach(start => {
				let end = pairs.get(start - 1);
				codeLines.slice(start, end).forEach(el => {
					let elm = closest("tr", el);
					if (elm) {
						elm.classList.add("hidden-line");
					}
				});
				if (!$(".ellipsis", codeLines[start - 1])) {
					codeLines[start - 1].appendChild(ellipsis.cloneNode(true));
				}
			});
		} else if (action === "show") {
			lineNums.forEach(start => {
				let end = pairs.get(start - 1);
				codeLines.slice(start, end).forEach(el => {
					let elm = closest("tr", el);
					if (elm) {
						elm.classList.remove("hidden-line");
						remove(".ellipsis", elm);
					}
					elm = $(".sideways", elm);
					if (elm) {
						elm.classList.remove("sideways");
					}
				});
				remove(".ellipsis", codeLines[start - 1]);
			});
		}
		// shift ends up selecting text on the page, so clear it
		if (lineNums.length > 1) {
			removeSelection();
		}
		busy = false;
	}

	function addBindings() {
		document.addEventListener("click", event => {
			let index, elm, isCollapsed;
			const el = event.target;

			// click on collapser
			if (el && el.classList.contains("collapser")) {
				isCollapsed = el.classList.contains("sideways");
				index = getLineNumber(el);
				// Shift + click to toggle them all
				if (index && event.getModifierState("Shift")) {
					return toggleCode(
						isCollapsed ? "show" : "hide",
						index,
						el.getAttribute("data-depth")
					);
				}
				if (index) {
					if (isCollapsed) {
						el.classList.remove("sideways");
						toggleCode("show", index);
					} else {
						el.classList.add("sideways");
						toggleCode("hide", index);
					}
				}
				return;
			}

			// click on ellipsis
			if (el && el.classList.contains("ellipsis")) {
				elm = $(".sideways", el.parentNode);
				if (elm) {
					elm.classList.remove("sideways");
				}
				index = getLineNumber(el);
				if (index) {
					toggleCode("show", index);
				}
			}
		});
	}

	function addCodeFolding() {
		if ($(".file table.highlight")) {
			busy = true;
			// In case this script has already been run and modified the DOM on a
			// previous page in github, make sure to reset it.
			remove("span.collapser");
			pairs.clear();

			const codeLines = $$(".file table.highlight .blob-code-inner"),
				spaceMap = new Map(),
				stack = [];

			codeLines.forEach((el, lineNum) => {
				let prevSpaces,
					line = el.textContent,
					count = line.trim().length ?
						countInitialWhiteSpace(line.split("")) :
						-1;
				spaceMap.set(lineNum, count);

				function tryPair() {
					let el,
						top = stack[stack.length - 1];
					if (count !== -1 && count <= spaceMap.get(top)) {
						pairs.set(top, lineNum);
						// prepend triangle
						el = triangle.cloneNode();
						el.setAttribute("data-depth", count + 1);
						codeLines[top].insertBefore(el, codeLines[top].childNodes[0]);
						stack.pop();
						return tryPair();
					}
				}
				tryPair();

				prevSpaces = getPreviousSpaces(spaceMap, lineNum);
				if (count > prevSpaces.count) {
					stack.push(prevSpaces.lineNum);
				}
			});
			busy = false;
		}
	}

	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.nodeName !== "BODY" && !el.matches(selector)) {
			el = el.parentNode;
		}
		return el && el.matches(selector) ? el : null;
	}
	function remove(selector, el) {
		let els = $$(selector, el),
			index = els.length;
		while (index--) {
			els[index].parentNode.removeChild(els[index]);
		}
	}
	function removeSelection() {
		// remove text selection - http://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();
			}
		}
	}

	// Detect GitHub dynamic ajax page loading
	$$(
		"#js-repo-pjax-container, .context-loader-container, [data-pjax-container]"
	).forEach(target => {
		new MutationObserver(mutations => {
			mutations.forEach(mutation => {
				// preform checks before adding code wrap to minimize function calls
				if (!busy && mutation.target === target) {
					addCodeFolding();
				}
			});
		}).observe(target, {
			childList: true,
			subtree: true
		});
	});

	addCodeFolding();
	addBindings();

})();