Adds an Emoji Substitution example (#110)

* Adds an Emoji Substitution example

* Monitors DOM for additions after initial load

* Fixes typos

* Fixes substitution ordering bug

* <textarea> bug fix. Also removes confusion between Elements and Nodes.
This commit is contained in:
Andrew Terranova
2016-10-24 19:08:39 -05:00
committed by wbamberg
parent 6bb818e62a
commit 07375ca50a
7 changed files with 250 additions and 0 deletions

View File

@@ -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.

View File

@@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -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": ["<all_urls>"],
"js": ["./emojiMap.js", "./substitute.js"]
}
]
}

View File

@@ -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
});

View File

@@ -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",