├── demo ├── nested │ ├── .editorconfig │ ├── bar.js │ └── foo.js ├── bar.js ├── foo.js └── .editorconfig ├── src ├── chrome │ └── extension_info.json ├── firefox │ └── extension_info.json ├── opera │ └── extension_info.json ├── common │ ├── icons │ │ ├── button.png │ │ ├── icon32.png │ │ ├── icon48.png │ │ ├── icon100.png │ │ └── icon128.png │ ├── res │ │ └── default.editorconfig │ ├── options.js │ ├── fs-http.js │ ├── options.html │ ├── background.js │ └── content.js └── safari │ └── extension_info.json ├── .gitignore ├── webpack.config.js ├── generate-manifest.js ├── LICENSE ├── package.json └── README.md /demo/nested/.editorconfig: -------------------------------------------------------------------------------- 1 | [foo.js] 2 | tab_width = 3 3 | -------------------------------------------------------------------------------- /src/chrome/extension_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "bppnolhdpdfmmpeefopdbpmabdpoefjh" 3 | } -------------------------------------------------------------------------------- /demo/bar.js: -------------------------------------------------------------------------------- 1 | function bar() { 2 | var FOO = 'FOO', 3 | barBazQux = 'BAR-BAZ-QUX'; 4 | } 5 | -------------------------------------------------------------------------------- /demo/foo.js: -------------------------------------------------------------------------------- 1 | function foo() { 2 | var foo = 'FOO', 3 | barBazQux = 'BAR-BAZ-QUX'; 4 | } 5 | -------------------------------------------------------------------------------- /src/firefox/extension_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "{E9686429-E445-4B89-9516-322DAE1E0389}" 3 | } -------------------------------------------------------------------------------- /demo/nested/bar.js: -------------------------------------------------------------------------------- 1 | function bar() { 2 | var FOO = 'FOO', 3 | barBazQux = 'BAR-BAZ-QUX'; 4 | } 5 | -------------------------------------------------------------------------------- /demo/nested/foo.js: -------------------------------------------------------------------------------- 1 | function foo() { 2 | var FOO = 'FOO', 3 | barBazQux = 'BAR-BAZ-QUX'; 4 | } 5 | -------------------------------------------------------------------------------- /src/opera/extension_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://kangoextensions.com/extensions/githubeditorconfig/" 3 | } -------------------------------------------------------------------------------- /src/common/icons/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cougar/github-editorconfig/master/src/common/icons/button.png -------------------------------------------------------------------------------- /src/common/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cougar/github-editorconfig/master/src/common/icons/icon32.png -------------------------------------------------------------------------------- /src/common/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cougar/github-editorconfig/master/src/common/icons/icon48.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /certificates 2 | /*.log 3 | *.built.* 4 | /src/common/extension_info.json 5 | /node_modules 6 | /output 7 | -------------------------------------------------------------------------------- /src/common/icons/icon100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cougar/github-editorconfig/master/src/common/icons/icon100.png -------------------------------------------------------------------------------- /src/common/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cougar/github-editorconfig/master/src/common/icons/icon128.png -------------------------------------------------------------------------------- /src/safari/extension_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "developer_id": "YOUR_SAFARI_DEVELOPER_ID", 3 | "id": "com.kangoextensions.githubeditorconfig" 4 | } -------------------------------------------------------------------------------- /demo/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | 6 | [foo.js] 7 | tab_width = 4 8 | 9 | [bar.js] 10 | tab_width = 3 11 | -------------------------------------------------------------------------------- /src/common/res/default.editorconfig: -------------------------------------------------------------------------------- 1 | # This will be used as default EditorConfig for repos that don't have their own. 2 | # It's saved automatically as you type. 3 | # You can find docs at http://editorconfig.org 4 | 5 | [*] 6 | tab_width = 4 7 | 8 | [*.rb] 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/common/background.js', 3 | output: { 4 | path: 'src/common', 5 | filename: 'background.built.js' 6 | }, 7 | module: { 8 | loaders: [ 9 | {test: /\.json$/, loader: 'json-loader'} 10 | ], 11 | noParse: /fnmatch/ 12 | }, 13 | resolve: { 14 | root: process.cwd(), 15 | alias: { 16 | fs: 'src/common/fs-http.js' 17 | } 18 | } 19 | }; -------------------------------------------------------------------------------- /src/common/options.js: -------------------------------------------------------------------------------- 1 | KangoAPI.onReady(function () { 2 | var storage = kango.storage; 3 | var area = document.getElementById('config'); 4 | 5 | area.value = storage.getItem('editorconfig'); 6 | 7 | function store() { 8 | storage.setItem('editorconfig', area.value); 9 | } 10 | 11 | var lastTimeout = 0; 12 | 13 | area.addEventListener('keyup', function () { 14 | clearTimeout(lastTimeout); 15 | lastTimeout = setTimeout(store, 100); 16 | }); 17 | }); -------------------------------------------------------------------------------- /src/common/fs-http.js: -------------------------------------------------------------------------------- 1 | var resolvePath = require('path').resolve; 2 | 3 | exports.readFile = function (path) { 4 | var callback = arguments[arguments.length - 1]; 5 | kango.xhr.send({ 6 | method: 'GET', 7 | async: true, 8 | url: 'https://raw.githubusercontent.com' + resolvePath(path) 9 | }, function (data) { 10 | if (data.status === 200) { 11 | callback(null, data.response); 12 | } else { 13 | callback(new Error(data.response)); 14 | } 15 | }); 16 | }; -------------------------------------------------------------------------------- /generate-manifest.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var pkg = require('./package.json'); 3 | 4 | var manifest = {}; 5 | 6 | ['name', 'version', 'description'].forEach(function (name) { 7 | manifest[name] = pkg[name]; 8 | }); 9 | 10 | Object.keys(pkg.extension_info).forEach(function (name) { 11 | manifest[name] = pkg.extension_info[name]; 12 | }); 13 | 14 | manifest.creator = pkg.author; 15 | manifest.homepage_url = pkg.homepage; 16 | 17 | fs.writeFileSync('src/common/extension_info.json', JSON.stringify(manifest, null, 4)); 18 | -------------------------------------------------------------------------------- /src/common/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Default EditorConfig for GitHub 5 | 6 | 7 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ingvar Stepanyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/common/background.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | var pathUtils = require('path'); 3 | var ec = require('editorconfig'); 4 | 5 | if (!kango.storage.getItem('editorconfig')) { 6 | kango.xhr.send({ 7 | method: 'GET', 8 | async: true, 9 | url: 'res/default.editorconfig' 10 | }, function (data) { 11 | kango.storage.setItem('editorconfig', data.response); 12 | }); 13 | } 14 | 15 | global.getEditorConfig = function (path, callback) { 16 | var defaultConfig = ec.parseFromFiles(path.relative, [{ 17 | name: pathUtils.resolve('.editorconfig'), 18 | contents: kango.storage.getItem('editorconfig') || '' 19 | }]); 20 | 21 | var repoConfig = ec.parse(pathUtils.join(path.root, path.relative), { 22 | root: path.root 23 | }); 24 | 25 | Promise.settle([defaultConfig, repoConfig]) 26 | .reduce(function (merged, current) { 27 | if (current.isFulfilled()) { 28 | current = current.value(); 29 | for (var name in current) { 30 | merged[name] = current[name]; 31 | } 32 | } 33 | return merged; 34 | }, {$path: path}) 35 | .done(callback); 36 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-editorconfig", 3 | "version": "1.1.1", 4 | "description": "EditorConfig support for GitHub", 5 | "dependencies": { 6 | "bluebird": "^2.3.6", 7 | "editorconfig": "^0.12.0" 8 | }, 9 | "devDependencies": { 10 | "json-loader": "^0.5.1", 11 | "webpack": "^1.4.8" 12 | }, 13 | "scripts": { 14 | "kango": "python ../kango/kango.py build .", 15 | "prepublish": "node generate-manifest && webpack -p && npm run kango", 16 | "watch": "webpack --watch --debug --devtool eval --output-path=output/chrome", 17 | "dev": "npm run prepublish && npm run watch" 18 | }, 19 | "extension_info": { 20 | "name": "GitHub EditorConfig", 21 | "background_scripts": [ 22 | "background.built.js" 23 | ], 24 | "content_scripts": [ 25 | "content.js" 26 | ], 27 | "options_page": "options.html", 28 | "permissions": { 29 | "content_scripts": [ 30 | "https://github.com/*" 31 | ], 32 | "xhr": [ 33 | "https://raw.githubusercontent.com/*" 34 | ] 35 | } 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/RReverser/github-editorconfig" 40 | }, 41 | "author": "Ingvar Stepanyan (http://rreverser.com)", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/RReverser/github-editorconfig/issues" 45 | }, 46 | "homepage": "https://github.com/RReverser/github-editorconfig", 47 | "private": true 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | github-editorconfig 2 | =================== 3 | 4 | This is a browser extension that provides [EditorConfig](http://editorconfig.org/) support for GitHub. 5 | 6 | Download links 7 | -------------- 8 | 9 | You can download extension for your browser from the corresponding store: 10 | 11 | * [Chrome](https://chrome.google.com/webstore/detail/github-editorconfig/bppnolhdpdfmmpeefopdbpmabdpoefjh) 12 | * [Firefox](https://github.com/RReverser/github-editorconfig/releases/download/1.1.0/githubeditorconfig_1.1.0.xpi) 13 | * [Opera](https://addons.opera.com/extensions/details/github-editorconfig/) 14 | 15 | Description 16 | ----------- 17 | 18 | Extension looks for [`.editorconfig`](http://editorconfig.org/#example-file) files in the repository the current file belongs to, and applies it's settings to code viewer and editor. Branch is always taken into account. 19 | 20 | On options page you can also set [default editorconfig](src/common/res/default.editorconfig). 21 | 22 | You can test extension on files in [demo](demo) folder of this repo. 23 | 24 | Extension is built with [Kango - cross-browser extension framework](http://kangoextensions.com/). 25 | 26 | Screenshots 27 | ----------- 28 | 29 | Sample .editorconfig: 30 | 31 | ![Sample .editorconfig](https://cloud.githubusercontent.com/assets/557590/4751070/01e62090-5a9a-11e4-96e8-85d0d1e3c79e.png) 32 | 33 | Code viewer (tabs are set to preconfigured width of 4 instead of GitHub's default 8): 34 | 35 | ![Code viewer](https://cloud.githubusercontent.com/assets/557590/4751072/01e6e6e2-5a9a-11e4-862d-53b65d109958.png) 36 | 37 | Code editor (preconfigured options are chosen and marked as **(auto)**; `trim_trailing_whitespace` and `insert_final_newline` are taken into account on commit): 38 | 39 | ![Code editor](https://cloud.githubusercontent.com/assets/557590/4751069/01e2c918-5a9a-11e4-83fe-be49db527d28.png) 40 | 41 | Options page (just a default `.editorconfig`): 42 | 43 | ![Options page (default editorconfig)](https://cloud.githubusercontent.com/assets/557590/4751071/01e66b9a-5a9a-11e4-91f3-36800fbc8466.png) 44 | -------------------------------------------------------------------------------- /src/common/content.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name github-editorconfig 3 | // @include https://github.com/* 4 | // ==/UserScript== 5 | 6 | function $(query, context) { 7 | return (context || document).querySelector(query); 8 | } 9 | 10 | function reselect(selectQuery, newValue, insert) { 11 | var select = $(selectQuery), option; 12 | // not applicable for current page 13 | if (!select) { 14 | return; 15 | } 16 | // remove ' (auto)' in old option 17 | option = select.options[select.selectedIndex]; 18 | option.textContent = option.textContent.replace(' (auto)', ''); 19 | // set new value 20 | select.value = newValue; 21 | option = select.options[select.selectedIndex]; 22 | // if such option doesn't exist, create it 23 | if (!option) { 24 | option = document.createElement('option'); 25 | option.value = newValue; 26 | insert(option, select); 27 | option.selected = true; 28 | } 29 | // add ' (auto)' to editorconfig'ured option 30 | option.textContent += ' (auto)'; 31 | // manually fire 'change' event on select (as it doesn't by default) 32 | var e = document.createEvent('HTMLEvents'); 33 | e.initEvent('change', false, true); 34 | select.dispatchEvent(e); 35 | return option; 36 | } 37 | 38 | var config = {}; 39 | 40 | function setEditorConfig(newConfig) { 41 | config = newConfig; 42 | 43 | var viewer = $('.highlight'); 44 | 45 | // set 'tab-size' CSS property 46 | if (viewer && config.tab_width) { 47 | ['tabSize', 'MozTabSize', 'OTabSize', 'WebkitTabSize'].some(function (propName) { 48 | if (propName in this) { 49 | this[propName] = config.tab_width; 50 | return true; 51 | } 52 | }, viewer.style); 53 | } 54 | 55 | if (config.indent_style) { 56 | reselect('.js-code-indent-mode', config.indent_style + 's'); 57 | } 58 | 59 | if (config.indent_size) { 60 | reselect('.js-code-indent-width', config.indent_size, function (option, select) { 61 | var optgroup = $('optgroup', select); 62 | option.textContent = option.value; 63 | 64 | // find place to insert new option and keep list sorted 65 | var options = select.options; 66 | for (var beforeIndex = 0; beforeIndex < options.length; beforeIndex++) { 67 | if (options[beforeIndex].value > option.value) { 68 | break; 69 | } 70 | } 71 | optgroup.insertBefore(option, options[beforeIndex]); 72 | }); 73 | } 74 | } 75 | 76 | // bind once for navigating through History API 77 | document.addEventListener('submit', function (event) { 78 | if (event.target !== $('.edit-file>form')) { 79 | return; 80 | } 81 | 82 | var editor = $('#blob_contents'); 83 | var text = editor.value; 84 | 85 | if (config.trim_trailing_whitespace) { 86 | text = text.replace(/\s*?$/mg, ''); 87 | } 88 | 89 | if (config.insert_final_newline && text.slice(-1) !== '\n') { 90 | text += '\n'; 91 | } 92 | 93 | editor.value = text; 94 | }); 95 | 96 | function getEditorConfig(pathname, callback) { 97 | var path = pathname.slice(1).split('/'); 98 | 99 | var repo = path.slice(0, 2); 100 | var action = path[2]; // 101 | var commit = path[3]; // use branch name by default 102 | 103 | if (action !== 'blob' && action !== 'edit') { 104 | return; 105 | } 106 | 107 | // try to find exact commit SHA on page 108 | var commitElement; 109 | if (commitElement = $('.js-permalink-shortcut')) { 110 | commit = commitElement.pathname.split('/')[4]; 111 | } else if (commitElement = $('input[name="commit"]')) { 112 | commit = commitElement.value; 113 | } 114 | 115 | kango.invokeAsyncCallback('getEditorConfig', { 116 | absolute: pathname, 117 | root: repo.concat([commit]).join('/'), 118 | relative: path.slice(4).join('/') 119 | }, callback); 120 | } 121 | 122 | var lastPathName = ''; 123 | 124 | function update() { 125 | var newPathName = location.pathname; 126 | if (newPathName === lastPathName) { 127 | return; 128 | } 129 | lastPathName = newPathName; 130 | getEditorConfig(newPathName, function (config) { 131 | if (config.$path.absolute === location.pathname) { 132 | setEditorConfig(config); 133 | } 134 | }); 135 | } 136 | 137 | var pjaxContainer = $('#js-repo-pjax-container'); 138 | 139 | if (pjaxContainer) { 140 | // use MutationObserver as we can't inject into History API 141 | new MutationObserver(update).observe(pjaxContainer, {childList: true}); 142 | 143 | // initial "update" 144 | update(); 145 | } --------------------------------------------------------------------------------