Imagine this: you come across a webpage that says "Just send a message to user@example.com" but to actually send the email, you need to highlight the address and then cut and paste it into the recipient field of a new compose window of your email client of choice.
Obviously life would be easier if it were simply a mailto: link, so you could click on it and have a new message created automatically. How do I build an extension that turns email addresses into clickable mailto: links?
I was originally going to ask if there was an extension to enable similar functionality for unlinked Twitter @username mentions but I thought this email address problem would be a simpler situation.
Use innerHTML
to replace the email - No, this breaks page's, especially because event listeners are removed and attributes are also replaced.
Recursively loop through all nodes:
- Loop in the reverse order, to prevent conflicts when you modify the DOM.
- For each item, check it's
nodeType
value.
- If
.nodeType === 1
(element), call the function again (recursion).
- If
.nodeType === 3
(text node):
- Use a regular expression and the
exec
method to find one email address. Use the result's .index
property to know the starting position of the email address, and result[0].length
to know the length of the address.
- Use the node's
splitText
method cut the text node in three parts.
- Create an
<a>
element.
- Append the email's text node (the second text node from the previous) to this anchor. It's automatically removed from the document.
- Insert this link before the third node.
Demo
Not a chrome extension, but it shows how a chrome extension would behave: http://jsfiddle.net/ckw89/
Chrome extension
(the regexp is based on MongoEngine's EmailField
pattern):
script.js
// Initiate recursion
wrapLink(document.body);
function wrapLink(elem) { // elem must be an element node
var nodes = elem.childNodes
, i = nodes.length
, regexp = /([-!\x23$%&'*+\/=?^_`{}|~0-9A-Z]+(\.[-!\x23$%&'*+\/=?^_`{}|~0-9A-Z]+)*|^"([\x01-\x08\x0b\x0c\x0e-\x1f!\x23-\\[\\]-\x7f]|\\[\x01-011\x0b\x0c\x0e-\x7f])*")@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}/i
, node, emailNode, a, result;
while (node = nodes[--i]) {
if (node.nodeType === 1) {
// Skip anchor tags, nested anchors make no sense
if (node.nodeName.toUpperCase() !== 'A')
wrapLink(node);
} else if (node.nodeType === 3) {
// 1: Please note that the regexp has NO global flag,
// and that `node.textContent` shrinks when an address is found
while (result = regexp.exec(node.textContent)) {
// 2: Contact <SPLIT> me@example.com for details
node = node.splitText(result.index);
// 2: Contact <SPLIT>me@example.com<SPLIT> for details
node = node.splitText(result[0].length);
// me@example.com
emailNode = node.previousSibling
// 3. Create link
a = document.createElement('a');
a.href = 'mailto:' + result[0];
// 4: Append emailNode
a.appendChild(emailNode);
// 5: Insert before
elem.insertBefore(a, node);
}
}
}
}
This script will work immediately when used as a Content script, because its only interaction with the page is the DOM. For completeness, here's the contents of the manifest.json
file:
{
"name": "Turns email addresses in `mailto:`s",
"version": "1",
"version_version": 2,
"content_scripts": [{
"matches": ["*://*/*"],
"js": ["script.js"]
}]
}
Performance notice
The current script replaces all nodes in the live document. Consider moving the root node (eg <body>
) to a document fragment before manipulating.