michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const LINKIFY_TIMEOUT = 0; michael@0: michael@0: function Linkifier() { michael@0: this._linkifyTimer = null; michael@0: this._phoneRegex = /(?:\s|^)[\+]?(\(?\d{1,8}\)?)?([- ]+\(?\d{1,8}\)?)+( ?(x|ext) ?\d{1,3})?(?:\s|$)/g; michael@0: } michael@0: michael@0: Linkifier.prototype = { michael@0: _buildAnchor : function(aDoc, aNumberText) { michael@0: let anchorNode = aDoc.createElement("a"); michael@0: let cleanedText = ""; michael@0: for (let i = 0; i < aNumberText.length; i++) { michael@0: let c = aNumberText.charAt(i); michael@0: if ((c >= '0' && c <= '9') || c == '+') //assuming there is only the leading '+'. michael@0: cleanedText += c; michael@0: } michael@0: anchorNode.setAttribute("href", "tel:" + cleanedText); michael@0: let nodeText = aDoc.createTextNode(aNumberText); michael@0: anchorNode.appendChild(nodeText); michael@0: return anchorNode; michael@0: }, michael@0: michael@0: _linkifyNodeNumbers : function(aNodeToProcess, aDoc) { michael@0: let parent = aNodeToProcess.parentNode; michael@0: let nodeText = aNodeToProcess.nodeValue; michael@0: michael@0: // Replacing the original text node with a sequence of michael@0: // |text before number|anchor with number|text after number nodes. michael@0: // Each step a couple of (optional) text node and anchor node are appended. michael@0: let anchorNode = null; michael@0: let m = null; michael@0: let startIndex = 0; michael@0: let prevNode = null; michael@0: while (m = this._phoneRegex.exec(nodeText)) { michael@0: anchorNode = this._buildAnchor(aDoc, nodeText.substr(m.index, m[0].length)); michael@0: michael@0: let textExistsBeforeNumber = (m.index > startIndex); michael@0: let nodeToAdd = null; michael@0: if (textExistsBeforeNumber) michael@0: nodeToAdd = aDoc.createTextNode(nodeText.substr(startIndex, m.index - startIndex)); michael@0: else michael@0: nodeToAdd = anchorNode; michael@0: michael@0: if (!prevNode) // first time, need to replace the whole node with the first new one. michael@0: parent.replaceChild(nodeToAdd, aNodeToProcess); michael@0: else michael@0: parent.insertBefore(nodeToAdd, prevNode.nextSibling); //inserts after. michael@0: michael@0: if (textExistsBeforeNumber) // if we added the text node before the anchor, we still need to add the anchor node. michael@0: parent.insertBefore(anchorNode, nodeToAdd.nextSibling); michael@0: michael@0: // next nodes need to be appended to this node. michael@0: prevNode = anchorNode; michael@0: startIndex = m.index + m[0].length; michael@0: } michael@0: michael@0: // if some text is remaining after the last anchor. michael@0: if (startIndex > 0 && startIndex < nodeText.length) { michael@0: let lastNode = aDoc.createTextNode(nodeText.substr(startIndex)); michael@0: parent.insertBefore(lastNode, prevNode.nextSibling); michael@0: return lastNode; michael@0: } michael@0: return anchorNode; michael@0: }, michael@0: michael@0: linkifyNumbers: function(aDoc) { michael@0: // Removing any installed timer in case the page has changed and a previous timer is still running. michael@0: if (this._linkifyTimer) { michael@0: clearTimeout(this._linkifyTimer); michael@0: this._linkifyTimer = null; michael@0: } michael@0: michael@0: let filterNode = function (node) { michael@0: if (node.parentNode.tagName != 'A' && michael@0: node.parentNode.tagName != 'SCRIPT' && michael@0: node.parentNode.tagName != 'NOSCRIPT' && michael@0: node.parentNode.tagName != 'STYLE' && michael@0: node.parentNode.tagName != 'APPLET' && michael@0: node.parentNode.tagName != 'TEXTAREA') michael@0: return NodeFilter.FILTER_ACCEPT; michael@0: else michael@0: return NodeFilter.FILTER_REJECT; michael@0: } michael@0: michael@0: let nodeWalker = aDoc.createTreeWalker(aDoc.body, NodeFilter.SHOW_TEXT, filterNode, false); michael@0: let parseNode = function() { michael@0: let node = nodeWalker.nextNode(); michael@0: if (!node) { michael@0: this._linkifyTimer = null; michael@0: return; michael@0: } michael@0: let lastAddedNode = this._linkifyNodeNumbers(node, aDoc); michael@0: // we assign a different timeout whether the node was processed or not. michael@0: if (lastAddedNode) { michael@0: nodeWalker.currentNode = lastAddedNode; michael@0: this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT); michael@0: } else { michael@0: this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT); michael@0: } michael@0: }.bind(this); michael@0: michael@0: this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT); michael@0: } michael@0: };