├── LICENSE ├── README.md ├── copy_plain_text_1 ├── background.js ├── content-script.js ├── icon │ └── icon.svg └── manifest.json ├── copy_plain_text_2 ├── background.js ├── content-script.js ├── icon │ └── icon.svg ├── manifest.json ├── options.html └── options.js ├── copy_plain_text_3 ├── _locales │ ├── en │ │ └── messages.json │ └── zh_TW │ │ └── messages.json ├── background.js ├── content-script.js ├── icon │ └── icon.svg ├── manifest.json ├── options.html └── options.js ├── hello_world ├── background.js ├── content-script.js └── manifest.json ├── quickdraw_helper ├── background.js ├── content-script.js ├── google-translate.js ├── icon │ └── icon.png └── manifest.json ├── rich_text_to_plain_text ├── background.js ├── content-script.js ├── icon │ └── icon.svg └── manifest.json └── tiny_url ├── background.js ├── content-script.js ├── icon ├── icon-white.svg └── icon.svg └── manifest.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ettoolong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebExtensionsWorkshop 2 | Example for WebExtensionsWorkshop 3 | -------------------------------------------------------------------------------- /copy_plain_text_1/background.js: -------------------------------------------------------------------------------- 1 | browser.contextMenus.create({ 2 | type: "normal", 3 | title: "Copy Plain Text", 4 | contexts: ["selection"], 5 | onclick: (info, tab) => { 6 | if(tab) { 7 | browser.tabs.sendMessage(tab.id, {action: "copyPlainText"}); 8 | } 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /copy_plain_text_1/content-script.js: -------------------------------------------------------------------------------- 1 | function onCopy(event) { 2 | document.removeEventListener("copy", onCopy); 3 | const transfer = event.clipboardData; 4 | transfer.clearData(); 5 | transfer.setData("text/plain", window.getSelection().toString()); 6 | event.preventDefault(); 7 | }; 8 | 9 | browser.runtime.onMessage.addListener( request => { 10 | if (request.action === "copyPlainText") { 11 | document.addEventListener("copy", onCopy); 12 | document.execCommand("copy"); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /copy_plain_text_1/icon/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 72 | 75 | 79 | 80 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /copy_plain_text_1/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Copies text without formatting.", 3 | "manifest_version": 2, 4 | "name": "Copy Plain Text", 5 | "version": "1.0.0", 6 | 7 | "icons": { 8 | "48": "icon/icon.svg" 9 | }, 10 | 11 | "applications": { 12 | "gecko": { 13 | "id": "@copyplaintext", 14 | "strict_min_version": "52.0" 15 | } 16 | }, 17 | 18 | "author": "Ett Chung", 19 | 20 | "background": { 21 | "scripts": ["background.js"] 22 | }, 23 | 24 | "content_scripts": [ 25 | { 26 | "matches": [""], 27 | "js": ["content-script.js"], 28 | "run_at": "document_start", 29 | "all_frames": false 30 | } 31 | ], 32 | 33 | "permissions": [ 34 | "", 35 | "contextMenus", 36 | "clipboardWrite" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /copy_plain_text_2/background.js: -------------------------------------------------------------------------------- 1 | browser.contextMenus.create({ 2 | type: "normal", 3 | title: "Copy Plain Text", 4 | contexts: ["selection"], 5 | onclick: (info, tab) => { 6 | if(tab) { 7 | browser.tabs.sendMessage(tab.id, {action: "copyPlainText"}); 8 | } 9 | } 10 | }); 11 | 12 | let defaultPreference = { 13 | trimSpace: true, 14 | removeEmptyLine: true, 15 | version: 1 16 | }; 17 | 18 | const loadPreference = () => { 19 | browser.storage.local.get().then(results => { 20 | if ((typeof results.length === "number") && (results.length > 0)) { 21 | results = results[0]; 22 | } 23 | if (!results.version) { 24 | browser.storage.local.set(defaultPreference); 25 | } 26 | }); 27 | }; 28 | 29 | window.addEventListener("DOMContentLoaded", event => { 30 | loadPreference(); 31 | }); 32 | -------------------------------------------------------------------------------- /copy_plain_text_2/content-script.js: -------------------------------------------------------------------------------- 1 | let preferences = {}; 2 | function onCopy(event) { 3 | document.removeEventListener("copy", onCopy); 4 | let text = window.getSelection().toString(); 5 | if(preferences.trimSpace) { 6 | text = text.replace(/^[ \t\f]+|[ \t\f]+$/gm, ""); 7 | } 8 | if(preferences.removeEmptyLine) { 9 | text = text.replace(/[\n\r]+/g, "\n"); 10 | text = text.replace(/\n$/,''); 11 | } 12 | const transfer = event.clipboardData; 13 | transfer.clearData(); 14 | transfer.setData("text/plain", text); 15 | event.preventDefault(); 16 | }; 17 | 18 | browser.runtime.onMessage.addListener( request => { 19 | if (request.action === "copyPlainText") { 20 | 21 | browser.storage.local.get().then(results => { 22 | if ((typeof results.length === "number") && (results.length > 0)) { 23 | results = results[0]; 24 | } 25 | preferences = results; 26 | document.addEventListener("copy", onCopy); 27 | document.execCommand("copy"); 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /copy_plain_text_2/icon/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 72 | 75 | 79 | 80 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /copy_plain_text_2/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Copies text without formatting.", 3 | "manifest_version": 2, 4 | "name": "Copy Plain Text", 5 | "version": "1.0.1", 6 | 7 | "icons": { 8 | "48": "icon/icon.svg" 9 | }, 10 | 11 | "applications": { 12 | "gecko": { 13 | "id": "@copyplaintext", 14 | "strict_min_version": "52.0" 15 | } 16 | }, 17 | 18 | "author": "Ett Chung", 19 | 20 | "background": { 21 | "scripts": ["background.js"] 22 | }, 23 | 24 | "content_scripts": [ 25 | { 26 | "matches": [""], 27 | "js": ["content-script.js"], 28 | "run_at": "document_start", 29 | "all_frames": false 30 | } 31 | ], 32 | 33 | "permissions": [ 34 | "", 35 | "contextMenus", 36 | "clipboardWrite", 37 | "storage" 38 | ], 39 | 40 | "options_ui": { 41 | "page": "options.html", 42 | "open_in_tab": false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /copy_plain_text_2/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Preferences 7 | 8 | 9 | Preferences: 10 | 11 | 12 | 13 | 14 | Trim space around text 15 | 16 | 17 | 18 | Remove empty line 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /copy_plain_text_2/options.js: -------------------------------------------------------------------------------- 1 | let currentPrefs = {}; 2 | 3 | const saveToPreference = (id, value) => { 4 | let update = {}; 5 | update[id] = value; 6 | browser.storage.local.set(update); 7 | } 8 | 9 | const handleVelueChange = id => { 10 | let elem = document.getElementById(id); 11 | if(elem) { 12 | let elemType = elem.getAttribute("type"); 13 | if(elemType === "checkbox") { 14 | elem.addEventListener("input", event => { 15 | saveToPreference(id, elem.checked ? true : false); 16 | }); 17 | } 18 | } 19 | } 20 | 21 | const setValueToElem = (id, value) => { 22 | let elem = document.getElementById(id); 23 | if(elem) { 24 | let elemType = elem.getAttribute("type"); 25 | if(elemType === "checkbox") { 26 | elem.checked = value; 27 | } 28 | } 29 | } 30 | 31 | const init = preferences => { 32 | currentPrefs = preferences; 33 | for(let p in preferences) { 34 | setValueToElem(p, preferences[p]); 35 | handleVelueChange(p); 36 | } 37 | } 38 | 39 | window.addEventListener("load", event => { 40 | browser.storage.local.get().then(results => { 41 | if ((typeof results.length === "number") && (results.length > 0)) { 42 | results = results[0]; 43 | } 44 | if (results.version) { 45 | init(results); 46 | } 47 | }); 48 | }, true); 49 | -------------------------------------------------------------------------------- /copy_plain_text_3/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "Copy Plain Text", 4 | "description": "" 5 | }, 6 | "extDescription": { 7 | "message": "Copies text without formatting.", 8 | "description": "" 9 | }, 10 | "preferencesLabel": { 11 | "message": "Preferences:", 12 | "description": "" 13 | }, 14 | "trimSpaceLabel": { 15 | "message": "Trim space around text", 16 | "description": "" 17 | }, 18 | "removeEmptyLineLabel": { 19 | "message": "Remove empty line", 20 | "description": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /copy_plain_text_3/_locales/zh_TW/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "Copy Plain Text", 4 | "description": "" 5 | }, 6 | "extDescription": { 7 | "message": "複製選取的內容為無格式文字。", 8 | "description": "" 9 | }, 10 | "preferencesLabel": { 11 | "message": "偏好設定:", 12 | "description": "" 13 | }, 14 | "trimSpaceLabel": { 15 | "message": "移除每行文字前後的空白", 16 | "description": "" 17 | }, 18 | "removeEmptyLineLabel": { 19 | "message": "移除多餘的換行", 20 | "description": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /copy_plain_text_3/background.js: -------------------------------------------------------------------------------- 1 | browser.contextMenus.create({ 2 | type: "normal", 3 | title: "Copy Plain Text", 4 | contexts: ["selection"], 5 | onclick: (info, tab) => { 6 | if(tab) { 7 | browser.tabs.sendMessage(tab.id, {action: "copyPlainText"}); 8 | } 9 | } 10 | }); 11 | 12 | let defaultPreference = { 13 | trimSpace: true, 14 | removeEmptyLine: true, 15 | version: 1 16 | }; 17 | 18 | const loadPreference = () => { 19 | browser.storage.local.get().then(results => { 20 | if ((typeof results.length === "number") && (results.length > 0)) { 21 | results = results[0]; 22 | } 23 | if (!results.version) { 24 | browser.storage.local.set(defaultPreference); 25 | } 26 | }); 27 | }; 28 | 29 | window.addEventListener("DOMContentLoaded", event => { 30 | loadPreference(); 31 | }); 32 | -------------------------------------------------------------------------------- /copy_plain_text_3/content-script.js: -------------------------------------------------------------------------------- 1 | let preferences = {}; 2 | function onCopy(event) { 3 | document.removeEventListener("copy", onCopy); 4 | let text = window.getSelection().toString(); 5 | if(preferences.trimSpace) { 6 | text = text.replace(/^[ \t\f]+|[ \t\f]+$/gm, ""); 7 | } 8 | if(preferences.removeEmptyLine) { 9 | text = text.replace(/[\n\r]+/g, "\n"); 10 | text = text.replace(/\n$/,''); 11 | } 12 | const transfer = event.clipboardData; 13 | transfer.clearData(); 14 | transfer.setData("text/plain", text); 15 | event.preventDefault(); 16 | }; 17 | 18 | browser.runtime.onMessage.addListener( request => { 19 | if (request.action === "copyPlainText") { 20 | 21 | browser.storage.local.get().then(results => { 22 | if ((typeof results.length === "number") && (results.length > 0)) { 23 | results = results[0]; 24 | } 25 | preferences = results; 26 | document.addEventListener("copy", onCopy); 27 | document.execCommand("copy"); 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /copy_plain_text_3/icon/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 72 | 75 | 79 | 80 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /copy_plain_text_3/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "__MSG_extDescription__", 3 | "manifest_version": 2, 4 | "name": "__MSG_extName__", 5 | "version": "1.0.2", 6 | 7 | "icons": { 8 | "48": "icon/icon.svg" 9 | }, 10 | 11 | "applications": { 12 | "gecko": { 13 | "id": "@copyplaintext", 14 | "strict_min_version": "52.0" 15 | } 16 | }, 17 | 18 | "author": "Ett Chung", 19 | 20 | "background": { 21 | "scripts": ["background.js"] 22 | }, 23 | 24 | "content_scripts": [ 25 | { 26 | "matches": [""], 27 | "js": ["content-script.js"], 28 | "run_at": "document_start", 29 | "all_frames": false 30 | } 31 | ], 32 | 33 | "permissions": [ 34 | "", 35 | "contextMenus", 36 | "clipboardWrite", 37 | "storage" 38 | ], 39 | 40 | "options_ui": { 41 | "page": "options.html", 42 | "open_in_tab": false 43 | }, 44 | 45 | "default_locale": "en" 46 | } 47 | -------------------------------------------------------------------------------- /copy_plain_text_3/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Preferences 7 | 8 | 9 | Preferences: 10 | 11 | 12 | 13 | 14 | Trim space around text 15 | 16 | 17 | 18 | Remove empty line 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /copy_plain_text_3/options.js: -------------------------------------------------------------------------------- 1 | let currentPrefs = {}; 2 | 3 | const saveToPreference = (id, value) => { 4 | let update = {}; 5 | update[id] = value; 6 | browser.storage.local.set(update); 7 | } 8 | 9 | const handleVelueChange = id => { 10 | let elem = document.getElementById(id); 11 | if(elem) { 12 | let elemType = elem.getAttribute("type"); 13 | if(elemType === "checkbox") { 14 | elem.addEventListener("input", event => { 15 | saveToPreference(id, elem.checked ? true : false); 16 | }); 17 | } 18 | } 19 | } 20 | 21 | const setValueToElem = (id, value) => { 22 | let elem = document.getElementById(id); 23 | if(elem) { 24 | let elemType = elem.getAttribute("type"); 25 | if(elemType === "checkbox") { 26 | elem.checked = value; 27 | } 28 | } 29 | } 30 | 31 | const init = preferences => { 32 | currentPrefs = preferences; 33 | for(let p in preferences) { 34 | setValueToElem(p, preferences[p]); 35 | handleVelueChange(p); 36 | } 37 | let l10nTags = Array.from(document.querySelectorAll('[data-l10n-id]')); 38 | l10nTags.forEach(tag => { 39 | tag.textContent = browser.i18n.getMessage(tag.getAttribute('data-l10n-id')); 40 | }); 41 | } 42 | 43 | window.addEventListener("load", event => { 44 | browser.storage.local.get().then(results => { 45 | if ((typeof results.length === "number") && (results.length > 0)) { 46 | results = results[0]; 47 | } 48 | if (results.version) { 49 | init(results); 50 | } 51 | }); 52 | }, true); 53 | -------------------------------------------------------------------------------- /hello_world/background.js: -------------------------------------------------------------------------------- 1 | browser.runtime.onMessage.addListener(message => { 2 | console.log(message.data); 3 | }); 4 | -------------------------------------------------------------------------------- /hello_world/content-script.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("click", event => { 2 | browser.runtime.sendMessage({data: document.title + ": hello world"}); 3 | }); 4 | -------------------------------------------------------------------------------- /hello_world/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "My first WebExtension", 3 | "manifest_version": 2, 4 | "name": "Hello World", 5 | "version": "1.0.0", 6 | 7 | "applications": { 8 | "gecko": { 9 | "id": "helloWorld@ettoolong", 10 | "strict_min_version": "48.0" 11 | } 12 | }, 13 | 14 | "author": "Ett Chung", 15 | 16 | "background": { 17 | "scripts": ["background.js"] 18 | }, 19 | 20 | "content_scripts": [ 21 | { 22 | "matches": [""], 23 | "js": [ 24 | "content-script.js" 25 | ], 26 | "run_at": "document_end", 27 | "all_frames": false 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /quickdraw_helper/background.js: -------------------------------------------------------------------------------- 1 | let cache = {}; 2 | let cacheList = []; 3 | 4 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 5 | if(message.action === "challengetext") { 6 | //sendResponse({action: "setText", text: "#" + message.text + "#"}); 7 | if(cache[message.text] === undefined) { 8 | cache[message.text] = ""; 9 | translate("en", "zh-TW", message.text, res => { 10 | cache[message.text] = res.translation; 11 | cacheList.push(message.text); 12 | if(cacheList.length>100) { 13 | let oldWord = cacheList.shift(); 14 | if(cache[oldWord]) { 15 | delete cache[oldWord]; 16 | } 17 | } 18 | sendResponse({action: "setText", text: res.translation}); 19 | }); 20 | return true; 21 | } 22 | else if(cache[message.text] === "") { 23 | //console.log("ignore"); 24 | } 25 | else { 26 | //console.log('use cache: ' + msg.text + ' => ' + cache[msg.text]); 27 | sendResponse({action: "setText", text: cache[message.text]}); 28 | return true; 29 | } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /quickdraw_helper/content-script.js: -------------------------------------------------------------------------------- 1 | const insertAfter = (newNode, referenceNode) => { 2 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); 3 | }; 4 | 5 | const cleanup = () => { 6 | let node = document.getElementById("translation-word"); 7 | if(node) { 8 | node.parentNode.removeChild(node); 9 | } 10 | }; 11 | 12 | // configuration of the observer: 13 | let config = { childList: true }; 14 | let target = document.getElementById("challengetext-word"); 15 | let observer = new MutationObserver(function(mutations) { 16 | mutations.forEach(function(mutation) { 17 | browser.runtime.sendMessage({action: "challengetext", text: mutation.target.textContent}).then(response => { 18 | //console.log(JSON.stringify(response, null, 4)); 19 | if(response) { 20 | cleanup(); 21 | let textNode = document.createTextNode(response.text); 22 | let node = document.createElement("span"); 23 | node.setAttribute("id", "translation-word"); 24 | node.appendChild(textNode); 25 | insertAfter(node, target); 26 | } 27 | }); 28 | }); 29 | }); 30 | // pass in the target node, as well as the observer options 31 | observer.observe(target, config); 32 | -------------------------------------------------------------------------------- /quickdraw_helper/google-translate.js: -------------------------------------------------------------------------------- 1 | // source code from another addon: 2 | // gtranslate 0.13.1, https://addons.mozilla.org/zh-TW/firefox/addon/gtranslate/ 3 | 4 | /* global require,exports,console */ 5 | 'use strict' 6 | 7 | // const request = require('sdk/request').Request 8 | 9 | function translationResult(str, onError) { 10 | let newstr = '[' 11 | let insideQuote = false 12 | str = str.replace(/\\(?=[^u])/g, '\\') 13 | 14 | // Fix the Google Translate JSON 15 | // start at 1, take into acount opening brace 16 | for (let i = 1, q = 0, len = str.length, prev; i < len; i++) { 17 | prev = str[i - 1] 18 | if (str[i] === '"' && prev !== '\\') { 19 | q++ 20 | } 21 | insideQuote = q % 2 !== 0 22 | if (!insideQuote && str[i] === ',' && (prev === ',' || prev === '[' )) { 23 | newstr += '""' 24 | } 25 | newstr += str[i] 26 | } 27 | 28 | let result = [null, null, null] 29 | let parseError = false 30 | try { 31 | result = JSON.parse(newstr) 32 | } catch (e) { 33 | if (onError) { 34 | onError(newstr) 35 | } 36 | parseError = true 37 | } 38 | 39 | const translation = parseError ? 'Google Translate Service Error' : ( 40 | result[0] && result[0].map(chunk => chunk[0]).join(' ') 41 | ) || null 42 | 43 | const alternatives = ( 44 | result[1] && result[1].map(chunk => ( 45 | chunk[0] + ':\n ' + chunk[2].map(chunk => ( 46 | chunk[0] + ': ' + Array( 47 | (10 - chunk[0].length) > 0 ? 10 - chunk[0].length : 0 48 | ).join(' ') + '\t' + chunk[1].join(', ') 49 | )).join('\n ') 50 | )).join('\n\n') 51 | ) || null 52 | 53 | const dict = ( 54 | result[12] && result[12].map(chunk => ( 55 | chunk[0] + ' \n ' + chunk[1].map(chunky => ( 56 | chunky[0] + ' \n "' + chunky[2] + '"' 57 | )).join(' \n ') 58 | )).join('\n\n') 59 | ) || null 60 | 61 | const syno = ( 62 | result[11] && result[11].map(chunk => ( 63 | chunk[0] + ' \n ' + chunk[1].map(chunky => ( 64 | chunky[0].join(', ') 65 | )).join(' \n ') 66 | )).join('\n\n') 67 | ) || null 68 | 69 | return { 70 | detectedSource: result[2], 71 | translation: translation ? translation.trim() : null, 72 | alternatives: alternatives ? alternatives.trim() : null, 73 | dictionary: dict ? dict.trim() : null, 74 | synonyms: syno ? syno.trim() : null, 75 | } 76 | } 77 | 78 | // Some sort of token google uses 79 | function generateToken(a) { 80 | //at first sight seems to be a constant, but couldn't easily find how it was generated. May change. 81 | var b = 406394 82 | //text to utf8 codepoints 83 | for (var d = [], e = 0, f = 0; f < a.length; f++) { 84 | var g = a.charCodeAt(f); 85 | 0x80 > g ? 86 | d[e++] = g 87 | : 88 | (0x800 > g ? 89 | d[e++] = g >> 6 | 192 90 | : 91 | (55296 == (g & 64512) && f + 1 < a.length && 56320 == (a.charCodeAt(f + 1) & 64512) ? 92 | (g = 65536 + ((g & 1023) << 10) + (a.charCodeAt(++f) & 1023), 93 | d[e++] = g >> 18 | 240, 94 | d[e++] = g >> 12 & 0x3f | 0x80) 95 | : 96 | d[e++] = g >> 12 | 0xe0, 97 | d[e++] = g >> 6 & 0x3f | 0x80) 98 | , d[e++] = g & 0x3f | 0x80) 99 | } 100 | a = b; 101 | for (e = 0; e < d.length; e++) a += d[e], a = tokenhelper(a, "+-a^+6"); 102 | a = tokenhelper(a, "+-3^+b+-f"); 103 | a ^= 2641390264; 104 | 0 > a && (a = (a & 2147483647) + 2147483648); 105 | a %= 1E6; 106 | return (a.toString() + "." + (a ^ b)) 107 | } 108 | 109 | function tokenhelper(a, b) { 110 | for (var c = 0; c < b.length - 2; c += 3) { 111 | var d = b.charAt(c + 2), 112 | d = d >= "a" ? d.charCodeAt(0) - 87 : Number(d), 113 | d = b.charAt(c + 1) == "+" ? a >>> d : a << d; 114 | a = b.charAt(c) == "+" ? a + d & 4294967295 : a ^ d 115 | } 116 | return a 117 | } 118 | 119 | function apiUrl(from, to, text, includeText) { 120 | const protocol = 'https://' 121 | const host = 'translate.google.com' 122 | const token = generateToken(text) 123 | let path = ( 124 | `/translate_a/single?client=t&ie=UTF-8&oe=UTF-8` + 125 | `&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&dt=at&tk=` + 126 | token + `&sl=${from}&tl=${to}&hl=${to}` 127 | ) 128 | if (typeof text !== 'undefined' && includeText) { 129 | path += `&q=${encodeURIComponent(text)}` 130 | } 131 | return `${protocol}${host}${path}` 132 | } 133 | 134 | function pageUrl(from, to, text) { 135 | const protocol = 'https://' 136 | const host = 'translate.google.com' 137 | return `${protocol}${host}/#${from}/${to}/${encodeURIComponent(text)}` 138 | } 139 | 140 | function wholePageUrl(from, to, url) { 141 | const base = 'https://translate.google.com' 142 | return `${base}/translate?sl=${from}&hl=${to}&u=${encodeURIComponent(url)}` 143 | } 144 | 145 | function translate(from, to, text, cb) { 146 | const url = apiUrl(from, to, text); 147 | 148 | const onComplete = event => { 149 | const translation = translationResult(event.target.responseText, () => { 150 | console.log(`[gtranslate] parse error with ${url}`); 151 | }) 152 | return cb(translation); 153 | } 154 | 155 | const onError = err => { 156 | console.log(err); 157 | } 158 | 159 | // Far below what google's cutoff is to decide 160 | // to use get or post, but post works anyway. 161 | if (text.length < 200 ) { 162 | //console.log('text.length < 200'); 163 | let request = new XMLHttpRequest(); 164 | request.onload = onComplete; 165 | request.onerror = onError; 166 | request.open("GET", apiUrl(from, to, text, true), true); 167 | request.send(); 168 | } else { 169 | //console.log('text.length >= 200'); 170 | let request = new XMLHttpRequest(); 171 | request.onload = onComplete; 172 | request.onerror = onError; 173 | request.open("POST", apiUrl(from, to, text, false), true); 174 | request.setRequestHeader("Content-Length", 'q='.concat(encodeURIComponent(text)).length); 175 | request.send('q='.concat(encodeURIComponent(text))); 176 | } 177 | } 178 | 179 | exports.translate = translate 180 | exports.translateUrl = pageUrl 181 | exports.translatePageUrl = wholePageUrl 182 | -------------------------------------------------------------------------------- /quickdraw_helper/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ettoolong/WebExtensionsWorkshop/5a666eef10940c097437c8011b6d7d0cb7e57c89/quickdraw_helper/icon/icon.png -------------------------------------------------------------------------------- /quickdraw_helper/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Auto translate challenge text on site: https://quickdraw.withgoogle.com", 3 | "manifest_version": 2, 4 | "name": "Quickdraw helper", 5 | "version": "1.0.0", 6 | 7 | "icons": { 8 | "48": "icon/icon.png" 9 | }, 10 | 11 | "applications": { 12 | "gecko": { 13 | "id": "@quickdraw-helper", 14 | "strict_min_version": "48.0" 15 | } 16 | }, 17 | 18 | "author": "Ett Chung", 19 | 20 | "background": { 21 | "scripts": [ 22 | "background.js", 23 | "google-translate.js" 24 | ] 25 | }, 26 | 27 | "content_scripts": [ 28 | { 29 | "matches": ["https://quickdraw.withgoogle.com/*"], 30 | "js": [ 31 | "content-script.js" 32 | ], 33 | "run_at": "document_end", 34 | "all_frames": false 35 | } 36 | ], 37 | 38 | "permissions": [ 39 | "https://translate.google.com/*" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /rich_text_to_plain_text/background.js: -------------------------------------------------------------------------------- 1 | let addonPrefs = {trimSpace: true, removeEmptyLine: true}; 2 | const getClipData = callback => { 3 | let textArea = document.getElementById("clipboard"); 4 | let onPaste = event => { 5 | const transfer = event.clipboardData; 6 | let pastedData = transfer.getData("Text"); 7 | callback(pastedData); 8 | document.removeEventListener("paste", onPaste, false); 9 | } 10 | let onInput = event => { 11 | event.target.textContent = ''; 12 | event.target.removeEventListener("input", onInput, false); 13 | }; 14 | let body = document.querySelector("body"); 15 | if(!textArea) { 16 | textArea = document.createElement("textarea"); 17 | textArea.setAttribute("id", "clipboard"); 18 | textArea.setAttribute("type", "text"); 19 | textArea.setAttribute("value", ''); 20 | textArea.setAttribute("contenteditable", "true"); 21 | body.appendChild(textArea); 22 | } 23 | else { 24 | textArea.textContent = ''; 25 | } 26 | textArea.addEventListener("input", onInput, false); 27 | textArea.focus(); 28 | document.addEventListener("paste", onPaste, false); 29 | document.execCommand("Paste"); 30 | }; 31 | 32 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 33 | if(message.action === "convertToPlainText") { 34 | getClipData( text => { 35 | if(text) { 36 | if(addonPrefs.trimSpace) { 37 | text = text.replace(/^[ \t\f]+|[ \t\f]+$/gm, ""); 38 | } 39 | if(addonPrefs.removeEmptyLine) { 40 | text = text.replace(/[\n\r]+/g, "\n"); 41 | text = text.replace(/\n$/,''); 42 | } 43 | sendResponse({action: "setClipData", text: text}); 44 | } 45 | }); 46 | return true; 47 | } 48 | }); 49 | 50 | browser.browserAction.onClicked.addListener(tab => { 51 | var executing = browser.tabs.executeScript(tab.id, { 52 | file: "content-script.js", 53 | runAt: "document_end" 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /rich_text_to_plain_text/content-script.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | let plainText = ''; 3 | let div; 4 | let selection; 5 | let newRange; 6 | let ranges; 7 | 8 | function onCopy(event) { 9 | document.removeEventListener("copy", onCopy); 10 | const transfer = event.clipboardData; 11 | transfer.clearData(); 12 | transfer.setData("text/plain", plainText); 13 | event.preventDefault(); 14 | selection.removeRange(newRange); 15 | div.remove(); 16 | // restore previous selection (if any) 17 | if (ranges.length > 0) { 18 | for (var range of ranges) { 19 | selection.addRange(range); 20 | } 21 | } 22 | }; 23 | if(window.self === window.top) { //this message only handle by top window. 24 | browser.runtime.sendMessage({action: "convertToPlainText"}).then(response => { 25 | plainText = response.text; 26 | if(response) { 27 | ranges = []; 28 | selection = window.getSelection(); 29 | if (!selection.isCollapsed) { 30 | for (var i = 0; i < selection.rangeCount; i++) { 31 | ranges.push(selection.getRangeAt(i)); 32 | } 33 | } 34 | selection.removeAllRanges(); 35 | 36 | div = document.createElement("div"); 37 | div.append(new Text(plainText)); 38 | document.body.append(div); 39 | 40 | newRange = new Range(); 41 | newRange.selectNodeContents(div); 42 | 43 | selection.addRange(newRange); 44 | document.addEventListener('copy', onCopy); 45 | document.execCommand("copy"); 46 | } 47 | }); 48 | } 49 | })(); 50 | -------------------------------------------------------------------------------- /rich_text_to_plain_text/icon/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 72 | 75 | 79 | 80 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /rich_text_to_plain_text/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Convert clipboard's text to plain text.", 3 | "manifest_version": 2, 4 | "name": "Rich text to plain text", 5 | "version": "1.0.0", 6 | 7 | "icons": { 8 | "48": "icon/icon.svg" 9 | }, 10 | 11 | "applications": { 12 | "gecko": { 13 | "id": "@richtexttoplaintext", 14 | "strict_min_version": "54.0a" 15 | } 16 | }, 17 | 18 | "author": "Ett Chung", 19 | 20 | "background": { 21 | "scripts": [ 22 | "background.js" 23 | ] 24 | }, 25 | 26 | "browser_action": { 27 | "browser_style": true, 28 | "default_title": "Rich text to plain text", 29 | "default_icon": "icon/icon.svg" 30 | }, 31 | 32 | "permissions": [ 33 | "", 34 | "clipboardRead", 35 | "clipboardWrite" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /tiny_url/background.js: -------------------------------------------------------------------------------- 1 | //Create short URL 2 | const showNotification = message => { 3 | browser.notifications.create("TinyUrl", { 4 | "type": "basic", 5 | "iconUrl": "icon/icon.svg", 6 | "title": "Tiny URL", 7 | "message": message 8 | }); 9 | }; 10 | 11 | const makeShortURL = (long_url, callback) => { 12 | //Encode URL 13 | let url = encodeURIComponent(long_url); 14 | let apiUrl = "https://tinyurl.com/api-create.php?url=" + url; 15 | const onComplete = event => { 16 | let short_url = event.target.responseText; 17 | if(typeof(callback) === "function"){ 18 | callback(short_url); 19 | } 20 | // console.log("short_url = " + short_url); 21 | // console.log("long_url = " + long_url); 22 | showNotification(`${short_url} has been copied to the clipboard. Shortened from ${long_url}`); 23 | } 24 | 25 | const onError = err => { 26 | callback(""); 27 | showNotification("Short URL creation failed"); 28 | } 29 | let request = new XMLHttpRequest(); 30 | request.onload = onComplete; 31 | request.onerror = onError; 32 | request.open("GET", apiUrl, true); 33 | request.send(); 34 | }; 35 | 36 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 37 | if(message.action === "tinyUrl") { 38 | makeShortURL( message.long_url, short_url => { 39 | sendResponse({action: "setClipData", short_url: short_url}); 40 | }); 41 | return true; 42 | } 43 | }); 44 | 45 | browser.pageAction.onClicked.addListener(tab => { 46 | var executing = browser.tabs.executeScript(tab.id, { 47 | file: "content-script.js", 48 | runAt: "document_end" 49 | }); 50 | }); 51 | 52 | browser.tabs.onUpdated.addListener( (tabId, changeInfo, tab) => { 53 | browser.tabs.get(tabId).then(tab => { 54 | if(tab.url.length <= 26) { 55 | browser.pageAction.hide(tab.id); 56 | } 57 | else { 58 | browser.pageAction.show(tab.id); 59 | } 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tiny_url/content-script.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | let short_url = ''; 3 | let div; 4 | let selection; 5 | let newRange; 6 | let ranges; 7 | 8 | function onCopy(event) { 9 | document.removeEventListener("copy", onCopy); 10 | const transfer = event.clipboardData; 11 | transfer.clearData(); 12 | transfer.setData("text/plain", short_url); 13 | event.preventDefault(); 14 | selection.removeRange(newRange); 15 | div.remove(); 16 | // restore previous selection (if any) 17 | if (ranges.length > 0) { 18 | for (var range of ranges) { 19 | selection.addRange(range); 20 | } 21 | } 22 | }; 23 | if(window.self === window.top) { //this message only handle by top window. 24 | browser.runtime.sendMessage({action: "tinyUrl", long_url: document.location.href}).then(response => { 25 | short_url = response.short_url; 26 | if(response) { 27 | ranges = []; 28 | selection = window.getSelection(); 29 | if (!selection.isCollapsed) { 30 | for (var i = 0; i < selection.rangeCount; i++) { 31 | ranges.push(selection.getRangeAt(i)); 32 | } 33 | } 34 | selection.removeAllRanges(); 35 | 36 | div = document.createElement("div"); 37 | div.append(new Text(short_url)); 38 | document.body.append(div); 39 | 40 | newRange = new Range(); 41 | newRange.selectNodeContents(div); 42 | 43 | selection.addRange(newRange); 44 | document.addEventListener("copy", onCopy); 45 | document.execCommand("copy"); 46 | } 47 | }); 48 | } 49 | })(); 50 | -------------------------------------------------------------------------------- /tiny_url/icon/icon-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tiny_url/icon/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tiny_url/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Make short URL by TinyURL service.", 3 | "manifest_version": 2, 4 | "name": "Tiny Url", 5 | "version": "1.0.0", 6 | 7 | "icons": { 8 | "48": "icon/icon.svg" 9 | }, 10 | 11 | "applications": { 12 | "gecko": { 13 | "id": "@tinyurl", 14 | "strict_min_version": "52.0" 15 | } 16 | }, 17 | 18 | "author": "Ett Chung", 19 | 20 | "background": { 21 | "scripts": [ 22 | "background.js" 23 | ] 24 | }, 25 | 26 | "page_action": { 27 | "browser_style": true, 28 | "default_title": "Tiny url", 29 | "default_icon": "icon/icon-white.svg" 30 | }, 31 | 32 | "permissions": [ 33 | "", 34 | "tabs", 35 | "clipboardWrite", 36 | "notifications" 37 | ] 38 | } 39 | --------------------------------------------------------------------------------