// ==UserScript==
// @name GitHub Code Folding
// @version 1.1.5
// @description A userscript that adds code folding to GitHub files
// @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_addStyle
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103
// @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
// @require https://greasyfork.org/scripts/398877-utils-js/code/utilsjs.js?version=1079637
// @icon https://github.githubassets.com/pinned-octocat.svg
// @supportURL https://github.com/Mottie/GitHub-userscripts/issues
// ==/UserScript==
/* global $ $$ on */
/**
* 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 { position:relative; padding-left:10px; }
.ghcf-collapser { position:absolute; left:2px; width:10px; cursor:pointer; }
.ghcf-collapser:after { display: inline-block; vertical-align: middle;
content:"\u25bc"; opacity:.5; transition:.15s; }
.ghcf-collapser:hover:after { opacity:1; }
.ghcf-collapsed.ghcf-collapser:after { transform:rotate(-90deg);
opacity:.8; }
.ghcf-hidden-line { display:none; }
.ghcf-ellipsis { padding:1px 2px; margin-left:2px; cursor:pointer;
background:rgba(255,235,59,.4); position:relative; z-index:1; }
.ghcf-ellipsis:hover { background:rgba(255,235,59,.7); }
`);
const blocks = {};
const ellipsis = document.createElement("span");
const triangle = document.createElement("span");
triangle.className = "ghcf-collapser";
ellipsis.className = "pl-smi ghcf-ellipsis";
ellipsis.innerHTML = "…";
function countInitialWhiteSpace(arr) {
const getWhiteSpaceIndex = i => {
if (arr[i] !== " " && arr[i] !== "\t" && arr[i] !== "\xa0") {
return 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 = el.closest("tr");
if (elm) {
elm = elm.querySelector("[data-line-number]");
return elm ? parseInt(elm.dataset.lineNumber, 10) : "";
}
return "";
}
function getCodeLines(codeBlock) {
return $$(".blob-code-inner", codeBlock);
}
function toggleCode({ action, codeBlock, index, depth }) {
let els, lineNums;
const codeLines = getCodeLines(codeBlock) || [];
const pairs = blocks[codeBlock.dataset.blockIndex];
if (!pairs || codeLines.length === 0) {
return;
}
// depth is a string containing a specific depth number to toggle
if (depth) {
els = $$(`.ghcf-collapser[data-depth="${depth}"]`, codeBlock);
lineNums = els.map(el => {
el.classList.toggle("ghcf-collapsed", action === "hide");
return getLineNumber(el);
});
} else {
lineNums = [index];
}
if (action === "hide") {
lineNums.forEach(start => {
let elm;
let end = pairs.get(start - 1);
codeLines.slice(start, end).forEach(el => {
elm = el.closest("tr");
if (elm) {
elm.classList.add("ghcf-hidden-line");
}
});
if (!$(".ghcf-ellipsis", codeLines[start - 1])) {
elm = $(".ghcf-collapser", codeLines[start - 1]);
elm.parentNode.insertBefore(
ellipsis.cloneNode(true),
null
);
}
});
} else if (action === "show") {
lineNums.forEach(start => {
let end = pairs.get(start - 1);
codeLines.slice(start, end).forEach(el => {
let elm = el.closest("tr");
if (elm) {
elm.classList.remove("ghcf-hidden-line");
removeEls(".ghcf-ellipsis", elm);
}
elm = $(".ghcf-collapsed", elm);
if (elm) {
elm.classList.remove("ghcf-collapsed");
}
});
removeEls(".ghcf-ellipsis", codeLines[start - 1]);
});
}
// shift ends up selecting text on the page, so clear it
if (lineNums.length > 1) {
removeSelection();
}
}
function addBindings() {
on(document, "click", event => {
let index, elm, isCollapsed;
const el = event.target;
const codeBlock = el.closest(".highlight");
// click on collapser
if (el && el.classList.contains("ghcf-collapser")) {
isCollapsed = el.classList.contains("ghcf-collapsed");
index = getLineNumber(el);
// Shift + click to toggle them all
if (index && event.getModifierState("Shift")) {
return toggleCode({
action: isCollapsed ? "show" : "hide",
codeBlock,
index,
depth: el.dataset.depth
});
}
if (index) {
if (isCollapsed) {
el.classList.remove("ghcf-collapsed");
toggleCode({ action: "show", codeBlock, index });
} else {
el.classList.add("ghcf-collapsed");
toggleCode({ action: "hide", codeBlock, index });
}
}
return;
}
// click on ellipsis
if (el && el.classList.contains("ghcf-ellipsis")) {
elm = $(".ghcf-collapsed", el.parentNode);
if (elm) {
elm.classList.remove("ghcf-collapsed");
}
index = getLineNumber(el);
if (index) {
toggleCode({ action: "show", codeBlock, index });
}
}
});
}
function addCodeFolding() {
// Keep .file in case someone needs this userscript for GitHub Enterprise
if ($(".file table.highlight, .blob-wrapper table.highlight")) {
$$("table.highlight").forEach((codeBlock, blockIndex) => {
if (codeBlock && codeBlock.classList.contains("ghcf-processed")) {
// Already processed
return;
}
const codeLines = getCodeLines(codeBlock);
removeEls("span.ghcf-collapser", codeBlock);
if (codeLines) {
// In case this script has already been run and modified the DOM on a
// previous page in github, make sure to reset it.
codeBlock.classList.add("ghcf-processed");
codeBlock.dataset.blockIndex = blockIndex;
const spaceMap = new Map();
const stack = [];
const pairs = blocks[blockIndex] = new Map();
codeLines.forEach((el, lineNum) => {
let prevSpaces;
let line = el.textContent;
let count = line.trim().length
? countInitialWhiteSpace(line.split(""))
: -1;
spaceMap.set(lineNum, count);
function tryPair() {
let el;
let top = stack[stack.length - 1];
if (count !== -1 && count <= spaceMap.get(top)) {
pairs.set(top, lineNum);
// prepend triangle
el = triangle.cloneNode();
el.dataset.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);
}
});
}
});
}
}
function removeEls(selector, el) {
let els = $$(selector, el);
let index = els.length;
while (index--) {
els[index].parentNode.removeChild(els[index]);
}
}
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();
}
}
}
on(document, "ghmo:container", addCodeFolding);
addCodeFolding();
addBindings();
})();