├── .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 | 
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 |
--------------------------------------------------------------------------------
/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 | '';
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 |
--------------------------------------------------------------------------------