summaryrefslogtreecommitdiff
path: root/static/js/src/text.js
blob: d99efbb4fe829218df4b9302fbb5f20a8d8cac7d (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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
// this doesn't properly deal with eg, .gov.uk .co.ck etc
function parseDomain(host){
  var chunks = host.split(".")
  if (chunks.length == 1) return chunks[0]
  else return chunks[chunks.length - 2]
}

function escapeHtml(txt) {
    if (!txt) { return ""; }
    // txt = annoyingCaps(txt)
    return $("<span>").text(txt).html()
}


URLRegex = /((\b(http\:\/\/|https\:\/\/|ftp\:\/\/)|(www\.))+(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi;
PicRegex = /\.(jpg|jpeg|png|gif|bmp|svg|webp|fid)$/i;
VideoRegex = /\.(mp4|webm|mov|m4v|gifv)$/i;
RecipRegex = /(^|\s)@\w+/g;
TopicRegex = /(^|\s)#\w+/g;

function splitTrailingPunctuation(url) {
  if (!url) return { "url": url, "suffix": "" };
  // Common punctuation that follows pasted links in chat.
  var match = url.match(/[)\]\}\.,!\?;:'"]+$/);
  if (!match) return { "url": url, "suffix": "" };
  return { "url": url.slice(0, -match[0].length), "suffix": match[0] };
}

function imgurIdFromUri(uri) {
  var host = (uri.host || "").toLowerCase();
  if (!host || !host.match(/(^|\.)imgur\.com$/i)) return null;
  if (!uri.file) return null;

  var fileLower = uri.file.toLowerCase();
  if (PicRegex.test(fileLower) || VideoRegex.test(fileLower)) return null;

  if (!uri.file.match(/^[A-Za-z0-9]+$/)) return null;
  return uri.file;
}

function imgurCandidateUrls(id) {
  var base = "https://i.imgur.com/" + id;
  return [base + ".jpg", base + ".png", base + ".gif", base + ".jpeg"];
}

function imgurHotlinkFallback(img) {
  try {
    var candidates = (img.getAttribute("data-imgur-candidates") || "").split("|");
    if (!candidates.length || !candidates[0]) return;

    var idx = parseInt(img.getAttribute("data-imgur-idx") || "0", 10);
    var next = idx + 1;
    if (next >= candidates.length) {
      var link = img.parentNode;
      if (link && link.tagName == "A") {
        var href = link.getAttribute("href") || "";
        while (link.firstChild) link.removeChild(link.firstChild);
        link.appendChild(document.createTextNode(href));
      }
      return;
    }

    img.setAttribute("data-imgur-idx", "" + next);
    img.src = candidates[next];
  } catch (e) {}
}


function getImagesAsArray(text) {
  var imgs = []
  var urls = text.match(URLRegex)
  if (urls === null) return imgs
  for (var i = 0; i<urls.length; i++){
    var split = splitTrailingPunctuation(urls[i]);
    var normalized = normalizeUrl(split.url);
    var uri = parseUri(normalized);

    // Try to turn common "page" links into direct image URLs (best-effort).
    var imgurId = imgurIdFromUri(uri);
    var candidate = imgurId ? imgurCandidateUrls(imgurId)[0] : normalized;

    var urlWithoutParams = candidate.replace(/[?#].*$/i, "");
    if (PicRegex.test(urlWithoutParams)) imgs.push(candidate)
  }
  return imgs
}

function topicReplace(text) {
    text = $.trim(text).toLowerCase();
    var topicLabel = text.substring(1);
    return ' <a target="_blank" href="'+ RootDomain + 't/' + topicLabel + '">' + text + '</a> ';
}

function recipientReplace(atText, recips) {
    recips = recips || [];

    var space = '';    
    if (atText[0] == ' ') {
        atText = atText.slice(1);
        space = ' ';
    }
    
    var nick = atText.slice(1).toLowerCase();
    var matchedRecip;
    for (var i = 0; i < recips.length; i++) {
        if (recips[i].toLowerCase() == nick) {
            matchedRecip = recips[i];
            break;
        }
    }
    
    if (matchedRecip) {
        return space + '<a target="_blank" href="' + RootDomain + matchedRecip + '">@' + matchedRecip + '</a>';
    } else {
        return space + atText;
    }
}

function linkify(text, recips) {
    LastMsgContainsImage = false;
    var recipWrapper = function(text) { return recipientReplace(text, recips); };
    return text
        .replace(URLRegex, linkReplace)
        .replace(RecipRegex, recipWrapper)
        .replace(TopicRegex, topicReplace);
}

// use this in escapeHtml to turn everyone's text lIkE tHiS
function annoyingCaps(text){
  var chunks = text.split(" ")
  for(var i=0; i<chunks.length; i++){
    var chunk = chunks[i]
    if (!chunk.length || chunk.substr(0,4) == 'http') continue;
    var letters=chunk.split("")
    for(var j = 0; j<letters.length; j++){
      if (j % 2) letters[j] = letters[j].toUpperCase()
      else letters[j] = letters[j].toLowerCase()
    }
    chunks[i] = letters.join("")
  } 
  return chunks.join(" ")
}


function imgClickHandler() {
	// Ugly hack. Don't open new links in chat, only in logs.
	// Ugly hack mkII: ensure middle-click opens images in new tab
	// c.f. http://code.google.com/p/chromium/issues/detail?id=255
	if ($.browser.webkit && event.button != 0) {
		event.stopPropagation();
	} else {
		return $('#chatrap').length == 0;
	}
}

// durty hack to use a global to check this... but otherwise i'd have to rewrite the String.replace function? :/
var LastMsgContainsImage = false
function linkReplace(url) {
    var split = splitTrailingPunctuation(url);
    var hrefUrl = normalizeUrl(split.url);
    var uri = parseUri(hrefUrl);

    // Best-effort support for Imgur page links (e.g. https://imgur.com/<id>).
    var imgurId = imgurIdFromUri(uri);
    if (imgurId) {
      LastMsgContainsImage = true;
      var candidates = imgurCandidateUrls(imgurId);
      var first = candidates[0];
      return "<a target=\"_blank\" href=\"" + hrefUrl + "\" class=\"img-wrapper\" onclick=\"return imgClickHandler()\">"
             + "<img src=\"" + first + "\" class=\"unbound imgur-hotlink\" data-imgur-idx=\"0\" data-imgur-candidates=\"" + candidates.join("|") + "\" onerror=\"imgurHotlinkFallback(this)\">"
             + "</a>" + split.suffix;
    }

    linkUrl = hrefUrl;

    var type = getUriType(uri)
    
    if (type == 'image') {
      LastMsgContainsImage = true;
      return "<a target=\"_blank\" href=\"" + linkUrl + "\" class=\"img-wrapper\" onclick=\"return imgClickHandler()\"><img src=\"" + linkUrl + "\" class=\"unbound\"></a>" + split.suffix;
    } else if (type == 'video') {
      LastMsgContainsImage = true;
      var videoUrl = linkUrl.replace(/\.gifv([?#].*)?$/i, ".mp4$1");
      return "<a target=\"_blank\" href=\"" + linkUrl + "\" class=\"video-wrapper\" onclick=\"return imgClickHandler()\"><video src=\"" + videoUrl + "\" autoplay loop muted controls playsinline></video></a>" + split.suffix;
    } else if (type == 'youtube') {
      Youtube.startAnimation();
      return "<a target=\"_blank\" class=\"youtube\" href=\"" + linkUrl + "\">" +
             "<img class=\"youtube-thumb\" width=\"130\" height=\"97\" src=\"" + Youtube.nextThumbUrl(uri.queryKey.v) + "\">" +
             "<img class=\"youtube-controls\" src=\"/static/img/youtube.controls.png\"></a>" + split.suffix;
    } else if (type == 'midi') {
      return '<embed src="' + linkUrl + '" autostart="false" loop="false" volume="80" width="150" height="20" style="vertical-align:bottom"> <a href="' + linkUrl + '">' + uri.file + '</a>' + split.suffix;
    } else if (type == 'wav') {
      return '<audio src="' + linkUrl + '" controls volume="80" width="150" height="20" style="vertical-align:bottom"></audio> <a href="' + linkUrl + '">' + uri.file + '</a>' + split.suffix;
    } else
        return "<a target=\"_blank\" href=\"" + linkUrl + "\">" + split.url + "</a>" + split.suffix;
    
}


function getUriType(uri){
  if (PicRegex.test(uri.file.toLowerCase()))
    return "image";

  if (VideoRegex.test(uri.file.toLowerCase()))
    return "video";
  
  var domain = parseDomain(uri.host)
  
  if (domain == "gstatic" && uri.path == "/images" && 'q' in uri.queryKey)
    return "image"; 
  
 // actual image url = uri.queryKey['q'].split(":").slice(2).join(":") but often the original image is broken...


  if (domain == "youtube" && ('v' in uri.queryKey || uri.anchor.indexOf('v') != -1))
    return "youtube";
  
  if (uri.path.substr(-4) == ".mid" || uri.path.substr(-5) == ".midi")
    return "midi"

  if (uri.path.substr(-4) == ".wav")
    return "wav"

  
  return "link";
}

function linkifyWithoutImage(text) {
    LastMsgContainsImage = false
    return text.replace(URLRegex, linkReplaceWithoutImage);
}

function linkReplaceWithoutImage(url){
    var urlWithoutParams = url.replace(/\?.*$/i, "");
    linkUrl = normalizeUrl(url);

    return "<a target='_blank' href='" + linkUrl + "'>" + url + "</a>"
}