├── .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 | ![Screenshot](img/screenshots/safari1.png) 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\n \n \n \n \n \n\n \n \n\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\n \n \n \n \n \n\n \n \n\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 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 40 | 41 | 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 | --------------------------------------------------------------------------------