diff --git a/emoji-substitution/README.md b/emoji-substitution/README.md new file mode 100644 index 0000000..cf98baa --- /dev/null +++ b/emoji-substitution/README.md @@ -0,0 +1,9 @@ +# Emoji Substitution + +## What it does + +Replaces words that describe an emoji with the emoji itself. + +## What it shows + +A good example for beginners that can be used as a "make your first add-on" tutorial and / or referenced to create other add-ons. \ No newline at end of file diff --git a/emoji-substitution/emojiMap.js b/emoji-substitution/emojiMap.js new file mode 100644 index 0000000..16213cd --- /dev/null +++ b/emoji-substitution/emojiMap.js @@ -0,0 +1,115 @@ +/* + * This file contains the Map of word --> emoji substitutions. + */ + +let dictionary = new Map(); +dictionary.set('apple', '🍎'); +dictionary.set('banana', '🍌'); +dictionary.set('bang', '💥'); +dictionary.set('baseball', '⚾'); +dictionary.set('basketball', '🏀'); +dictionary.set('beer', '🍺'); +dictionary.set('bicycle', '🚴'); +dictionary.set('bike', '🚴'); +dictionary.set('bomb', '💣'); +dictionary.set('boy', '👦'); +dictionary.set('bug', '🐛'); +dictionary.set('burger', '🍔'); +dictionary.set('burn', '🔥'); +dictionary.set('cake', '🎂'); +dictionary.set('candy', '🍬'); +dictionary.set('cat', '🐱'); +dictionary.set('celebration', '🎉'); +dictionary.set('cheeseburger', '🍔'); +dictionary.set('cookie', '🍪'); +dictionary.set('cool', '😎'); +dictionary.set('cry', '😢'); +dictionary.set('dog', '🐶'); +dictionary.set('doge', '🐕'); +dictionary.set('earth', '🌎'); +dictionary.set('explode', '💥'); +dictionary.set('fart', '💨'); +dictionary.set('fast', '💨'); +dictionary.set('female', '👩'); +dictionary.set('fire', '🔥'); +dictionary.set('fish', '🐟'); +dictionary.set('flame', '🔥'); +dictionary.set('flower', '🌹'); +dictionary.set('food', '🍕'); +dictionary.set('football', '🏈'); +dictionary.set('girl', '👧'); +dictionary.set('golf', '⛳'); +dictionary.set('hamburger', '🍔'); +dictionary.set('happy', '😀'); +dictionary.set('horse', '🐴'); +dictionary.set('hot', '🔥'); +dictionary.set('kiss', '😘'); +dictionary.set('laugh', '😂'); +dictionary.set('lit', '🔥'); +dictionary.set('lock', '🔒'); +dictionary.set('lol', '😂'); +dictionary.set('love', '😍'); +dictionary.set('male', '👨'); +dictionary.set('man', '👨'); +dictionary.set('monkey', '🐵'); +dictionary.set('moon', '🌙'); +dictionary.set('note', '📝'); +dictionary.set('paint', '🎨'); +dictionary.set('panda', '🐼'); +dictionary.set('party', '🎉'); +dictionary.set('pig', '🐷'); +dictionary.set('pizza', '🍕'); +dictionary.set('planet', '🌎'); +dictionary.set('rose', '🌹'); +dictionary.set('sad', '😢'); +dictionary.set('sleep', '😴'); +dictionary.set('smile', '😀'); +dictionary.set('smiley', '😀'); +dictionary.set('soccer', '⚽'); +dictionary.set('star', '⭐'); +dictionary.set('sun', '☀️'); +dictionary.set('sunglasses', '😎'); +dictionary.set('surprised', '😮'); +dictionary.set('tree', '🌲'); +dictionary.set('trophy', '🏆'); +dictionary.set('win', '🏆'); +dictionary.set('wind', '💨'); +dictionary.set('wine', '🍷'); +dictionary.set('wink', '😉'); +dictionary.set('woman', '👩'); +dictionary.set('world', '🌎'); +dictionary.set('wow', '😮'); + +/* + * After all the dictionary entries have been set, sort them by length. + * + * Because iteration over Maps happens by insertion order, this avoids + * scenarios where words that are substrings of other words get substituted + * first, leading to the longer word's substitution never triggering. + * + * For example, the 'woman' substitution would never get triggered + * if the 'man' substitution happens first because the input term 'woman' + * would become 'wo👨', and the search for 'woman' would not find any matches. + */ +let tempArray = Array.from(dictionary); +tempArray.sort((pair1, pair2) => { + // Each pair is an array with two entries: a word, and its emoji. + // Ex: ['woman', '👩'] + const firstWord = pair1[0]; + const secondWord = pair2[0]; + + if (firstWord.length > secondWord.length) { + // The first word should come before the second word. + return -1; + } + if (secondWord.length > firstWord.length) { + // The second word should come before the first word. + return 1; + } + + // The words have the same length, it doesn't matter which comes first. + return 0; +}); + +// Now that the entries are sorted, put them back into a Map. +let sortedEmojiMap = new Map(tempArray); diff --git a/emoji-substitution/icons/icon.png b/emoji-substitution/icons/icon.png new file mode 100644 index 0000000..b35102a Binary files /dev/null and b/emoji-substitution/icons/icon.png differ diff --git a/emoji-substitution/icons/icon@2x.png b/emoji-substitution/icons/icon@2x.png new file mode 100644 index 0000000..a5acdb1 Binary files /dev/null and b/emoji-substitution/icons/icon@2x.png differ diff --git a/emoji-substitution/manifest.json b/emoji-substitution/manifest.json new file mode 100644 index 0000000..c5dbeeb --- /dev/null +++ b/emoji-substitution/manifest.json @@ -0,0 +1,24 @@ +{ + "manifest_version": 2, + "applications": { + "gecko": { + "strict_min_version": "49.0.1" + } + }, + + "name": "Emoji Substitution", + "description": "Replaces words with emojis.", + "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/emoji-substitution", + "version": "1.0", + "icons": { + "48": "icons/icon.png", + "96": "icons/icon@2x.png" + }, + + "content_scripts": [ + { + "matches": [""], + "js": ["./emojiMap.js", "./substitute.js"] + } + ] +} diff --git a/emoji-substitution/substitute.js b/emoji-substitution/substitute.js new file mode 100644 index 0000000..c886063 --- /dev/null +++ b/emoji-substitution/substitute.js @@ -0,0 +1,93 @@ +/* + * This file is responsible for performing the logic of replacing + * all occurrences of each mapped word with its emoji counterpart. + */ + +// emojiMap.js defines the 'sortedEmojiMap' variable. +// Referenced here to reduce confusion. +const emojiMap = sortedEmojiMap; + +/* + * For efficiency, create a word --> search RegEx Map too. + */ +let regexs = new Map(); +for (let word of emojiMap.keys()) { + // We want a global, case-insensitive replacement. + // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp + regexs.set(word, new RegExp(word, 'gi')); +} + +/** + * Substitutes emojis into text nodes. + * If the node contains more than just text (ex: it has child nodes), + * call replaceText() on each of its children. + * + * @param {Node} node - The target DOM Node. + * @return {void} - Note: the emoji substitution is done inline. + */ +function replaceText (node) { + // Setting textContent on a node removes all of its children and replaces + // them with a single text node. Since we don't want to alter the DOM aside + // from substituting text, we only substitute on single text nodes. + // @see https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent + if (node.nodeType === Node.TEXT_NODE) { + // This node only contains text. + // @see https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType. + + // Skip textarea nodes due to the potential for accidental submission + // of substituted emoji where none was intended. + if (node.parentNode && + node.parentNode.nodeName === 'TEXTAREA') { + return; + } + + // Because DOM manipulation is slow, we don't want to keep setting + // textContent after every replacement. Instead, manipulate a copy of + // this string outside of the DOM and then perform the manipulation + // once, at the end. + let content = node.textContent; + + // Replace every occurrence of 'word' in 'content' with its emoji. + // Use the emojiMap for replacements. + for (let [word, emoji] of emojiMap) { + // Grab the search regex for this word. + const regex = regexs.get(word); + + // Actually do the replacement / substitution. + // Note: if 'word' does not appear in 'content', nothing happens. + content = content.replace(regex, emoji); + } + + // Now that all the replacements are done, perform the DOM manipulation. + node.textContent = content; + } + else { + // This node contains more than just text, call replaceText() on each + // of its children. + for (let i = 0; i < node.childNodes.length; i++) { + replaceText(node.childNodes[i]); + } + } +} + +// Start the recursion from the body tag. +replaceText(document.body); + +// Now monitor the DOM for additions and substitute emoji into new nodes. +// @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver. +const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.addedNodes && mutation.addedNodes.length > 0) { + // This DOM change was new nodes being added. Run our substitution + // algorithm on each newly added node. + for (let i = 0; i < mutation.addedNodes.length; i++) { + const newNode = mutation.addedNodes[i]; + replaceText(newNode); + } + } + }); +}); +observer.observe(document.body, { + childList: true, + subtree: true +}); diff --git a/examples.json b/examples.json index 02e3a83..5adaeb5 100644 --- a/examples.json +++ b/examples.json @@ -207,6 +207,15 @@ } ] }, + { + "name": "Emoji Substitution", + "description": "Replaces words with emojis.", + "url": "https://github.com/mdn/webextensions-examples/tree/master/emoji-substitution", + "manifest_keys": [ + "content_scripts" + ], + "javascript_modules": [] + }, { "name": "favourite-colour", "description": "An example options ui",