summaryrefslogtreecommitdiffstats
path: root/mobile/android/chrome/content/Linkify.js
blob: 3c757cc1808bbe3c272b4785969a6b432ca7f72e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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); 
  }
};