From a283015e652cd349eda79ddd703441ce1650a97d Mon Sep 17 00:00:00 2001 From: Federico Paolinelli Date: Tue, 20 Aug 2013 13:41:58 -0700 Subject: [PATCH] Bug 566225 - Add framework to linkify phone numbers in pages, preffed off. r=wesj --- mobile/android/app/mobile.js | 3 + mobile/android/chrome/content/Linkify.js | 108 +++++++++++++++++++++++ mobile/android/chrome/content/browser.js | 7 ++ mobile/android/chrome/jar.mn | 1 + 4 files changed, 119 insertions(+) create mode 100644 mobile/android/chrome/content/Linkify.js diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index 2662b81a599..441586cfdf9 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -777,3 +777,6 @@ pref("gfx.canvas.azure.backends", "skia"); pref("gfx.canvas.azure.accelerated", true); pref("general.useragent.override.youtube.com", "Android; Tablet;#Android; Mobile;"); + +// When true, phone number linkification is enabled. +pref("browser.ui.linkify.phone", false); diff --git a/mobile/android/chrome/content/Linkify.js b/mobile/android/chrome/content/Linkify.js new file mode 100644 index 00000000000..3c757cc1808 --- /dev/null +++ b/mobile/android/chrome/content/Linkify.js @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const LINKIFY_TIMEOUT = 0; + +function Linkifier() { + this._linkifyTimer = null; + this._phoneRegex = /(?:\s|^)[\+]?(\(?\d{1,8}\)?)?([- ]+\(?\d{1,8}\)?)+( ?(x|ext) ?\d{1,3})?(?:\s|$)/g; +} + +Linkifier.prototype = { + _buildAnchor : function(aDoc, aNumberText) { + let anchorNode = aDoc.createElement("a"); + let cleanedText = ""; + for (let i = 0; i < aNumberText.length; i++) { + let c = aNumberText.charAt(i); + if ((c >= '0' && c <= '9') || c == '+') //assuming there is only the leading '+'. + cleanedText += c; + } + anchorNode.setAttribute("href", "tel:" + cleanedText); + let nodeText = aDoc.createTextNode(aNumberText); + anchorNode.appendChild(nodeText); + return anchorNode; + }, + + _linkifyNodeNumbers : function(aNodeToProcess, aDoc) { + let parent = aNodeToProcess.parentNode; + let nodeText = aNodeToProcess.nodeValue; + + // Replacing the original text node with a sequence of + // |text before number|anchor with number|text after number nodes. + // Each step a couple of (optional) text node and anchor node are appended. + let anchorNode = null; + let m = null; + let startIndex = 0; + let prevNode = null; + while (m = this._phoneRegex.exec(nodeText)) { + anchorNode = this._buildAnchor(aDoc, nodeText.substr(m.index, m[0].length)); + + let textExistsBeforeNumber = (m.index > startIndex); + let nodeToAdd = null; + if (textExistsBeforeNumber) + nodeToAdd = aDoc.createTextNode(nodeText.substr(startIndex, m.index - startIndex)); + else + nodeToAdd = anchorNode; + + if (!prevNode) // first time, need to replace the whole node with the first new one. + parent.replaceChild(nodeToAdd, aNodeToProcess); + else + parent.insertBefore(nodeToAdd, prevNode.nextSibling); //inserts after. + + if (textExistsBeforeNumber) // if we added the text node before the anchor, we still need to add the anchor node. + parent.insertBefore(anchorNode, nodeToAdd.nextSibling); + + // next nodes need to be appended to this node. + prevNode = anchorNode; + startIndex = m.index + m[0].length; + } + + // if some text is remaining after the last anchor. + if (startIndex > 0 && startIndex < nodeText.length) { + let lastNode = aDoc.createTextNode(nodeText.substr(startIndex)); + parent.insertBefore(lastNode, prevNode.nextSibling); + return lastNode; + } + return anchorNode; + }, + + linkifyNumbers: function(aDoc) { + // Removing any installed timer in case the page has changed and a previous timer is still running. + if (this._linkifyTimer) { + clearTimeout(this._linkifyTimer); + this._linkifyTimer = null; + } + + let filterNode = function (node) { + if (node.parentNode.tagName != 'A' && + node.parentNode.tagName != 'SCRIPT' && + node.parentNode.tagName != 'NOSCRIPT' && + node.parentNode.tagName != 'STYLE' && + node.parentNode.tagName != 'APPLET' && + node.parentNode.tagName != 'TEXTAREA') + return NodeFilter.FILTER_ACCEPT; + else + return NodeFilter.FILTER_REJECT; + } + + let nodeWalker = aDoc.createTreeWalker(aDoc.body, NodeFilter.SHOW_TEXT, filterNode, false); + let parseNode = function() { + let node = nodeWalker.nextNode(); + if (!node) { + this._linkifyTimer = null; + return; + } + let lastAddedNode = this._linkifyNodeNumbers(node, aDoc); + // we assign a different timeout whether the node was processed or not. + if (lastAddedNode) { + nodeWalker.currentNode = lastAddedNode; + this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT); + } else { + this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT); + } + }.bind(this); + + this._linkifyTimer = setTimeout(parseNode, LINKIFY_TIMEOUT); + } +}; diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index c7dac897c42..9c231b444ad 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -69,6 +69,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", ["MasterPassword", "chrome://browser/content/MasterPassword.js"], ["PluginHelper", "chrome://browser/content/PluginHelper.js"], ["OfflineApps", "chrome://browser/content/OfflineApps.js"], + ["Linkifier", "chrome://browser/content/Linkify.js"], ].forEach(function (aScript) { let [name, script] = aScript; XPCOMUtils.defineLazyGetter(window, name, function() { @@ -3540,6 +3541,12 @@ Tab.prototype = { tabID: this.id }); + if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) { + if (!this._linkifier) + this._linkifier = new Linkifier(); + this._linkifier.linkifyNumbers(this.browser.contentWindow.document); + } + if (!Reader.isEnabledForParseOnLoad) return; diff --git a/mobile/android/chrome/jar.mn b/mobile/android/chrome/jar.mn index 9b7919cf0cd..d7086901a5e 100644 --- a/mobile/android/chrome/jar.mn +++ b/mobile/android/chrome/jar.mn @@ -50,6 +50,7 @@ chrome.jar: content/PermissionsHelper.js (content/PermissionsHelper.js) content/FeedHandler.js (content/FeedHandler.js) content/Feedback.js (content/Feedback.js) + content/Linkify.js (content/Linkify.js) #ifdef MOZ_SERVICES_HEALTHREPORT content/aboutHealthReport.xhtml (content/aboutHealthReport.xhtml) * content/aboutHealthReport.js (content/aboutHealthReport.js)