├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── building.md
├── dist
├── SafariUpdate.plist
├── chrome.zip
├── firefox.zip
├── github-toc.user.js
└── safari.safariextension
│ ├── Icon.png
│ ├── Info.plist
│ ├── github-toc.js
│ └── style.css
├── img
├── banners
│ ├── chrome-banner.png
│ └── chrome-banner.pxm
├── icons
│ ├── icon128.png
│ ├── icon16.png
│ └── icon48.png
└── screenshots
│ ├── chrome-store1.png
│ ├── chrome-store2.png
│ ├── chrome-store3.png
│ ├── cursor.png
│ ├── firefox-store1.png
│ └── safari1.png
├── package.json
├── src
├── chrome
│ └── manifest.json
├── firefox
│ └── manifest.json
├── github-toc.js
├── html
│ ├── backlink.html
│ ├── entry.html
│ └── toc.html
├── index.js
├── safari
│ ├── Info.plist
│ └── SafariUpdate.plist
├── style.css
├── toc.js
├── userscript
│ ├── header.txt
│ └── index.js
└── util.js
├── test
├── README.md
└── test.markdown
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Changelog
2 |
3 | ### 0.2.5
4 |
5 | - Added a shiny new search bar
6 | - Fixed layout bugs caused by updates to GitHub
7 | - Switched the Firefox version to the new WebExtensions API
8 |
9 | ### 0.2.4
10 |
11 | - Fixed an issue where the table of contents would attach to the sidebar on wikis with custom sidebars
12 | - Fixed backlinks not centered in Safari
13 | - Minor internal changes
14 |
15 | ### 0.2.3
16 |
17 | - Added Safari version
18 | - Added support for editing and creating files on GitHub
19 | - Removed option to disable back to top links
20 | - Changed Firefox version to now require reloading existing pages on install/enable
21 | - Lots of internal changes
22 |
23 | ### 0.2.2
24 |
25 | - Fixed several issues caused by updates to the GitHub website
26 | - Various minor updates
27 |
28 | ### 0.2.1
29 |
30 | - Added Firefox version
31 | - Added userscript version
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Arthur Hammer
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Table of Contents for GitHub
2 |
3 | ---
4 |
5 | **Note**: GitHub finally [added this feature natively to the site](https://github.com/isaacs/github/issues/215#issuecomment-807688648)! As such, this project is not maintained anymore.
6 |
7 | ---
8 |
9 | Browser extension that adds a table of contents to repositories, gists and wikis.
10 |
11 | Available for [Google Chrome][Chrome], [Firefox][Firefox], [Safari][Safari] and as [userscript][Userscript].
12 |
13 | 
14 |
15 | This is a simple browser extension that makes reading long files and pages on GitHub easier. If you regurlarly scroll around readmes and wikis looking for specific information, this is for you. Find what you are looking for, quickly.
16 |
17 | Works almost anywhere on GitHub.
18 |
19 | - Works with files in repos, gists, and wikis
20 | - Supports any [GitHub markup](https://github.com/github/markup#markups)
21 | - Supports editing and creating files and wiki pages directly on GitHub
22 | - It's simple and unobtrusive
23 |
24 | ## Install
25 |
26 | ❤️ **[Chrome (Chrome Web Store)][Chrome]**
27 |
28 | 💚 **[Firefox (Mozilla Add-Ons)][Firefox]**
29 |
30 | 💙 **[Safari][Safari]**
31 |
32 | 💜 **[Userscript][Userscript]**
33 |
34 | Note on Safari: The Safari extension is not (yet) hosted on Apple's Extension Gallery. To install, [download the extension `safari.safariextz` from the `dist` folder][Safari] and open it. Since the extension is not from the Gallery, Safari will ask you to trust it.
35 |
36 | ## Build
37 |
38 | npm run install
39 | npm run build
40 |
41 | See [building](building.md) for more.
42 |
43 | ## Contribute
44 |
45 | Contributions are welcome! 👍😀
46 |
47 | ## Changelog
48 |
49 | See [CHANGELOG](CHANGELOG.md).
50 |
51 | ## License
52 |
53 | [MIT](LICENSE).
54 |
55 |
56 | [Chrome]: https://chrome.google.com/webstore/detail/table-of-contents-for-git/hlkhpeomjgelmljaknhoboeohhgmmgcn
57 | [Firefox]: https://addons.mozilla.org/en-US/firefox/addon/github-toc/
58 | [Userscript]: https://github.com/arthurhammer/github-toc/raw/master/dist/github-toc.user.js
59 | [Safari]: https://github.com/arthurhammer/github-toc/releases/download/v0.2.3/safari.safariextz
60 |
--------------------------------------------------------------------------------
/building.md:
--------------------------------------------------------------------------------
1 | ## Building
2 |
3 | You need [`node`](https://nodejs.org/)/[`npm`](https://www.npmjs.com/).
4 |
5 | # Clone or download zip file
6 | git clone git@github.com:arthurhammer/github-toc.git
7 | cd github-toc
8 |
9 | # Install development dependencies
10 | npm install
11 |
12 | # Build unpackaged extensions for testing and running locally
13 | npm run build
14 |
15 | # Build extensions packaged for distribution
16 | npm run dist
17 |
18 | Packaged and unpackaged builds live in the [`dist`](dist/) folder.
19 |
20 | ### Testing in the Browser
21 |
22 | Build the unpackaged extensions with `npm run build`. Then, install the extensions in the browsers as described below. Test it on the [cases described in the `test` folder](test/Readme.md).
23 |
24 | #### Google Chrome
25 |
26 | **Manually**:
27 |
28 | - Open the extensions page in Chrome
29 | - Choose `dist/chrome` under “Load unpacked extension...”
30 |
31 | **Command line**:
32 |
33 | - `npm run chrome` opens a new Chrome instance with the extension installed.
34 |
35 | Chrome has to be closed for this to work. The path to Chrome is hard-coded, change if needed.
36 |
37 | #### Firefox
38 |
39 | **Manually**:
40 |
41 | - Open `about:debugging` in Firefox
42 | - Choose `dist/firefox/manifest.json` under "Load Temporary Add-on"
43 |
44 | **Command line**:
45 |
46 | - `npm run firefox` opens a new Firefox instance with the extension installed.
47 |
48 | See the documentation for Mozilla's [`web-ext`][web-ext] tool.
49 |
50 | [web-ext]: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext
51 |
52 | #### Safari
53 |
54 | - Open Extension Builder in Safari
55 | - Add `dist/safari.safariextension` as existing extension
56 | - Click “Install”
57 |
58 | Note: Unless you have a valid Safari Extension certificate, the extension will automatically be removed whenever you quit Safari. You will also not be able to build the packaged extension for direct install. [The certificate requires a (paid) Apple Developer Program membership](https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/ExtensionsOverview/ExtensionsOverview.html#//apple_ref/doc/uid/TP40009977-CH15-SW26).
59 |
60 | #### Userscript
61 |
62 | Install `dist/github-toc.user.js` directly in the browser if supported or with your favorite userscript manager (such as [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/) or [Tampermonkey](https://tampermonkey.net)).
63 |
--------------------------------------------------------------------------------
/dist/SafariUpdate.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Extension Updates
6 |
7 |
8 | CFBundleIdentifier
9 | me.ahammer.github-toc
10 | Developer Identifier
11 | PY49CKQ6VF
12 | CFBundleVersion
13 | 0.2.3
14 | CFBundleShortVersionString
15 | 0.2.3
16 | URL
17 | https://github.com/arthurhammer/github-toc/releases/download/v0.2.3/safari.safariextz
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/dist/chrome.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/dist/chrome.zip
--------------------------------------------------------------------------------
/dist/firefox.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/dist/firefox.zip
--------------------------------------------------------------------------------
/dist/github-toc.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Table of Contents for GitHub
3 | // @description Adds a table of contents to repositories, gists and wikis on GitHub
4 | // @version 0.2.5
5 | // @author Arthur Hammer
6 | // @namespace https://github.com/arthurhammer
7 | // @license MIT
8 | // @homepage https://github.com/arthurhammer/github-toc
9 | // @updateURL https://github.com/arthurhammer/github-toc/raw/master/dist/github-toc.user.js
10 | // @downloadURL https://github.com/arthurhammer/github-toc/raw/master/dist/github-toc.user.js
11 | // @supportURL https://github.com/arthurhammer/github-toc/issues
12 | // @icon64 https://github.com/arthurhammer/github-toc/raw/master/img/icons/icon128.png
13 | // @match https://github.com/*/*
14 | // @match https://gist.github.com/*/*
15 | // @run-at document-body
16 | // @grant none
17 | // ==/UserScript==
18 |
19 | (function() {
20 |
21 |
22 | var TableOfContents = (function() {
23 |
24 | var defaults = {
25 | // Where to insert the toc (selector or `Element`, first match)
26 | target: '#toc',
27 | // Where to look for headings (selector or `Element`, first match)
28 | content: 'body',
29 | // Which elements to create toc entries for (selector, not limited to `h1`-`h6`)
30 | headings: 'h1, h2, h3, h4, h5, h6',
31 | // Prefix to add to classes
32 | prefix: 'toc',
33 | // Wrap toc entry link elements with this element
34 | entryTagType: 'li',
35 |
36 | // Anchor id for a toc entry by which to identify the heading.
37 | // By default, an existing id on headings is expected.
38 | anchorId: function(i, heading, prefix) {
39 | return heading.id;
40 | },
41 | // Title for a toc entry
42 | title: function(i, heading, prefix) {
43 | return heading.textContent.trim();
44 | },
45 | // Class to add to a toc entry
46 | entryClass: function(i, heading, prefix) {
47 | var classPrefix = prefix ? (prefix + '-') : '';
48 | return classPrefix + heading.tagName.toLowerCase();
49 | },
50 |
51 | // Creates the actual toc entry element.
52 | // Default: `title`
53 | // By default, entries without an `anchorId` are skipped.
54 | entryElement: function(i, heading, data) {
55 | if (!data.anchorId) return null;
56 |
57 | var entry = document.createElement('a');
58 | entry.textContent = data.title;
59 | entry.href = '#' + data.anchorId;
60 |
61 | if (data.entryTagType) {
62 | var parent = document.createElement(data.entryTagType);
63 | parent.appendChild(entry);
64 | entry = parent;
65 | }
66 |
67 | if (data.entryClass) {
68 | entry.classList.add(data.entryClass);
69 | }
70 |
71 | return entry;
72 | }
73 | };
74 |
75 | function toc(options) {
76 | options = extend({}, TableOfContents.defaults, options);
77 |
78 | var target = getElement(options.target);
79 | var content = getElement(options.content);
80 | if (!target || !content) return null;
81 |
82 | var headings = content.querySelectorAll(options.headings);
83 |
84 | forEach(headings, function(i, h) {
85 | var anchorId = options.anchorId(i, h, options.prefix);
86 |
87 | var element = options.entryElement(i, h, {
88 | prefix: options.prefix,
89 | entryTagType: options.entryTagType,
90 | anchorId: anchorId,
91 | title: options.title(i, h, options.prefix),
92 | entryClass: options.entryClass(i, h, options.prefix)
93 | });
94 |
95 | if (element) {
96 | addAnchor(h, anchorId, options);
97 | target.appendChild(element);
98 | }
99 | });
100 |
101 | return target;
102 | }
103 |
104 | // TODO: Inserting elements can break CSS and other stuff
105 | // TODO: signature?
106 | function addAnchor(heading, anchorId, options) {
107 | if (!heading || !anchorId) return;
108 |
109 | if (anchorId !== heading.id) {
110 | var classPrefix = options.prefix ? (options.prefix + '-') : '';
111 | var anchorClass = classPrefix + 'anchor';
112 | var anchor = heading.querySelector(':scope > .' + anchorClass);
113 | if (!anchor) {
114 | anchor = document.createElement('span');
115 | }
116 | anchor.id = anchorId;
117 | anchor.classList.add(anchorClass);
118 | heading.insertBefore(anchor, heading.firstChild);
119 | }
120 | }
121 |
122 | function getElement(element) {
123 | // For now, only considers first match
124 | return (typeof element === 'string') ?
125 | document.querySelector(element) : element;
126 | }
127 |
128 | // from http://youmightnotneedjquery.com/#extend
129 | function extend(out) {
130 | out = out || {};
131 |
132 | for (var i = 1; i < arguments.length; i++) {
133 | if (!arguments[i]) continue;
134 |
135 | for (var key in arguments[i]) {
136 | if (arguments[i].hasOwnProperty(key)) {
137 | out[key] = arguments[i][key];
138 | }
139 | }
140 | }
141 |
142 | return out;
143 | }
144 |
145 | function forEach(array, callback, scope) {
146 | for (var i = 0; i < array.length; i++) {
147 | callback.call(scope, i, array[i]);
148 | }
149 | }
150 |
151 | return {
152 | defaults: defaults,
153 | toc: toc
154 | };
155 |
156 | })();
157 |
158 | Node.prototype.prependChild = function(element) {
159 | return this.firstChild ? this.insertBefore(element, this.firstChild) : this.appendChild(element);
160 | };
161 |
162 | // Very rudamentary:
163 | // - New observer each call
164 | // - Caller responsible for storing and disconnecting observer
165 | // - `querySelector` against container instead of going through actual mutations
166 | // For something more robust, see for example arrive.js.
167 | HTMLElement.prototype.arrive = function(selector, existing, callback) {
168 | function checkMutations() {
169 | var didArriveData = 'finallyHere';
170 | var target = query(selector);
171 |
172 | if (target && !target.dataset[didArriveData]) {
173 | target.dataset[didArriveData] = true;
174 | callback.call(target, target);
175 | }
176 | }
177 |
178 | var observer = new MutationObserver(checkMutations);
179 | observer.observe(this, { childList: true, subtree: true });
180 | if (existing) checkMutations();
181 |
182 | return observer;
183 | };
184 |
185 | function toElement(str) {
186 | var d = document.createElement('div');
187 | d.innerHTML = str;
188 | return d.firstElementChild;
189 | }
190 |
191 | function query(selector, scope) {
192 | return (scope || document).querySelector(selector);
193 | }
194 |
195 | // Inserted with gulp
196 | var css = '/* Anchor for .select-menu-modal-holder */\n#github-toc {\n position: relative;\n}\n/* Right-align menu on button */\n#github-toc > .select-menu-modal-holder {\n right: 0;\n top: 20px;\n}\n\n/* Center button in file actions bar */\n.github-toc-center-btn {\n margin-top: -5px;\n}\n\n.github-toc-right {\n float: right;\n}\n\n.github-toc-h1 {\n padding-left: 10px !important;\n font-weight: bold;\n font-size: 1.1em;\n}\n.github-toc-h2 {\n padding-left: 30px !important;\n font-weight: bold;\n}\n.github-toc-h3 {\n padding-left: 50px !important;\n font-weight: normal;\n}\n.github-toc-h4 {\n padding-left: 70px !important;\n font-weight: normal;\n}\n.github-toc-h5 {\n padding-left: 90px !important;\n font-weight: normal;\n}\n.github-toc-h6 {\n padding-left: 110px !important;\n font-weight: normal;\n}\n\n.github-toc-entry {\n color: black !important;\n border: none !important;\n line-height: 1.0 !important;\n}\n.github-toc-entry.navigation-focus {\n color: white !important;\n}\n\n.github-toc-backlink {\n color: black !important;\n display: none;\n}\n.github-toc-backlink > svg {\n vertical-align: middle !important;\n}\n\nh1:hover > .github-toc-backlink,\nh2:hover > .github-toc-backlink,\nh3:hover > .github-toc-backlink,\nh4:hover > .github-toc-backlink,\nh5:hover > .github-toc-backlink,\nh6:hover > .github-toc-backlink {\n display: block;\n}\n';
197 |
198 | var style = document.createElement('style');
199 | style.textContent = css;
200 | document.head.appendChild(style);
201 |
202 | var extPrefix = 'github-toc';
203 | var anchorIdGitHubPrefix = 'user-content-';
204 |
205 | var defaults = {
206 | backlinks: true
207 | };
208 |
209 | var templates = { // Inserted with gulp
210 | toc : '\n\n',
211 | entry : '\n',
212 | backlink : '\n \n\n'
213 | };
214 |
215 | var classes = {
216 | centerButton : extPrefix + '-center-btn',
217 | floatRight : extPrefix + '-right',
218 | wikiActions : 'gh-header-actions'
219 | };
220 |
221 | var selectors = {
222 | tocContainer : '#' + extPrefix,
223 | tocEntries : '#' + extPrefix + '-entries',
224 | headingAnchor : ':scope > a.anchor, :scope > ins > a.anchor',
225 | };
226 |
227 | var tocTargets = [
228 | { // Repo main page
229 | readme: '#readme .markdown-body',
230 | target: '#readme > h3',
231 | insert: function(toc, target) {
232 | toc.classList.add(classes.floatRight);
233 | toc.firstElementChild.classList.add(classes.centerButton);
234 | return target.appendChild(toc);
235 | }
236 | },
237 | { // Repo sub page (viewing, creating, editing files) and gists
238 | readme: '#files .markdown-body',
239 | target: '.file > .file-header > .file-actions',
240 | insert: function(toc, target) {
241 | return target.prependChild(toc);
242 | }
243 | },
244 | { // Wiki main and sub page (viewing, editing existing pages)
245 | readme: '#wiki-content .markdown-body:not(.wiki-custom-sidebar)',
246 | target: '#wiki-wrapper > .gh-header .gh-header-actions',
247 | insert: function(toc, target) {
248 | return target.prependChild(toc);
249 | }
250 | },
251 | { // Wiki main and sub page without actions bar (logged out or creating new pages)
252 | readme: '#wiki-content .markdown-body:not(.wiki-custom-sidebar)',
253 | target: '#wiki-wrapper > .gh-header',
254 | insert: function(toc, target) {
255 | toc.classList.add(classes.wikiActions);
256 | return target.prependChild(toc);
257 | }
258 | }
259 | ];
260 |
261 | var readmeSelector = tocTargets
262 | .map(function(t) { return t.readme; })
263 | .join(', ');
264 |
265 | var observer = document.body.arrive(readmeSelector, true, function(readme) {
266 |
267 | if (!readme || readme.classList.contains(extPrefix)) return;
268 | readme.classList.add(extPrefix);
269 |
270 | var existing = query(selectors.tocContainer);
271 | if (existing) {
272 | existing.remove();
273 | }
274 |
275 | var tocContainer = toElement(templates.toc);
276 | if (!insertToc(tocContainer)) return;
277 |
278 | TableOfContents.toc({
279 | target: selectors.tocEntries,
280 | content: readme,
281 | prefix: extPrefix,
282 | anchorId: anchorId,
283 | entryElement: entryElement,
284 | });
285 |
286 | // Include headings:
287 | // h2 > a.anchor (normal)
288 | // ins > h2 > a.anchor (inserted in rich diff)
289 | // h2 > ins > a.anchor (modified in rich diff)
290 | // Exclude:
291 | // del > h2 > a.anchor (deleted in rich diff)
292 | // h2 > del > a.anchor (modified in rich diff)
293 | function anchorId(_, heading) {
294 | var parentTag = heading.parentNode.tagName.toLowerCase();
295 | if (parentTag === 'del' ) return null;
296 | var anchor = query(selectors.headingAnchor, heading);
297 | if (!anchor || !anchor.id) return null;
298 |
299 | return anchor.id.split(anchorIdGitHubPrefix)[1];
300 | }
301 |
302 | function entryElement(_, heading, data) {
303 | if (!data.anchorId) return null;
304 |
305 | var entry = toElement(templates.entry);
306 | entry.classList.add(data.entryClass);
307 | entry.href = '#' + data.anchorId;
308 | entry.title = data.title;
309 | entry.textContent = data.title;
310 |
311 | if (defaults.backlinks) {
312 | var backlink = toElement(templates.backlink);
313 | heading.appendChild(backlink);
314 | }
315 |
316 | return entry;
317 | }
318 |
319 | function insertToc(toc) {
320 | return tocTargets.some(function(t) {
321 | var target = query(t.target);
322 | return target && t.insert(toc, target);
323 | });
324 | }
325 |
326 | });
327 |
328 | // For now, only used in Firefox
329 | function destroy() {
330 | if (observer) observer.disconnect();
331 | }
332 |
333 | })();
--------------------------------------------------------------------------------
/dist/safari.safariextension/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/dist/safari.safariextension/Icon.png
--------------------------------------------------------------------------------
/dist/safari.safariextension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Author
6 | Arthur Hammer
7 | Builder Version
8 | 11601.5.17.1
9 | CFBundleDisplayName
10 | Table of Contents for GitHub
11 | CFBundleIdentifier
12 | me.ahammer.github-toc
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleShortVersionString
16 | 0.2.5
17 | CFBundleVersion
18 | 0.2.5
19 | Chrome
20 |
21 | Global Page
22 | global.html
23 |
24 | Content
25 |
26 | Scripts
27 |
28 | End
29 |
30 | github-toc.js
31 |
32 |
33 | Stylesheets
34 |
35 | style.css
36 |
37 | Whitelist
38 |
39 | http://github.com/*/*
40 | https://github.com/*/*
41 | http://gist.github.com/*/*
42 | https://gist.github.com/*/*
43 |
44 |
45 | Description
46 | Adds a table of contents to repositories, gists and wikis on GitHub
47 | DeveloperIdentifier
48 | PY49CKQ6VF
49 | ExtensionInfoDictionaryVersion
50 | 1.0
51 | Permissions
52 |
53 | Website Access
54 |
55 | Allowed Domains
56 |
57 | github.com
58 | gist.github.com
59 |
60 | Include Secure Pages
61 |
62 | Level
63 | Some
64 |
65 |
66 | Update Manifest URL
67 | https://raw.githubusercontent.com/arthurhammer/github-toc/master/dist/SafariUpdate.plist
68 | Website
69 | https://github.com/arthurhammer/github-toc/
70 |
71 |
72 |
--------------------------------------------------------------------------------
/dist/safari.safariextension/github-toc.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 |
4 | var TableOfContents = (function() {
5 |
6 | var defaults = {
7 | // Where to insert the toc (selector or `Element`, first match)
8 | target: '#toc',
9 | // Where to look for headings (selector or `Element`, first match)
10 | content: 'body',
11 | // Which elements to create toc entries for (selector, not limited to `h1`-`h6`)
12 | headings: 'h1, h2, h3, h4, h5, h6',
13 | // Prefix to add to classes
14 | prefix: 'toc',
15 | // Wrap toc entry link elements with this element
16 | entryTagType: 'li',
17 |
18 | // Anchor id for a toc entry by which to identify the heading.
19 | // By default, an existing id on headings is expected.
20 | anchorId: function(i, heading, prefix) {
21 | return heading.id;
22 | },
23 | // Title for a toc entry
24 | title: function(i, heading, prefix) {
25 | return heading.textContent.trim();
26 | },
27 | // Class to add to a toc entry
28 | entryClass: function(i, heading, prefix) {
29 | var classPrefix = prefix ? (prefix + '-') : '';
30 | return classPrefix + heading.tagName.toLowerCase();
31 | },
32 |
33 | // Creates the actual toc entry element.
34 | // Default: `title`
35 | // By default, entries without an `anchorId` are skipped.
36 | entryElement: function(i, heading, data) {
37 | if (!data.anchorId) return null;
38 |
39 | var entry = document.createElement('a');
40 | entry.textContent = data.title;
41 | entry.href = '#' + data.anchorId;
42 |
43 | if (data.entryTagType) {
44 | var parent = document.createElement(data.entryTagType);
45 | parent.appendChild(entry);
46 | entry = parent;
47 | }
48 |
49 | if (data.entryClass) {
50 | entry.classList.add(data.entryClass);
51 | }
52 |
53 | return entry;
54 | }
55 | };
56 |
57 | function toc(options) {
58 | options = extend({}, TableOfContents.defaults, options);
59 |
60 | var target = getElement(options.target);
61 | var content = getElement(options.content);
62 | if (!target || !content) return null;
63 |
64 | var headings = content.querySelectorAll(options.headings);
65 |
66 | forEach(headings, function(i, h) {
67 | var anchorId = options.anchorId(i, h, options.prefix);
68 |
69 | var element = options.entryElement(i, h, {
70 | prefix: options.prefix,
71 | entryTagType: options.entryTagType,
72 | anchorId: anchorId,
73 | title: options.title(i, h, options.prefix),
74 | entryClass: options.entryClass(i, h, options.prefix)
75 | });
76 |
77 | if (element) {
78 | addAnchor(h, anchorId, options);
79 | target.appendChild(element);
80 | }
81 | });
82 |
83 | return target;
84 | }
85 |
86 | // TODO: Inserting elements can break CSS and other stuff
87 | // TODO: signature?
88 | function addAnchor(heading, anchorId, options) {
89 | if (!heading || !anchorId) return;
90 |
91 | if (anchorId !== heading.id) {
92 | var classPrefix = options.prefix ? (options.prefix + '-') : '';
93 | var anchorClass = classPrefix + 'anchor';
94 | var anchor = heading.querySelector(':scope > .' + anchorClass);
95 | if (!anchor) {
96 | anchor = document.createElement('span');
97 | }
98 | anchor.id = anchorId;
99 | anchor.classList.add(anchorClass);
100 | heading.insertBefore(anchor, heading.firstChild);
101 | }
102 | }
103 |
104 | function getElement(element) {
105 | // For now, only considers first match
106 | return (typeof element === 'string') ?
107 | document.querySelector(element) : element;
108 | }
109 |
110 | // from http://youmightnotneedjquery.com/#extend
111 | function extend(out) {
112 | out = out || {};
113 |
114 | for (var i = 1; i < arguments.length; i++) {
115 | if (!arguments[i]) continue;
116 |
117 | for (var key in arguments[i]) {
118 | if (arguments[i].hasOwnProperty(key)) {
119 | out[key] = arguments[i][key];
120 | }
121 | }
122 | }
123 |
124 | return out;
125 | }
126 |
127 | function forEach(array, callback, scope) {
128 | for (var i = 0; i < array.length; i++) {
129 | callback.call(scope, i, array[i]);
130 | }
131 | }
132 |
133 | return {
134 | defaults: defaults,
135 | toc: toc
136 | };
137 |
138 | })();
139 |
140 | Node.prototype.prependChild = function(element) {
141 | return this.firstChild ? this.insertBefore(element, this.firstChild) : this.appendChild(element);
142 | };
143 |
144 | // Very rudamentary:
145 | // - New observer each call
146 | // - Caller responsible for storing and disconnecting observer
147 | // - `querySelector` against container instead of going through actual mutations
148 | // For something more robust, see for example arrive.js.
149 | HTMLElement.prototype.arrive = function(selector, existing, callback) {
150 | function checkMutations() {
151 | var didArriveData = 'finallyHere';
152 | var target = query(selector);
153 |
154 | if (target && !target.dataset[didArriveData]) {
155 | target.dataset[didArriveData] = true;
156 | callback.call(target, target);
157 | }
158 | }
159 |
160 | var observer = new MutationObserver(checkMutations);
161 | observer.observe(this, { childList: true, subtree: true });
162 | if (existing) checkMutations();
163 |
164 | return observer;
165 | };
166 |
167 | function toElement(str) {
168 | var d = document.createElement('div');
169 | d.innerHTML = str;
170 | return d.firstElementChild;
171 | }
172 |
173 | function query(selector, scope) {
174 | return (scope || document).querySelector(selector);
175 | }
176 |
177 | var extPrefix = 'github-toc';
178 | var anchorIdGitHubPrefix = 'user-content-';
179 |
180 | var defaults = {
181 | backlinks: true
182 | };
183 |
184 | var templates = { // Inserted with gulp
185 | toc : '\n\n',
186 | entry : '\n',
187 | backlink : '\n \n\n'
188 | };
189 |
190 | var classes = {
191 | centerButton : extPrefix + '-center-btn',
192 | floatRight : extPrefix + '-right',
193 | wikiActions : 'gh-header-actions'
194 | };
195 |
196 | var selectors = {
197 | tocContainer : '#' + extPrefix,
198 | tocEntries : '#' + extPrefix + '-entries',
199 | headingAnchor : ':scope > a.anchor, :scope > ins > a.anchor',
200 | };
201 |
202 | var tocTargets = [
203 | { // Repo main page
204 | readme: '#readme .markdown-body',
205 | target: '#readme > h3',
206 | insert: function(toc, target) {
207 | toc.classList.add(classes.floatRight);
208 | toc.firstElementChild.classList.add(classes.centerButton);
209 | return target.appendChild(toc);
210 | }
211 | },
212 | { // Repo sub page (viewing, creating, editing files) and gists
213 | readme: '#files .markdown-body',
214 | target: '.file > .file-header > .file-actions',
215 | insert: function(toc, target) {
216 | return target.prependChild(toc);
217 | }
218 | },
219 | { // Wiki main and sub page (viewing, editing existing pages)
220 | readme: '#wiki-content .markdown-body:not(.wiki-custom-sidebar)',
221 | target: '#wiki-wrapper > .gh-header .gh-header-actions',
222 | insert: function(toc, target) {
223 | return target.prependChild(toc);
224 | }
225 | },
226 | { // Wiki main and sub page without actions bar (logged out or creating new pages)
227 | readme: '#wiki-content .markdown-body:not(.wiki-custom-sidebar)',
228 | target: '#wiki-wrapper > .gh-header',
229 | insert: function(toc, target) {
230 | toc.classList.add(classes.wikiActions);
231 | return target.prependChild(toc);
232 | }
233 | }
234 | ];
235 |
236 | var readmeSelector = tocTargets
237 | .map(function(t) { return t.readme; })
238 | .join(', ');
239 |
240 | var observer = document.body.arrive(readmeSelector, true, function(readme) {
241 |
242 | if (!readme || readme.classList.contains(extPrefix)) return;
243 | readme.classList.add(extPrefix);
244 |
245 | var existing = query(selectors.tocContainer);
246 | if (existing) {
247 | existing.remove();
248 | }
249 |
250 | var tocContainer = toElement(templates.toc);
251 | if (!insertToc(tocContainer)) return;
252 |
253 | TableOfContents.toc({
254 | target: selectors.tocEntries,
255 | content: readme,
256 | prefix: extPrefix,
257 | anchorId: anchorId,
258 | entryElement: entryElement,
259 | });
260 |
261 | // Include headings:
262 | // h2 > a.anchor (normal)
263 | // ins > h2 > a.anchor (inserted in rich diff)
264 | // h2 > ins > a.anchor (modified in rich diff)
265 | // Exclude:
266 | // del > h2 > a.anchor (deleted in rich diff)
267 | // h2 > del > a.anchor (modified in rich diff)
268 | function anchorId(_, heading) {
269 | var parentTag = heading.parentNode.tagName.toLowerCase();
270 | if (parentTag === 'del' ) return null;
271 | var anchor = query(selectors.headingAnchor, heading);
272 | if (!anchor || !anchor.id) return null;
273 |
274 | return anchor.id.split(anchorIdGitHubPrefix)[1];
275 | }
276 |
277 | function entryElement(_, heading, data) {
278 | if (!data.anchorId) return null;
279 |
280 | var entry = toElement(templates.entry);
281 | entry.classList.add(data.entryClass);
282 | entry.href = '#' + data.anchorId;
283 | entry.title = data.title;
284 | entry.textContent = data.title;
285 |
286 | if (defaults.backlinks) {
287 | var backlink = toElement(templates.backlink);
288 | heading.appendChild(backlink);
289 | }
290 |
291 | return entry;
292 | }
293 |
294 | function insertToc(toc) {
295 | return tocTargets.some(function(t) {
296 | var target = query(t.target);
297 | return target && t.insert(toc, target);
298 | });
299 | }
300 |
301 | });
302 |
303 | // For now, only used in Firefox
304 | function destroy() {
305 | if (observer) observer.disconnect();
306 | }
307 |
308 | })();
--------------------------------------------------------------------------------
/dist/safari.safariextension/style.css:
--------------------------------------------------------------------------------
1 | /* Anchor for .select-menu-modal-holder */
2 | #github-toc {
3 | position: relative;
4 | }
5 | /* Right-align menu on button */
6 | #github-toc > .select-menu-modal-holder {
7 | right: 0;
8 | top: 20px;
9 | }
10 |
11 | /* Center button in file actions bar */
12 | .github-toc-center-btn {
13 | margin-top: -5px;
14 | }
15 |
16 | .github-toc-right {
17 | float: right;
18 | }
19 |
20 | .github-toc-h1 {
21 | padding-left: 10px !important;
22 | font-weight: bold;
23 | font-size: 1.1em;
24 | }
25 | .github-toc-h2 {
26 | padding-left: 30px !important;
27 | font-weight: bold;
28 | }
29 | .github-toc-h3 {
30 | padding-left: 50px !important;
31 | font-weight: normal;
32 | }
33 | .github-toc-h4 {
34 | padding-left: 70px !important;
35 | font-weight: normal;
36 | }
37 | .github-toc-h5 {
38 | padding-left: 90px !important;
39 | font-weight: normal;
40 | }
41 | .github-toc-h6 {
42 | padding-left: 110px !important;
43 | font-weight: normal;
44 | }
45 |
46 | .github-toc-entry {
47 | color: black !important;
48 | border: none !important;
49 | line-height: 1.0 !important;
50 | }
51 | .github-toc-entry.navigation-focus {
52 | color: white !important;
53 | }
54 |
55 | .github-toc-backlink {
56 | color: black !important;
57 | display: none;
58 | }
59 | .github-toc-backlink > svg {
60 | vertical-align: middle !important;
61 | }
62 |
63 | h1:hover > .github-toc-backlink,
64 | h2:hover > .github-toc-backlink,
65 | h3:hover > .github-toc-backlink,
66 | h4:hover > .github-toc-backlink,
67 | h5:hover > .github-toc-backlink,
68 | h6:hover > .github-toc-backlink {
69 | display: block;
70 | }
71 |
--------------------------------------------------------------------------------
/img/banners/chrome-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/banners/chrome-banner.png
--------------------------------------------------------------------------------
/img/banners/chrome-banner.pxm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/banners/chrome-banner.pxm
--------------------------------------------------------------------------------
/img/icons/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/icons/icon128.png
--------------------------------------------------------------------------------
/img/icons/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/icons/icon16.png
--------------------------------------------------------------------------------
/img/icons/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/icons/icon48.png
--------------------------------------------------------------------------------
/img/screenshots/chrome-store1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/screenshots/chrome-store1.png
--------------------------------------------------------------------------------
/img/screenshots/chrome-store2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/screenshots/chrome-store2.png
--------------------------------------------------------------------------------
/img/screenshots/chrome-store3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/screenshots/chrome-store3.png
--------------------------------------------------------------------------------
/img/screenshots/cursor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/screenshots/cursor.png
--------------------------------------------------------------------------------
/img/screenshots/firefox-store1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/screenshots/firefox-store1.png
--------------------------------------------------------------------------------
/img/screenshots/safari1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurhammer/github-toc/b63cf835d4b7f028fb08f56a02300ffba253745b/img/screenshots/safari1.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-toc",
3 | "version": "0.2.5",
4 | "description": "Adds a table of contents to repositories, gists and wikis on GitHub",
5 | "author": "Arthur Hammer",
6 | "homepage": "https://github.com/arthurhammer/github-toc",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/arthurhammer/github-toc"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/arthurhammer/github-toc/issues",
13 | "email": "arthur@ahammer.me"
14 | },
15 | "license": "MIT",
16 | "main": "index.js",
17 | "scripts": {
18 | "clean": "rm -rf dist/*",
19 | "clean:build": "cd dist; rm -rf chrome firefox github-toc.js",
20 | "build": "npm-run-all build:js --parallel build:chrome build:firefox build:safari build:userscript",
21 | "build:js": "webpack",
22 | "build:chrome": "dest=dist/chrome; mkdir -p $dest; cp -r dist/github-toc.js img/icons src/style.css src/chrome/* $dest",
23 | "build:firefox": "dest=dist/firefox; mkdir -p $dest; cp -r dist/github-toc.js img/icons src/style.css src/firefox/* $dest",
24 | "build:safari": "dest=dist/safari.safariextension; mkdir -p $dest; cp -r dist/github-toc.js src/style.css src/safari/Info.plist $dest; cp img/icons/icon128.png $dest/Icon.png",
25 | "build:userscript": "TARGET=userscript webpack; cat src/userscript/header.txt dist/github-toc.user.js > dist/tmp; mv dist/tmp dist/github-toc.user.js",
26 | "dist": "npm-run-all clean build --parallel dist:* --sequential clean:build",
27 | "dist:chrome": "cd dist; zip -r chrome.zip chrome > /dev/null",
28 | "dist:firefox": "cd dist/firefox; web-ext build -a=. && mv *.zip ../firefox.zip",
29 | "dist:safari": "cp src/safari/SafariUpdate.plist dist",
30 | "chrome": "'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' --load-extension=dist/chrome",
31 | "firefox": "cd dist/firefox; web-ext run --start-url https://github.com/arthurhammer/github-toc"
32 | },
33 | "devDependencies": {
34 | "html-loader": "^0.4.5",
35 | "npm-run-all": "^4.0.2",
36 | "raw-loader": "^0.5.1",
37 | "uglifyjs-webpack-plugin": "^0.4.0",
38 | "web-ext": "^1.8.1",
39 | "webpack": "^2.3.3"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/chrome/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Table of Contents for GitHub",
4 | "short_name": "GitHub ToC",
5 | "version": "0.2.5",
6 | "author": "Arthur Hammer",
7 | "homepage_url": "https://github.com/arthurhammer/github-toc",
8 | "description": "Adds a table of contents to repositories, gists and wikis on GitHub",
9 | "icons": {
10 | "16": "icons/icon16.png",
11 | "48": "icons/icon48.png",
12 | "128": "icons/icon128.png"
13 | },
14 | "content_scripts": [{
15 | "matches": ["https://github.com/*/*", "https://gist.github.com/*/*"],
16 | "css": ["style.css"],
17 | "js": ["github-toc.js"]
18 | }]
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/firefox/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Table of Contents for GitHub",
4 | "short_name": "GitHub ToC",
5 | "version": "0.2.5",
6 | "author": "Arthur Hammer",
7 | "homepage_url": "https://github.com/arthurhammer/github-toc",
8 | "description": "Adds a table of contents to repositories, gists and wikis on GitHub",
9 | "applications": {
10 | "gecko": {
11 | "id": "@github-readme-toc",
12 | "strict_min_version": "42.0"
13 | }
14 | },
15 | "icons": {
16 | "16": "icons/icon16.png",
17 | "48": "icons/icon48.png",
18 | "128": "icons/icon128.png"
19 | },
20 | "content_scripts": [{
21 | "matches": ["https://github.com/*/*", "https://gist.github.com/*/*"],
22 | "css": ["style.css"],
23 | "js": ["github-toc.js"]
24 | }]
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/src/github-toc.js:
--------------------------------------------------------------------------------
1 | var TableOfContents = require('./toc');
2 | var util = require('./util');
3 |
4 | var extPrefix = 'github-toc'; // appId!?
5 | var anchorIdGitHubPrefix = 'user-content-'; // githubAnchorIdPrefix
6 |
7 | var defaults = {
8 | backlinks: true
9 | };
10 |
11 | var templates = { // Inserted with webpack
12 | toc : require('./html/toc.html'),
13 | entry : require('./html/entry.html'),
14 | backlink : require('./html/backlink.html')
15 | };
16 |
17 | var classes = {
18 | centerButton : extPrefix + '-center-btn',
19 | floatRight : extPrefix + '-right',
20 | wikiActions : 'gh-header-actions'
21 | };
22 |
23 | var selectors = {
24 | tocContainer : '#' + extPrefix,
25 | tocEntries : '#' + extPrefix + '-entries',
26 | headingAnchor : ':scope > a.anchor, :scope > ins > a.anchor',
27 | };
28 |
29 | var tocTargets = [
30 | { // Repo main page
31 | readme: '#readme .markdown-body',
32 | target: '#readme > h3',
33 | insert: function(toc, target) {
34 | toc.classList.add(classes.floatRight);
35 | toc.firstElementChild.classList.add(classes.centerButton);
36 | return target.appendChild(toc);
37 | }
38 | },
39 | { // Repo sub page (viewing, creating, editing files) and gists
40 | readme: '#files .markdown-body',
41 | target: '.file > .file-header > .file-actions',
42 | insert: function(toc, target) {
43 | return target.prependChild(toc);
44 | }
45 | },
46 | { // Wiki main and sub page (viewing, editing existing pages)
47 | readme: '#wiki-content .markdown-body:not(.wiki-custom-sidebar)',
48 | target: '#wiki-wrapper > .gh-header .gh-header-actions',
49 | insert: function(toc, target) {
50 | return target.prependChild(toc);
51 | }
52 | },
53 | { // Wiki main and sub page without actions bar (logged out or creating new pages)
54 | readme: '#wiki-content .markdown-body:not(.wiki-custom-sidebar)',
55 | target: '#wiki-wrapper > .gh-header',
56 | insert: function(toc, target) {
57 | toc.classList.add(classes.wikiActions);
58 | return target.prependChild(toc);
59 | }
60 | }
61 | ];
62 |
63 | var readmeSelector = tocTargets
64 | .map(function(t) { return t.readme; })
65 | .join(', ');
66 |
67 | document.body.arrive(readmeSelector, true, function(readme) {
68 |
69 | if (!readme || readme.classList.contains(extPrefix)) return;
70 | readme.classList.add(extPrefix);
71 |
72 | var existing = util.query(selectors.tocContainer);
73 | if (existing) {
74 | existing.remove();
75 | }
76 |
77 | var tocContainer = util.toElement(templates.toc);
78 | if (!insertToc(tocContainer)) return;
79 |
80 | TableOfContents.toc({
81 | target: selectors.tocEntries,
82 | content: readme,
83 | prefix: extPrefix,
84 | anchorId: anchorId,
85 | entryElement: entryElement,
86 | });
87 |
88 | // Include headings:
89 | // h2 > a.anchor (normal)
90 | // ins > h2 > a.anchor (inserted in rich diff)
91 | // h2 > ins > a.anchor (modified in rich diff)
92 | // Exclude:
93 | // del > h2 > a.anchor (deleted in rich diff)
94 | // h2 > del > a.anchor (modified in rich diff)
95 | function anchorId(_, heading) {
96 | var parentTag = heading.parentNode.tagName.toLowerCase();
97 | if (parentTag === 'del' ) return null;
98 | var anchor = util.query(selectors.headingAnchor, heading);
99 | if (!anchor || !anchor.id) return null;
100 |
101 | return anchor.id.split(anchorIdGitHubPrefix)[1];
102 | }
103 |
104 | function entryElement(_, heading, data) {
105 | if (!data.anchorId) return null;
106 |
107 | var entry = util.toElement(templates.entry);
108 | entry.classList.add(data.entryClass);
109 | entry.href = '#' + data.anchorId;
110 | entry.title = data.title;
111 | entry.textContent = data.title;
112 |
113 | if (defaults.backlinks) {
114 | var backlink = util.toElement(templates.backlink);
115 | heading.appendChild(backlink);
116 | }
117 |
118 | return entry;
119 | }
120 |
121 | function insertToc(toc) {
122 | return tocTargets.some(function(t) {
123 | var target = util.query(t.target);
124 | return target && t.insert(toc, target);
125 | });
126 | }
127 |
128 | });
129 |
--------------------------------------------------------------------------------
/src/html/backlink.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/html/entry.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/html/toc.html:
--------------------------------------------------------------------------------
1 |
2 |
42 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | require('./github-toc');
2 |
3 | if (TARGET === 'userscript') {
4 | require('./userscript/index');
5 | }
6 |
--------------------------------------------------------------------------------
/src/safari/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Author
6 | Arthur Hammer
7 | Builder Version
8 | 11601.5.17.1
9 | CFBundleDisplayName
10 | Table of Contents for GitHub
11 | CFBundleIdentifier
12 | me.ahammer.github-toc
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleShortVersionString
16 | 0.2.5
17 | CFBundleVersion
18 | 0.2.5
19 | Chrome
20 |
21 | Global Page
22 | global.html
23 |
24 | Content
25 |
26 | Scripts
27 |
28 | End
29 |
30 | github-toc.js
31 |
32 |
33 | Stylesheets
34 |
35 | style.css
36 |
37 | Whitelist
38 |
39 | http://github.com/*/*
40 | https://github.com/*/*
41 | http://gist.github.com/*/*
42 | https://gist.github.com/*/*
43 |
44 |
45 | Description
46 | Adds a table of contents to repositories, gists and wikis on GitHub
47 | DeveloperIdentifier
48 | PY49CKQ6VF
49 | ExtensionInfoDictionaryVersion
50 | 1.0
51 | Permissions
52 |
53 | Website Access
54 |
55 | Allowed Domains
56 |
57 | github.com
58 | gist.github.com
59 |
60 | Include Secure Pages
61 |
62 | Level
63 | Some
64 |
65 |
66 | Update Manifest URL
67 | https://raw.githubusercontent.com/arthurhammer/github-toc/master/dist/SafariUpdate.plist
68 | Website
69 | https://github.com/arthurhammer/github-toc/
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/safari/SafariUpdate.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Extension Updates
6 |
7 |
8 | CFBundleIdentifier
9 | me.ahammer.github-toc
10 | Developer Identifier
11 | PY49CKQ6VF
12 | CFBundleVersion
13 | 0.2.3
14 | CFBundleShortVersionString
15 | 0.2.3
16 | URL
17 | https://github.com/arthurhammer/github-toc/releases/download/v0.2.3/safari.safariextz
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | /* Anchor for .select-menu-modal-holder */
2 | #github-toc {
3 | position: relative;
4 | }
5 | /* Right-align menu on button */
6 | #github-toc > .select-menu-modal-holder {
7 | right: 0;
8 | top: 20px;
9 | }
10 |
11 | /* Center button in file actions bar */
12 | .github-toc-center-btn {
13 | margin-top: -5px;
14 | }
15 |
16 | .github-toc-right {
17 | float: right;
18 | }
19 |
20 | .github-toc-h1 {
21 | padding-left: 10px !important;
22 | font-weight: bold;
23 | font-size: 1.1em;
24 | }
25 | .github-toc-h2 {
26 | padding-left: 30px !important;
27 | font-weight: bold;
28 | }
29 | .github-toc-h3 {
30 | padding-left: 50px !important;
31 | font-weight: normal;
32 | }
33 | .github-toc-h4 {
34 | padding-left: 70px !important;
35 | font-weight: normal;
36 | }
37 | .github-toc-h5 {
38 | padding-left: 90px !important;
39 | font-weight: normal;
40 | }
41 | .github-toc-h6 {
42 | padding-left: 110px !important;
43 | font-weight: normal;
44 | }
45 |
46 | .github-toc-entry {
47 | color: black !important;
48 | border: none !important;
49 | line-height: 1.0 !important;
50 | }
51 | .github-toc-entry.navigation-focus {
52 | color: white !important;
53 | }
54 |
55 | .github-toc-backlink {
56 | color: black !important;
57 | display: none;
58 | }
59 | .github-toc-backlink > svg {
60 | vertical-align: middle !important;
61 | }
62 |
63 | h1:hover > .github-toc-backlink,
64 | h2:hover > .github-toc-backlink,
65 | h3:hover > .github-toc-backlink,
66 | h4:hover > .github-toc-backlink,
67 | h5:hover > .github-toc-backlink,
68 | h6:hover > .github-toc-backlink {
69 | display: block;
70 | }
71 |
--------------------------------------------------------------------------------
/src/toc.js:
--------------------------------------------------------------------------------
1 |
2 | var TableOfContents = (function() {
3 |
4 | var defaults = {
5 | // Where to insert the toc (selector or `Element`, first match)
6 | target: '#toc',
7 | // Where to look for headings (selector or `Element`, first match)
8 | content: 'body',
9 | // Which elements to create toc entries for (selector, not limited to `h1`-`h6`)
10 | headings: 'h1, h2, h3, h4, h5, h6',
11 | // Prefix to add to classes
12 | prefix: 'toc',
13 | // Wrap toc entry link elements with this element
14 | entryTagType: 'li',
15 |
16 | // Anchor id for a toc entry by which to identify the heading.
17 | // By default, an existing id on headings is expected.
18 | anchorId: function(i, heading, prefix) {
19 | return heading.id;
20 | },
21 | // Title for a toc entry
22 | title: function(i, heading, prefix) {
23 | return heading.textContent.trim();
24 | },
25 | // Class to add to a toc entry
26 | entryClass: function(i, heading, prefix) {
27 | var classPrefix = prefix ? (prefix + '-') : '';
28 | return classPrefix + heading.tagName.toLowerCase();
29 | },
30 |
31 | // Creates the actual toc entry element.
32 | // Default: `title`
33 | // By default, entries without an `anchorId` are skipped.
34 | entryElement: function(i, heading, data) {
35 | if (!data.anchorId) return null;
36 |
37 | var entry = document.createElement('a');
38 | entry.textContent = data.title;
39 | entry.href = '#' + data.anchorId;
40 |
41 | if (data.entryTagType) {
42 | var parent = document.createElement(data.entryTagType);
43 | parent.appendChild(entry);
44 | entry = parent;
45 | }
46 |
47 | if (data.entryClass) {
48 | entry.classList.add(data.entryClass);
49 | }
50 |
51 | return entry;
52 | }
53 | };
54 |
55 | function toc(options) {
56 | options = extend({}, TableOfContents.defaults, options);
57 |
58 | var target = getElement(options.target);
59 | var content = getElement(options.content);
60 | if (!target || !content) return null;
61 |
62 | var headings = content.querySelectorAll(options.headings);
63 |
64 | forEach(headings, function(i, h) {
65 | var anchorId = options.anchorId(i, h, options.prefix);
66 |
67 | var element = options.entryElement(i, h, {
68 | prefix: options.prefix,
69 | entryTagType: options.entryTagType,
70 | anchorId: anchorId,
71 | title: options.title(i, h, options.prefix),
72 | entryClass: options.entryClass(i, h, options.prefix)
73 | });
74 |
75 | if (element) {
76 | addAnchor(h, anchorId, options);
77 | target.appendChild(element);
78 | }
79 | });
80 |
81 | return target;
82 | }
83 |
84 | // TODO: Inserting elements can break CSS and other stuff
85 | // TODO: signature?
86 | function addAnchor(heading, anchorId, options) {
87 | if (!heading || !anchorId) return;
88 |
89 | if (anchorId !== heading.id) {
90 | var classPrefix = options.prefix ? (options.prefix + '-') : '';
91 | var anchorClass = classPrefix + 'anchor';
92 | var anchor = heading.querySelector(':scope > .' + anchorClass);
93 | if (!anchor) {
94 | anchor = document.createElement('span');
95 | }
96 | anchor.id = anchorId;
97 | anchor.classList.add(anchorClass);
98 | heading.insertBefore(anchor, heading.firstChild);
99 | }
100 | }
101 |
102 | function getElement(element) {
103 | // For now, only considers first match
104 | return (typeof element === 'string') ?
105 | document.querySelector(element) : element;
106 | }
107 |
108 | // from http://youmightnotneedjquery.com/#extend
109 | function extend(out) {
110 | out = out || {};
111 |
112 | for (var i = 1; i < arguments.length; i++) {
113 | if (!arguments[i]) continue;
114 |
115 | for (var key in arguments[i]) {
116 | if (arguments[i].hasOwnProperty(key)) {
117 | out[key] = arguments[i][key];
118 | }
119 | }
120 | }
121 |
122 | return out;
123 | }
124 |
125 | function forEach(array, callback, scope) {
126 | for (var i = 0; i < array.length; i++) {
127 | callback.call(scope, i, array[i]);
128 | }
129 | }
130 |
131 | return {
132 | defaults: defaults,
133 | toc: toc
134 | };
135 |
136 | })();
137 |
138 | module.exports = TableOfContents;
139 |
--------------------------------------------------------------------------------
/src/userscript/header.txt:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Table of Contents for GitHub
3 | // @description Adds a table of contents to repositories, gists and wikis on GitHub
4 | // @version 0.2.5
5 | // @author Arthur Hammer
6 | // @namespace https://github.com/arthurhammer
7 | // @license MIT
8 | // @homepage https://github.com/arthurhammer/github-toc
9 | // @updateURL https://github.com/arthurhammer/github-toc/raw/master/dist/github-toc.user.js
10 | // @downloadURL https://github.com/arthurhammer/github-toc/raw/master/dist/github-toc.user.js
11 | // @supportURL https://github.com/arthurhammer/github-toc/issues
12 | // @icon64 https://github.com/arthurhammer/github-toc/raw/master/img/icons/icon128.png
13 | // @match https://github.com/*/*
14 | // @match https://gist.github.com/*/*
15 | // @run-at document-body
16 | // @grant GM_addStyle
17 | // ==/UserScript==
18 |
--------------------------------------------------------------------------------
/src/userscript/index.js:
--------------------------------------------------------------------------------
1 | GM_addStyle(require('../style.css'));
2 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | Node.prototype.prependChild = function(element) {
2 | return this.firstChild ? this.insertBefore(element, this.firstChild) : this.appendChild(element);
3 | };
4 |
5 | // Very rudamentary:
6 | // - New observer each call
7 | // - Caller responsible for storing and disconnecting observer
8 | // - `querySelector` against container instead of going through actual mutations
9 | // For something more robust, see for example arrive.js.
10 | HTMLElement.prototype.arrive = function(selector, existing, callback) {
11 | function checkMutations() {
12 | var didArriveData = 'finallyHere';
13 | var target = module.exports.query(selector);
14 |
15 | if (target && !target.dataset[didArriveData]) {
16 | target.dataset[didArriveData] = true;
17 | callback.call(target, target);
18 | }
19 | }
20 |
21 | var observer = new MutationObserver(checkMutations);
22 | observer.observe(this, { childList: true, subtree: true });
23 | if (existing) checkMutations();
24 |
25 | return observer;
26 | };
27 |
28 | module.exports = {
29 |
30 | toElement: function(str) {
31 | var d = document.createElement('div');
32 | d.innerHTML = str;
33 | return d.firstElementChild;
34 | },
35 |
36 | query: function(selector, scope) {
37 | return (scope || document).querySelector(selector);
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | Informal (and incomplete) description of where the extension should work.
4 |
5 | These functional tests are necessary since the extension is subject to breaking whenever the GitHub website changes.
6 |
7 | **TODO**: Add proper testing.
8 |
9 | ---
10 |
11 | - Check the table of contents appears and behaves correctly on the following cases
12 | - Test all versions (Chrome, Firefox, Safari, userscript) of the extension
13 | - Test while logged in and logged out, if applicable
14 |
15 |
16 | ## Repositories
17 |
18 | - [Front page](https://github.com/rspec/rspec-core)
19 | - [Detail page](https://github.com/rspec/rspec-core/blob/master/README.md)
20 |
21 | ### Files in Other Formats
22 |
23 | - [Rst](https://github.com/jkbrzt/httpie)
24 | - [Rdoc](https://github.com/rdoc/rdoc)
25 | - [Org](https://github.com/yjwen/org-reveal)
26 |
27 | Full list of supported formats [here](https://github.com/github/markup#markups).
28 |
29 | ### Editing
30 |
31 | - Edit an existing markup file, make changes and preview
32 | - Create and edit a new markup file and preview
33 | - Rename a file from a non-markup to a markup extension and preview (e.g. from `js` to `md`)
34 | - Rename a file from a markup to a non-markup extension and preview
35 |
36 | In all cases, try removing and changing existing and inserting new headings.
37 |
38 | ### Other
39 |
40 | - [Rich diffs for markup files in commit pages](https://github.com/arthurhammer/github-toc/commit/a2d9a04b3aa5cfbb4434c54b184c31afa3450278?short_path=1e290ac#diff-1e290ac8433d555bce009b162cb869d0)
41 | - (toc will currently only appear on the first rich diff)
42 | - Check the table of contents appears when navigating while not triggering a new page load (ajax)
43 | - e.g. navigate from the [main repo page](https://github.com/arthurhammer/github-toc) to the detail page for [`Readme.md`](https://github.com/arthurhammer/github-toc/blob/master/Readme.md)
44 |
45 | ## Wiki
46 |
47 | - [Front page](https://github.com/gollum/gollum/wiki)
48 | - [Sub page](https://github.com/gollum/gollum/wiki/Git-adapters)
49 | - [Wiki with custom sidebar](https://github.com/mbostock/d3/wiki)
50 |
51 | Test while logged in and while logged out.
52 |
53 | ### Editing
54 |
55 | - Edit an existing page, make changes and preview
56 | - Create and edit a new page
57 |
58 | In all cases, try removing and changing existing and inserting new headings.
59 |
60 | ## Gist
61 |
62 | - [Gist](https://gist.github.com/benweet/6312489)
63 | - [Gist with multiple readmes](https://gist.github.com/arthurhammer/2261163aca4c0e931517)
64 | - (toc will currently only appear on the first one)
65 |
66 | ### Editing
67 |
68 | Currently there are no markup previews in gists, so nothing should happen while editing.
69 |
70 | ## Other
71 |
72 | - [Random](https://github.com/arthurhammer/github-toc/blob/master/test/test.markdown)
73 | - Test other random stuff in headings, e.g. strong, italics, special characters, custom html, mix of all of these etc.
74 | - e.g. edge cases like images in headings, non-breaking spaces etc. don't work currently (this is due GitHub not supporting these for the most part, it doesn't generate `id` attributes)
75 |
76 | ## Where It Shouldn't Work
77 |
78 | Markup content appears in a lot of places on GitHub. A table of contents should not appear for comments and descriptions in issues, pull requests, commit pages and similar.
79 |
--------------------------------------------------------------------------------
/test/test.markdown:
--------------------------------------------------------------------------------
1 | # Curabitur mollis eget orci nec dignissim. Cras sed interdum orci.
2 |
3 | Phasellus nec tortor non sapien luctus iaculis nec et erat. Curabitur dignissim ligula id tortor placerat, eleifend finibus lacus faucibus. Pellentesque eros nulla, ullamcorper nec dui non, dignissim venenatis nisi. Aliquam eleifend id libero sit amet laoreet. Aenean eu egestas.
4 |
5 | ###### Suspendisse commodo imperdiet blandit. Quisque sollicitudin quis ligula rhoncus pulvinar. In eget est ac lectus.
6 |
7 | Phasellus nec tortor non sapien luctus iaculis nec et erat. Curabitur dignissim ligula id tortor placerat, eleifend finibus lacus faucibus. Pellentesque eros nulla, ullamcorper nec dui non, dignissim venenatis nisi. Aliquam eleifend id libero sit amet laoreet. Aenean eu egestas.
8 |
9 | ## Nam ut sagittis lectus. Curabitur.
10 |
11 | ### Interdum et malesuada fames ac.
12 |
13 | #### Sed congue mi vitae dui.
14 |
15 | sajdnsaf
16 |
17 | # Curabitur mollis eget orci nec dignissim. Cras sed interdum orci.
18 |
19 | ###### Suspendisse commodo imperdiet blandit. Quisque sollicitudin quis ligula rhoncus pulvinar. In eget est ac lectus.
20 |
21 | ## Cras a dui et est.
22 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const DefinePlugin = require('webpack').DefinePlugin;
3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
4 |
5 | // Get environment variable
6 | const target = process.env.TARGET || '';
7 |
8 | const config = {
9 |
10 | entry: './src/index.js',
11 | output: {
12 | filename: (target === 'userscript') ? 'github-toc.user.js' : 'github-toc.js',
13 | path: path.resolve(__dirname, 'dist')
14 | },
15 |
16 | module: {
17 | rules: [{
18 | test: /\.html$/,
19 | loader: 'html-loader',
20 | options: { minimize: true },
21 | }, {
22 | test: /\.css$/,
23 | loader: 'raw-loader'
24 | }
25 | ]
26 | },
27 |
28 | plugins: [
29 | // Inject environment variable as a global into code
30 | new DefinePlugin({
31 | TARGET: JSON.stringify(target),
32 | }),
33 | // Remove dead code from target branching
34 | new UglifyJSPlugin({
35 | beautify: true,
36 | mangle: false,
37 | })
38 | ]
39 |
40 | };
41 |
42 | module.exports = config;
43 |
--------------------------------------------------------------------------------