├── .prettierrc.yml ├── LICENSE ├── README.md ├── background.js ├── images ├── demo.gif ├── icon-128.png ├── screenshot-1.png └── triangle.svg ├── main.js ├── manifest.json └── style.css /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | tabWidth: 4 3 | singleQuote: true 4 | tralingComma: 'all' 5 | semi: true 6 | endOfLine: 'auto' 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Noam Lustiger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Code Folding 2 | ### Chrome and Firefox extension that enables code folding in GitHub 3 | Install via the [Chrome web store](https://chrome.google.com/webstore/detail/github-code-folding/lefcpjbffalgdcdgidjdnmabfenecjdf/) or [Mozilla AMO](https://addons.mozilla.org/en-US/firefox/addon/github-code-folding/) 4 | 5 | ![demo](/images/demo.gif) 6 | 7 | Code folding - the ability to selectively hide and display sections of a code - is an invaluable feature in many text editors and IDEs. 8 | Now, developers can utilize that same style code-folding while poring over source code on the web in GitHub. Works for any type of indentation- spaces or tabs. 9 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | chrome.tabs.onUpdated.addListener((tabId, changeinfo) => { 2 | if (changeinfo.status === 'complete') { 3 | chrome.scripting.executeScript({ 4 | target: { tabId }, 5 | files: ['main.js'], 6 | }); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noam3127/github-code-folding/12a188d8ff4fbcd631fd8c4e554169d384c106b3/images/demo.gif -------------------------------------------------------------------------------- /images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noam3127/github-code-folding/12a188d8ff4fbcd631fd8c4e554169d384c106b3/images/icon-128.png -------------------------------------------------------------------------------- /images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noam3127/github-code-folding/12a188d8ff4fbcd631fd8c4e554169d384c106b3/images/screenshot-1.png -------------------------------------------------------------------------------- /images/triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 3 | 4 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const classes = { 5 | hidden: 'gcf-hidden-line', 6 | sideways: 'gcf-sideways', 7 | collapser: 'gcf-collapser', 8 | ellipsis: 'gcf-ellipsis', 9 | blockStart: 'gcf-block-start', 10 | previouslyCollapsed: 'gcf-nested-hidden', 11 | }; 12 | 13 | // Clear old classes and attributes from previous page loads 14 | document.querySelectorAll('.' + classes.collapser).forEach((arrow) => { 15 | arrow.parentElement.removeChild(arrow); 16 | }); 17 | 18 | document.querySelectorAll('.' + classes.ellipsis).forEach((el) => { 19 | el.parentElement.removeChild(el); 20 | }); 21 | 22 | document.querySelectorAll('.' + classes.hidden).forEach((el) => { 23 | el.classList.remove(classes.hidden); 24 | }); 25 | 26 | document.querySelectorAll(`[${classes.previouslyCollapsed}]`).forEach((el) => { 27 | el.removeAttribute(classes.previouslyCollapsed); 28 | }); 29 | 30 | const codeLines = [...document.querySelectorAll('table.js-file-line-container tr .blob-code-inner')]; 31 | const codeLinesText = codeLines.map((l) => l.textContent); 32 | 33 | const _arrow = 34 | '' + 36 | ' Svg Vector Icons : http://www.onlinewebfonts.com/icon ' + 37 | '' + 38 | ''; 39 | 40 | class Element { 41 | constructor(name) { 42 | this.element = document.createElement(name); 43 | } 44 | addClass(className) { 45 | this.element.classList.add(className); 46 | return this; 47 | } 48 | setId(id) { 49 | this.element.id = id; 50 | return this; 51 | } 52 | setHTML(str) { 53 | this.element.innerHTML = str; 54 | return this; 55 | } 56 | } 57 | 58 | const arrowFactory = (id) => { 59 | return new Element('span').addClass(classes.collapser).setId(id).setHTML(_arrow).element; 60 | }; 61 | 62 | const ellipsisFactory = (id) => { 63 | return new Element('span').addClass(classes.ellipsis).setId(id).setHTML('...').element; 64 | }; 65 | 66 | const spaceMap = new Map(); 67 | const pairs = new Map(); 68 | const stack = []; 69 | const blockStarts = []; 70 | const countLeadingWhitespace = (arr) => { 71 | const getWhitespaceIndex = (i) => { 72 | if (arr[i] !== ' ' && arr[i] !== '\t') { 73 | return i; 74 | } 75 | i++; 76 | return getWhitespaceIndex(i); 77 | }; 78 | return getWhitespaceIndex(0); 79 | }; 80 | 81 | const last = (arr) => arr[arr.length - 1]; 82 | const getPreviousSpaces = (map, lineNum) => { 83 | let prev = map.get(lineNum - 1); 84 | return prev === -1 ? getPreviousSpaces(map, lineNum - 1) : { lineNum: lineNum - 1, count: prev }; 85 | }; 86 | 87 | for (let lineNum = 0; lineNum < codeLinesText.length; lineNum++) { 88 | let line = codeLinesText[lineNum]; 89 | let count = line.trim().length ? countLeadingWhitespace(line.split('')) : -1; 90 | spaceMap.set(lineNum, count); 91 | 92 | function tryPair() { 93 | let top = last(stack); 94 | if (count !== -1 && count <= spaceMap.get(top)) { 95 | pairs.set(top, lineNum); 96 | // codeLines[top].setAttribute(classes.blockStart, true); 97 | const arrow = arrowFactory(`gcf-${top + 1}`); 98 | codeLines[top].prepend(arrow); 99 | blockStarts.push(codeLines[top]); 100 | stack.pop(); 101 | return tryPair(); 102 | } 103 | } 104 | tryPair(); 105 | 106 | let prevSpaces = getPreviousSpaces(spaceMap, lineNum); 107 | if (count > prevSpaces.count) { 108 | stack.push(prevSpaces.lineNum); 109 | } 110 | } 111 | 112 | const toggleCode = (action, start, end) => { 113 | if (action === 'hide') { 114 | const sliced = codeLines.slice(start, end); 115 | sliced.forEach((elem) => { 116 | const tr = elem.parentElement; 117 | 118 | // If a line was already hidden, there was an inner block that 119 | // was previously collapsed. Setting this attribute will 120 | // protect the inner block from being expanded 121 | // when this current outer block is expanded 122 | if (tr.classList.contains(classes.hidden)) { 123 | const prev = parseInt(tr.getAttribute(classes.previouslyCollapsed)); 124 | const count = prev ? prev + 1 : 1; 125 | tr.setAttribute(classes.previouslyCollapsed, count); 126 | } 127 | tr.classList.add(classes.hidden); 128 | }); 129 | codeLines[start - 1].appendChild(ellipsisFactory(`ellipsis-${start - 1}`)); 130 | } else if (action === 'show') { 131 | const sliced = codeLines.slice(start, end); 132 | const topLine = codeLines[start - 1]; 133 | 134 | sliced.forEach((elem) => { 135 | const tr = elem.parentElement; 136 | const nestedCount = parseInt(tr.getAttribute(classes.previouslyCollapsed)); 137 | if (!nestedCount) { 138 | tr.classList.remove(classes.hidden); 139 | } else if (nestedCount === 1) { 140 | tr.removeAttribute(classes.previouslyCollapsed); 141 | } else { 142 | tr.setAttribute(classes.previouslyCollapsed, nestedCount - 1); 143 | } 144 | }); 145 | topLine.removeChild(topLine.lastChild); 146 | } 147 | }; 148 | 149 | const arrows = document.querySelectorAll('.' + classes.collapser); 150 | function arrowListener(e) { 151 | e.preventDefault(); 152 | let svg = e.currentTarget; 153 | let td = e.currentTarget.parentElement; 154 | let id = td.getAttribute('id'); 155 | let index = parseInt(id.slice(2)) - 1; 156 | if (svg.classList.contains(classes.sideways)) { 157 | svg.classList.remove(classes.sideways); 158 | toggleCode('show', index + 1, pairs.get(index)); 159 | } else { 160 | svg.classList.add(classes.sideways); 161 | toggleCode('hide', index + 1, pairs.get(index)); 162 | } 163 | } 164 | 165 | arrows.forEach((c) => { 166 | c.addEventListener('click', arrowListener); 167 | }); 168 | 169 | function ellipsisListener(e) { 170 | if (!e.target.parentElement) return; 171 | if (e.target.classList.contains(classes.ellipsis)) { 172 | let td = e.target.parentElement; 173 | let svg = td.querySelector('.' + classes.sideways); 174 | let id = e.target.parentElement.getAttribute('id'); 175 | let index = parseInt(id.slice(2)) - 1; 176 | svg.classList.remove(classes.sideways); 177 | toggleCode('show', index + 1, pairs.get(index)); 178 | } 179 | } 180 | 181 | blockStarts.forEach((line) => { 182 | line.addEventListener('click', ellipsisListener); 183 | }); 184 | 185 | })(); 186 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitHub Code Folding", 3 | "version": "0.3.3", 4 | "description": "Enable code folding when viewing files in GitHub.", 5 | "homepage_url": "https://github.com/noam3127/github-code-folding", 6 | "manifest_version": 3, 7 | "minimum_chrome_version": "88", 8 | "author": "Noam Lustiger", 9 | "short_name": "Github Code Folding", 10 | "permissions": [ 11 | "scripting" 12 | ], 13 | "host_permissions": [ 14 | "*://github.com/*" 15 | ], 16 | "background": { 17 | "service_worker": "background.js", 18 | "type": "module" 19 | }, 20 | "icons": { 21 | "128": "images/icon-128.png" 22 | }, 23 | "content_scripts": [{ 24 | "run_at" : "document_end", 25 | "matches": [ 26 | "*://github.com/*" 27 | ], 28 | "css": [ 29 | "style.css" 30 | ] 31 | }] 32 | } 33 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | td.blob-code.blob-code-inner { 2 | padding-left: 13px; 3 | } 4 | .gcf-collapser { 5 | margin-left: -16px; 6 | margin-right: 2px; 7 | padding-right: 6px; 8 | opacity: 0.5; 9 | } 10 | 11 | .gcf-collapser:hover { 12 | opacity: 1; 13 | } 14 | 15 | .gcf-collapser > svg { 16 | transition: 0.15s ease-in-out; 17 | } 18 | 19 | .gcf-sideways > svg { 20 | transform: rotate(-90deg); 21 | transform-origin: center; 22 | opacity: 0.8; 23 | } 24 | 25 | .gcf-hidden-line { 26 | display: none; 27 | } 28 | 29 | .gcf-ellipsis { 30 | padding: 1px 2px; 31 | margin-left: 2px; 32 | cursor: pointer; 33 | background: rgba(255, 235, 59, 0.4); 34 | } 35 | 36 | .gcf-ellipsis:hover { 37 | background: rgba(255, 235, 59, 0.7); 38 | } 39 | --------------------------------------------------------------------------------