├── .editorconfig
├── .eslintrc
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── dist
├── content.js
├── content.js.map
├── manifest.json
├── options.html
├── options.js
└── options.js.map
├── lib
├── content.js
├── content
│ ├── catPhoto.js
│ ├── gittenify.js
│ ├── replaceUsers.js
│ └── selectors.js
├── options.js
└── options
│ ├── elements.js
│ ├── itemForUser.js
│ ├── renderList.js
│ ├── ui.js
│ └── updateUi.js
└── package.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is Awesome: http://editorconfig.org
2 |
3 | # Top-most EditorConfig file.
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file.
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | charset = utf-8
11 | indent_style = space
12 | indent_size = 2
13 | trim_trailing_whitespace = true
14 |
15 | # Don't trim whitespace in Markdown in order to be able
16 | # to do two spaces for line breaks.
17 | [*.md]
18 | trim_trailing_whitespace = false
19 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb/base"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io
2 |
3 | ### OSX ###
4 | .DS_Store
5 | .AppleDouble
6 | .LSOverride
7 |
8 | # Icon must end with two \r
9 | Icon
10 |
11 |
12 | # Thumbnails
13 | ._*
14 |
15 | # Files that might appear in the root of a volume
16 | .DocumentRevisions-V100
17 | .fseventsd
18 | .Spotlight-V100
19 | .TemporaryItems
20 | .Trashes
21 | .VolumeIcon.icns
22 |
23 | # Directories potentially created on remote AFP share
24 | .AppleDB
25 | .AppleDesktop
26 | Network Trash Folder
27 | Temporary Items
28 | .apdisk
29 |
30 |
31 | ### Node ###
32 | # Logs
33 | logs
34 | *.log
35 |
36 | # Runtime data
37 | pids
38 | *.pid
39 | *.seed
40 |
41 | # Directory for instrumented libs generated by jscoverage/JSCover
42 | lib-cov
43 |
44 | # Coverage directory used by tools like istanbul
45 | coverage
46 |
47 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
48 | .grunt
49 |
50 | # node-waf configuration
51 | .lock-wscript
52 |
53 | # Compiled binary addons (http://nodejs.org/api/addons.html)
54 | build/Release
55 |
56 | # Dependency directory
57 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
58 | node_modules
59 |
60 |
61 | ### Windows ###
62 | # Windows image file caches
63 | Thumbs.db
64 | ehthumbs.db
65 |
66 | # Folder config file
67 | Desktop.ini
68 |
69 | # Recycle Bin used on file shares
70 | $RECYCLE.BIN/
71 |
72 | # Windows Installer files
73 | *.cab
74 | *.msi
75 | *.msm
76 | *.msp
77 |
78 | # Windows shortcuts
79 | *.lnk
80 |
81 |
82 | ### Linux ###
83 | *~
84 |
85 | # KDE directory preferences
86 | .directory
87 |
88 | # Linux trash folder which might appear on any partition or disk
89 | .Trash-*
90 |
91 |
92 | ### Sass ###
93 | .sass-cache
94 | *.css.map
95 |
96 |
97 | ### WebStorm ###
98 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
99 |
100 | *.iml
101 |
102 | ## Directory-based project format:
103 | .idea/
104 | # if you remove the above rule, at least ignore the following:
105 |
106 | # User-specific stuff:
107 | # .idea/workspace.xml
108 | # .idea/tasks.xml
109 | # .idea/dictionaries
110 |
111 | # Sensitive or high-churn files:
112 | # .idea/dataSources.ids
113 | # .idea/dataSources.xml
114 | # .idea/sqlDataSources.xml
115 | # .idea/dynamic.xml
116 | # .idea/uiDesigner.xml
117 |
118 | # Gradle:
119 | # .idea/gradle.xml
120 | # .idea/libraries
121 |
122 | # Mongo Explorer plugin:
123 | # .idea/mongoSettings.xml
124 |
125 | ## File-based project format:
126 | *.ipr
127 | *.iws
128 |
129 | ## Plugin-specific files:
130 |
131 | # IntelliJ
132 | /out/
133 |
134 | # mpeltonen/sbt-idea plugin
135 | .idea_modules/
136 |
137 | # JIRA plugin
138 | atlassian-ide-plugin.xml
139 |
140 | # Crashlytics plugin (for Android Studio and IntelliJ)
141 | com_crashlytics_export_strings.xml
142 | crashlytics.properties
143 | crashlytics-build.properties
144 |
145 |
146 | ### PhpStorm ###
147 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
148 |
149 | *.iml
150 |
151 | ## Directory-based project format:
152 | .idea/
153 | # if you remove the above rule, at least ignore the following:
154 |
155 | # User-specific stuff:
156 | # .idea/workspace.xml
157 | # .idea/tasks.xml
158 | # .idea/dictionaries
159 |
160 | # Sensitive or high-churn files:
161 | # .idea/dataSources.ids
162 | # .idea/dataSources.xml
163 | # .idea/sqlDataSources.xml
164 | # .idea/dynamic.xml
165 | # .idea/uiDesigner.xml
166 |
167 | # Gradle:
168 | # .idea/gradle.xml
169 | # .idea/libraries
170 |
171 | # Mongo Explorer plugin:
172 | # .idea/mongoSettings.xml
173 |
174 | ## File-based project format:
175 | *.ipr
176 | *.iws
177 |
178 | ## Plugin-specific files:
179 |
180 | # IntelliJ
181 | /out/
182 |
183 | # mpeltonen/sbt-idea plugin
184 | .idea_modules/
185 |
186 | # JIRA plugin
187 | atlassian-ide-plugin.xml
188 |
189 | # Crashlytics plugin (for Android Studio and IntelliJ)
190 | com_crashlytics_export_strings.xml
191 | crashlytics.properties
192 | crashlytics-build.properties
193 |
194 |
195 | ### SublimeText ###
196 | # cache files for sublime text
197 | *.tmlanguage.cache
198 | *.tmPreferences.cache
199 | *.stTheme.cache
200 |
201 | # workspace files are user-specific
202 | *.sublime-workspace
203 |
204 | # project files should be checked into the repository, unless a significant
205 | # proportion of contributors will probably not be using SublimeText
206 | # *.sublime-project
207 |
208 | # sftp configuration file
209 | sftp-config.json
210 |
211 |
212 | ### Bower ###
213 | bower_components
214 | .bower-cache
215 | .bower-registry
216 | .bower-tmp
217 |
218 |
219 | ### Vim ###
220 | [._]*.s[a-w][a-z]
221 | [._]s[a-w][a-z]
222 | *.un~
223 | Session.vim
224 | .netrwhist
225 | *~
226 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Will Binns-Smith
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gittens
2 |
3 | Disguises unpleasant gits (as in: [a completely ignorant, childish person with no manners](http://www.urbandictionary.com/define.php?term=Git)) that sometimes crop up on GitHub by replacing them with KITTENS!
4 |
5 | ## Installation
6 |
7 | This should be on the Chrome Web Store soon™, but for now, go to Chrome Settings → Extensions → check off "Developer Mode" → "Load unpacked extension...", and point it at the `dist` directory.
8 |
9 | ## Building
10 |
11 | For development, just run `npm start`. To do a single build, run `npm run build`, if you want to do a single build without running Uglify over the code run `npm run build-dev`.
12 |
13 | ## Screenshots
14 |
15 |
16 |
17 |
18 |
19 | # License
20 |
21 | MIT © [Reactivists](https://github.com/orgs/reactivepod/teams/reactivists)
22 |
--------------------------------------------------------------------------------
/dist/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gittens",
3 | "version": "1.0.0",
4 | "manifest_version": 2,
5 | "permissions": [
6 | "storage"
7 | ],
8 | "content_scripts": [
9 | {
10 | "matches": ["*://*.github.com/*"],
11 | "js": ["content.js"]
12 | }
13 | ],
14 | "options_ui": {
15 | "page": "options.html",
16 | "chrome_style": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/dist/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Gittens
4 |
5 |
44 |
45 |
46 |
47 |
48 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
60 |
61 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/dist/options.js:
--------------------------------------------------------------------------------
1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o {\n userList.appendChild(itemForUser(user));\n });\n}\n",
22 | "import { themeSelect, replaceTextInput, form, userInput, userList } from './elements';\nimport { updateTheme, updateReplaceText } from './updateUi';\nimport renderList from './renderList';\n\nexport default function optionsUi(data) {\n const theme = data.theme || 'cats';\n const replaceText = !!data.replaceText;\n const regex = new RegExp(/user-list-remove/);\n let users = data.users || [];\n\n themeSelect.value = theme;\n replaceTextInput.checked = replaceText;\n renderList(users);\n\n themeSelect.addEventListener('change', updateTheme);\n replaceTextInput.addEventListener('change', updateReplaceText);\n\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n\n const user = userInput.value.trim();\n\n if (user && !~users.indexOf(user)) {\n users.push(user);\n chrome.storage.local.set({ users });\n renderList(users);\n userInput.value = '';\n }\n });\n\n // handle removals\n userList.addEventListener('click', (e) => {\n const t = e.target;\n\n if (t.className.match(regex)) {\n const user = t.parentNode.getAttribute('data-user');\n\n users = users.filter(u => u !== user);\n chrome.storage.local.set({ users });\n renderList(users);\n }\n });\n}\n",
23 | "import { themeSelect, replaceTextInput } from './elements';\n\nexport function updateTheme() {\n const theme = themeSelect.options[theme.selectedIndex].value;\n\n chrome.storage.local.set({theme});\n}\n\nexport function updateReplaceText() {\n const replaceText = !!replaceTextInput.checked;\n\n chrome.storage.local.set({replaceText});\n}\n"
24 | ]
25 | }
--------------------------------------------------------------------------------
/lib/content.js:
--------------------------------------------------------------------------------
1 | import 'babel/polyfill';
2 | import gittenify from './content/gittenify';
3 |
4 | chrome.storage.local.get(null, gittenify);
5 |
--------------------------------------------------------------------------------
/lib/content/catPhoto.js:
--------------------------------------------------------------------------------
1 | export default function catPhoto(n, w, h) {
2 | return `https://placekitten.com/${w}/${h}?image=${n}`;
3 | }
4 |
--------------------------------------------------------------------------------
/lib/content/gittenify.js:
--------------------------------------------------------------------------------
1 | import catNames from 'cat-names';
2 | import mutationSummary from 'mutation-summary';
3 | import catPhoto from './catPhoto';
4 | import replaceUsers from './replaceUsers';
5 |
6 | export default function gittenify(store) {
7 | const userMap = {};
8 |
9 | // Eslint seems to lint this incorrectly so
10 | // I am turning off eslint for that one line.
11 | /* eslint-disable */
12 | for (let u of store.users) {
13 | /* eslint-enable */
14 | const n = Math.floor(Math.random() * 15) + 1;
15 | const avatar = catPhoto.bind(null, n);
16 |
17 | userMap[u] = {
18 | name: catNames.random(),
19 | avatar,
20 | };
21 | }
22 |
23 | replaceUsers(document, store, userMap);
24 |
25 | const pjax = document.querySelector('[data-pjax-container]');
26 |
27 | if (pjax) {
28 | new mutationSummary({rootNode: pjax, queries: [{all: true}], callback: (summaries) => {
29 | const summary = summaries[0];
30 | summary.added.forEach((el) => {
31 | if (el.parentNode === pjax && el.querySelectorAll) {
32 | replaceUsers(el, store, userMap);
33 | }
34 | });
35 | }});
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/content/replaceUsers.js:
--------------------------------------------------------------------------------
1 | import lorem from 'lorem-ipsum';
2 | import { catWords, containerSelector, authorSelector, avatarSelector, commentSelector } from './selectors';
3 |
4 | export default function replaceUsers(el, store, userMap) {
5 | const containers = el.querySelectorAll(containerSelector);
6 |
7 | [...containers].forEach((container) => {
8 | const author = container.querySelectorAll(authorSelector)[0];
9 | const avatar = container.querySelectorAll(avatarSelector)[0];
10 | const comments = container.querySelectorAll(commentSelector);
11 |
12 | if (!author) return;
13 |
14 | const username = author.textContent || author.getAttribute('aria-label');
15 |
16 | if (username in userMap) {
17 | const uData = userMap[username];
18 |
19 | author.textContent = uData.name;
20 |
21 | if (avatar) {
22 | avatar.src = uData.avatar(avatar.width, avatar.height);
23 | }
24 |
25 | if (comments && store.replaceText) {
26 | [...comments].forEach((comment) => {
27 | comment.textContent = lorem({units: 'paragraphs', words: catWords});
28 | });
29 | }
30 | }
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/lib/content/selectors.js:
--------------------------------------------------------------------------------
1 | export const catWords = [
2 | 'meooow',
3 | 'meow',
4 | 'scratching post',
5 | 'naww',
6 | 'purrr',
7 | 'meeeeow',
8 | 'kibbles',
9 | 'cuddle',
10 | 'snuggle',
11 | 'purr',
12 | 'nyaaa',
13 | 'nyan',
14 | ];
15 |
16 | // Selectors.
17 | export const containerSelector = [
18 | '.js-comment-container',
19 | '.issue-meta',
20 | '.gh-header-meta',
21 | ].join(', ');
22 |
23 | export const authorSelector = [
24 | '.author',
25 | '.author-name > a',
26 | '.opened-by > a',
27 | ].join(', ');
28 |
29 | export const avatarSelector = [
30 | '.avatar',
31 | '.timeline-comment-avatar',
32 | ].join(', ');
33 |
34 | export const commentSelector = [
35 | '.comment-body > p',
36 | '.comment-body .email-fragment',
37 | ].join(', ');
38 |
--------------------------------------------------------------------------------
/lib/options.js:
--------------------------------------------------------------------------------
1 | import optionsUi from './options/ui';
2 |
3 | chrome.storage.local.get(null, optionsUi);
4 |
--------------------------------------------------------------------------------
/lib/options/elements.js:
--------------------------------------------------------------------------------
1 | export const themeSelect = document.getElementById('theme');
2 | export const replaceTextInput = document.getElementById('replace-text');
3 | export const form = document.getElementById('add-form');
4 | export const userInput = document.getElementById('user-input');
5 | export const userList = document.getElementById('user-list');
6 |
--------------------------------------------------------------------------------
/lib/options/itemForUser.js:
--------------------------------------------------------------------------------
1 | export default function itemForUser(user) {
2 | const close = document.createElement('div');
3 | const li = document.createElement('li');
4 |
5 | close.classList.add('user-list-remove');
6 | close.textContent = 'x';
7 |
8 | li.classList.add('user-list-item');
9 | li.setAttribute('data-user', user);
10 | li.textContent = user;
11 | li.appendChild(close);
12 |
13 | return li;
14 | }
15 |
--------------------------------------------------------------------------------
/lib/options/renderList.js:
--------------------------------------------------------------------------------
1 | import { userList } from './elements';
2 | import itemForUser from './itemForUser';
3 |
4 | export default function renderList(users) {
5 | userList.innerHTML = '';
6 | users.forEach((user) => {
7 | userList.appendChild(itemForUser(user));
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/lib/options/ui.js:
--------------------------------------------------------------------------------
1 | import { themeSelect, replaceTextInput, form, userInput, userList } from './elements';
2 | import { updateTheme, updateReplaceText } from './updateUi';
3 | import renderList from './renderList';
4 |
5 | export default function optionsUi(data) {
6 | const theme = data.theme || 'cats';
7 | const replaceText = !!data.replaceText;
8 | const regex = new RegExp(/user-list-remove/);
9 | let users = data.users || [];
10 |
11 | themeSelect.value = theme;
12 | replaceTextInput.checked = replaceText;
13 | renderList(users);
14 |
15 | themeSelect.addEventListener('change', updateTheme);
16 | replaceTextInput.addEventListener('change', updateReplaceText);
17 |
18 | form.addEventListener('submit', (e) => {
19 | e.preventDefault();
20 |
21 | const user = userInput.value.trim();
22 |
23 | if (user && !~users.indexOf(user)) {
24 | users.push(user);
25 | chrome.storage.local.set({ users });
26 | renderList(users);
27 | userInput.value = '';
28 | }
29 | });
30 |
31 | // handle removals
32 | userList.addEventListener('click', (e) => {
33 | const t = e.target;
34 |
35 | if (t.className.match(regex)) {
36 | const user = t.parentNode.getAttribute('data-user');
37 |
38 | users = users.filter(u => u !== user);
39 | chrome.storage.local.set({ users });
40 | renderList(users);
41 | }
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/lib/options/updateUi.js:
--------------------------------------------------------------------------------
1 | import { themeSelect, replaceTextInput } from './elements';
2 |
3 | export function updateTheme() {
4 | const theme = themeSelect.options[theme.selectedIndex].value;
5 |
6 | chrome.storage.local.set({theme});
7 | }
8 |
9 | export function updateReplaceText() {
10 | const replaceText = !!replaceTextInput.checked;
11 |
12 | chrome.storage.local.set({replaceText});
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gittens",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "build": "npm run build-options && npm run build-content",
7 | "build-dev": "npm run build-options-dev && npm run build-content-dev",
8 | "start": "npm run watch-options & npm run watch-content",
9 | "build-options": "esnow -e ./lib/options.js -o dist/ -p",
10 | "build-options-dev": "esnow -e ./lib/options.js -o dist/",
11 | "watch-options": "esnow -e ./lib/options.js -o dist/ -w",
12 | "build-content": "esnow -e ./lib/content.js -o dist/ -p",
13 | "build-content-dev": "esnow -e ./lib/content.js -o dist/",
14 | "watch-content": "esnow -e ./lib/content.js -o dist/ -w"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "MIT",
19 | "devDependencies": {
20 | "babel": "^5.8.23",
21 | "babel-eslint": "^4.1.1",
22 | "eslint": "^1.3.1",
23 | "eslint-config-airbnb": "0.0.8",
24 | "esnow": "^2.0.1"
25 | },
26 | "dependencies": {
27 | "cat-names": "^1.0.2",
28 | "inserted-component": "0.0.2",
29 | "lorem-ipsum": "^1.0.3",
30 | "mutation-summary": "0.0.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------