summaryrefslogtreecommitdiffstats
path: root/comm/chat/modules/imSmileys.sys.mjs
blob: 1658033786375c7bbded4f123ab2c55a8662882f (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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/* 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/. */

/** Used to add smileys to the content of a textnode. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

XPCOMUtils.defineLazyGetter(lazy, "gTextDecoder", () => {
  return new TextDecoder();
});

ChromeUtils.defineModuleGetter(
  lazy,
  "NetUtil",
  "resource://gre/modules/NetUtil.jsm"
);

var kEmoticonsThemePref = "messenger.options.emoticonsTheme";
var kThemeFile = "theme.json";

Object.defineProperty(lazy, "gTheme", {
  configurable: true,
  enumerable: true,

  get() {
    delete this.gTheme;
    gPrefObserver.init();
    return (this.gTheme = getTheme());
  },
});

var gPrefObserver = {
  init() {
    Services.prefs.addObserver(kEmoticonsThemePref, gPrefObserver);
  },

  observe(aObject, aTopic, aMsg) {
    if (aTopic != "nsPref:changed" || aMsg != kEmoticonsThemePref) {
      throw new Error("bad notification");
    }

    lazy.gTheme = getTheme();
  },
};

function getTheme(aName) {
  let name = aName || Services.prefs.getCharPref(kEmoticonsThemePref);

  let theme = {
    name,
    iconsHash: null,
    json: null,
    regExp: null,
  };

  if (name == "none") {
    return theme;
  }

  if (name == "default") {
    theme.baseUri = "chrome://instantbird-emoticons/skin/";
  } else {
    theme.baseUri = "chrome://" + theme.name + "/skin/";
  }
  try {
    let channel = Services.io.newChannel(
      theme.baseUri + kThemeFile,
      null,
      null,
      null,
      Services.scriptSecurityManager.getSystemPrincipal(),
      null,
      Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
      Ci.nsIContentPolicy.TYPE_IMAGE
    );
    let stream = channel.open();
    let bytes = lazy.NetUtil.readInputStream(stream, stream.available());
    theme.json = JSON.parse(lazy.gTextDecoder.decode(bytes));
    stream.close();
    theme.iconsHash = {};
    for (let smiley of theme.json.smileys) {
      for (let textCode of smiley.textCodes) {
        theme.iconsHash[textCode] = smiley;
      }
    }
  } catch (e) {
    console.error(e);
  }
  return theme;
}

function getRegexp() {
  if (lazy.gTheme.regExp) {
    lazy.gTheme.regExp.lastIndex = 0;
    return lazy.gTheme.regExp;
  }

  // return null if smileys are disabled
  if (!lazy.gTheme.iconsHash) {
    return null;
  }

  if ("" in lazy.gTheme.iconsHash) {
    console.error(
      "Emoticon " +
        lazy.gTheme.iconsHash[""].filename +
        " matches the empty string!"
    );
    delete lazy.gTheme.iconsHash[""];
  }

  let emoticonList = [];
  for (let emoticon in lazy.gTheme.iconsHash) {
    emoticonList.push(emoticon);
  }

  let exp = /[[\]{}()*+?.\\^$|]/g;
  emoticonList = emoticonList
    .sort()
    .reverse()
    .map(x => x.replace(exp, "\\$&"));

  if (!emoticonList.length) {
    // the theme contains no valid emoticon, make sure we will return
    // early next time
    lazy.gTheme.iconsHash = null;
    return null;
  }

  lazy.gTheme.regExp = new RegExp(emoticonList.join("|"), "g");
  return lazy.gTheme.regExp;
}

export function smileTextNode(aNode) {
  /*
   * Skip text nodes that contain the href in the child text node.
   * We must check both the testNode.textContent and the aNode.data since they
   * cover different cases:
   *   textContent: The URL is split over multiple nodes for some reason
   *   data: The URL is not the only content in the link, skip only the one node
   * Check the class name to skip any autolinked nodes from mozTXTToHTMLConv.
   */
  let testNode = aNode;
  while ((testNode = testNode.parentNode)) {
    if (
      testNode.nodeName.toLowerCase() == "a" &&
      (testNode.getAttribute("href") == testNode.textContent.trim() ||
        testNode.getAttribute("href") == aNode.data.trim() ||
        testNode.className.includes("moz-txt-link-"))
    ) {
      return 0;
    }
  }

  let result = 0;
  let exp = getRegexp();
  if (!exp) {
    return result;
  }

  let match;
  while ((match = exp.exec(aNode.data))) {
    let smileNode = aNode.splitText(match.index);
    aNode = smileNode.splitText(exp.lastIndex - match.index);
    // at this point, smileNode is a text node with only the text
    // of the smiley and aNode is a text node with the text after
    // the smiley. The text in aNode hasn't been processed yet.
    let smile = smileNode.data;
    let elt = aNode.ownerDocument.createElement("span");
    elt.appendChild(
      aNode.ownerDocument.createTextNode(lazy.gTheme.iconsHash[smile].glyph)
    );
    // Add the title attribute (to show the original text in a tooltip) in case
    // the replacement was done incorrectly.
    elt.setAttribute("title", smile);
    smileNode.parentNode.replaceChild(elt, smileNode);
    result += 2;
    exp.lastIndex = 0;
  }
  return result;
}