This repository has been archived by the owner on Sep 18, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 127
/
Autolink.java
414 lines (369 loc) · 14.2 KB
/
Autolink.java
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
package com.twitter;
import com.twitter.Extractor.Entity;
import java.util.List;
/**
* A class for adding HTML links to hashtag, username and list references in Tweet text.
*/
public class Autolink {
/** Default CSS class for auto-linked URLs */
public static final String DEFAULT_URL_CLASS = "tweet-url";
/** Default CSS class for auto-linked list URLs */
public static final String DEFAULT_LIST_CLASS = "list-slug";
/** Default CSS class for auto-linked username URLs */
public static final String DEFAULT_USERNAME_CLASS = "username";
/** Default CSS class for auto-linked hashtag URLs */
public static final String DEFAULT_HASHTAG_CLASS = "hashtag";
/** Default href for username links (the username without the @ will be appended) */
public static final String DEFAULT_USERNAME_URL_BASE = "https://twitter.com/";
/** Default href for list links (the username/list without the @ will be appended) */
public static final String DEFAULT_LIST_URL_BASE = "https://twitter.com/";
/** Default href for hashtag links (the hashtag without the # will be appended) */
public static final String DEFAULT_HASHTAG_URL_BASE = "https://twitter.com/#!/search?q=%23";
/** HTML attribute to add when noFollow is true (default) */
public static final String NO_FOLLOW_HTML_ATTRIBUTE = " rel=\"nofollow\"";
/** Default attribute for invisible span tag */
public static final String DEFAULT_INVISIBLE_TAG_ATTRS = "style='position:absolute;left:-9999px;'";
protected String urlClass;
protected String listClass;
protected String usernameClass;
protected String hashtagClass;
protected String usernameUrlBase;
protected String listUrlBase;
protected String hashtagUrlBase;
protected String invisibleTagAttrs;
protected boolean noFollow = true;
protected boolean usernameIncludeSymbol = false;
private Extractor extractor = new Extractor();
private static CharSequence escapeHTML(String text) {
StringBuilder builder = new StringBuilder(text.length() * 2);
for (char c : text.toCharArray()) {
switch(c) {
case '&': builder.append("&"); break;
case '>': builder.append(">"); break;
case '<': builder.append("<"); break;
case '"': builder.append("""); break;
case '\'': builder.append("'"); break;
default: builder.append(c); break;
}
}
return builder;
}
public Autolink() {
urlClass = DEFAULT_URL_CLASS;
listClass = DEFAULT_LIST_CLASS;
usernameClass = DEFAULT_USERNAME_CLASS;
hashtagClass = DEFAULT_HASHTAG_CLASS;
usernameUrlBase = DEFAULT_USERNAME_URL_BASE;
listUrlBase = DEFAULT_LIST_URL_BASE;
hashtagUrlBase = DEFAULT_HASHTAG_URL_BASE;
invisibleTagAttrs = DEFAULT_INVISIBLE_TAG_ATTRS;
extractor.setExtractURLWithoutProtocol(false);
}
public String escapeBrackets(String text) {
int len = text.length();
if (len == 0)
return text;
StringBuilder sb = new StringBuilder(len + 16);
for (int i = 0; i < len; ++i) {
char c = text.charAt(i);
if (c == '>')
sb.append(">");
else if (c == '<')
sb.append("<");
else
sb.append(c);
}
return sb.toString();
}
public void linkToHashtag(Entity entity, String text, StringBuilder builder) {
// Get the original hash char from text as it could be a full-width char.
CharSequence hashChar = text.subSequence(entity.getStart(), entity.getStart() + 1);
builder.append("<a href=\"").append(hashtagUrlBase).append(entity.getValue()).append("\"");
builder.append(" title=\"#").append(entity.getValue()).append("\"");
builder.append(" class=\"").append(urlClass).append(" ").append(hashtagClass).append("\"");
if (noFollow) {
builder.append(NO_FOLLOW_HTML_ATTRIBUTE);
}
builder.append(">");
builder.append(hashChar);
builder.append(entity.getValue()).append("</a>");
}
public void linkToMentionAndList(Entity entity, String text, StringBuilder builder) {
String mention = entity.getValue();
// Get the original at char from text as it could be a full-width char.
CharSequence atChar = text.subSequence(entity.getStart(), entity.getStart() + 1);
if (!usernameIncludeSymbol) {
builder.append(atChar);
}
builder.append("<a class=\"").append(urlClass).append(" ");
if (entity.listSlug != null) {
// this is list
builder.append(listClass).append("\" href=\"").append(listUrlBase);
mention += entity.listSlug;
} else {
// this is @mention
builder.append(usernameClass).append("\" href=\"").append(usernameUrlBase);
}
builder.append(mention).append("\"");
if (noFollow){
builder.append(NO_FOLLOW_HTML_ATTRIBUTE);
}
builder.append(">");
if (usernameIncludeSymbol) {
builder.append(atChar);
}
builder.append(mention).append("</a>");
}
public void linkToURL(Entity entity, String text, StringBuilder builder) {
CharSequence url = escapeHTML(entity.getValue());
CharSequence linkText = url;
if (entity.displayURL != null && entity.expandedURL != null) {
// Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
// should contain the full original URL (expanded_url), not the display URL.
//
// Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
// font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
// Elements with font-size:0 get copied even though they are not visible.
// Note that display:none doesn't work here. Elements with display:none don't get copied.
//
// Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
// wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
// everything with the tco-ellipsis class.
//
// As an example: The user tweets "hi http://longdomainname.com/foo"
// This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
// This will get rendered as:
// <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
// …
// <!-- There's a chance the onCopy event handler might not fire. In case that happens,
// we include an here so that the … doesn't bump up against the URL and ruin it.
// The is inside the tco-ellipsis span so that when the onCopy handler *does*
// fire, it doesn't get copied. Otherwise the copied text would have two spaces in a row,
// e.g. "hi http://longdomainname.com/foo".
// <span style='font-size:0'> </span>
// </span>
// <span style='font-size:0'> <!-- This stuff should get copied but not displayed -->
// http://longdomai
// </span>
// <span class='js-display-url'> <!-- This stuff should get displayed *and* copied -->
// nname.com/foo
// </span>
// <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
// <span style='font-size:0'> </span>
// …
// </span>
//
// Exception: pic.twitter.com images, for which expandedUrl = "https://twitter.com/#!/username/status/1234/photo/1
// For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
// For a pic.twitter.com URL, the only elided part will be the "https://", so this is fine.
String displayURLSansEllipses = entity.displayURL.replace("…", "");
int diplayURLIndexInExpandedURL = entity.expandedURL.indexOf(displayURLSansEllipses);
if (diplayURLIndexInExpandedURL != -1) {
String beforeDisplayURL = entity.expandedURL.substring(0, diplayURLIndexInExpandedURL);
String afterDisplayURL = entity.expandedURL.substring(diplayURLIndexInExpandedURL + displayURLSansEllipses.length());
String precedingEllipsis = entity.displayURL.startsWith("…") ? "…" : "";
String followingEllipsis = entity.displayURL.endsWith("…") ? "…" : "";
String invisibleSpan = "<span " + invisibleTagAttrs + ">";
StringBuilder sb = new StringBuilder("<span class='tco-ellipsis'>");
sb.append(precedingEllipsis);
sb.append(invisibleSpan).append(" </span></span>");
sb.append(invisibleSpan).append(escapeHTML(beforeDisplayURL)).append("</span>");
sb.append("<span class='js-display-url'>").append(escapeHTML(displayURLSansEllipses)).append("</span>");
sb.append(invisibleSpan).append(escapeHTML(afterDisplayURL)).append("</span>");
sb.append("<span class='tco-ellipsis'>").append(invisibleSpan).append(" </span>").append(followingEllipsis).append("</span>");
linkText = sb;
} else {
linkText = entity.displayURL;
}
}
builder.append("<a href=\"").append(url).append("\"");
if (noFollow){
builder.append(NO_FOLLOW_HTML_ATTRIBUTE);
}
builder.append(">").append(linkText).append("</a>");
}
public String autoLinkEntities(String text, List<Entity> entities) {
StringBuilder builder = new StringBuilder(text.length() * 2);
int beginIndex = 0;
for (Entity entity : entities) {
builder.append(text.subSequence(beginIndex, entity.start));
switch(entity.type) {
case URL:
linkToURL(entity, text, builder);
break;
case HASHTAG:
linkToHashtag(entity, text, builder);
break;
case MENTION:
linkToMentionAndList(entity, text, builder);
}
beginIndex = entity.end;
}
builder.append(text.subSequence(beginIndex, text.length()));
return builder.toString();
}
/**
* Auto-link hashtags, URLs, usernames and lists.
*
* @param text of the Tweet to auto-link
* @return text with auto-link HTML added
*/
public String autoLink(String text) {
text = escapeBrackets(text);
// extract entities
List<Entity> entities = extractor.extractEntitiesWithIndices(text);
return autoLinkEntities(text, entities);
}
/**
* Auto-link the @username and @username/list references in the provided text. Links to @username references will
* have the usernameClass CSS classes added. Links to @username/list references will have the listClass CSS class
* added.
*
* @param text of the Tweet to auto-link
* @return text with auto-link HTML added
*/
public String autoLinkUsernamesAndLists(String text) {
return autoLinkEntities(text, extractor.extractMentionsOrListsWithIndices(text));
}
/**
* Auto-link #hashtag references in the provided Tweet text. The #hashtag links will have the hashtagClass CSS class
* added.
*
* @param text of the Tweet to auto-link
* @return text with auto-link HTML added
*/
public String autoLinkHashtags(String text) {
return autoLinkEntities(text, extractor.extractHashtagsWithIndices(text));
}
/**
* Auto-link URLs in the Tweet text provided.
* <p/>
* This only auto-links URLs with protocol.
*
* @param text of the Tweet to auto-link
* @return text with auto-link HTML added
*/
public String autoLinkURLs(String text) {
return autoLinkEntities(text, extractor.extractURLsWithIndices(text));
}
/**
* @return CSS class for auto-linked URLs
*/
public String getUrlClass() {
return urlClass;
}
/**
* Set the CSS class for auto-linked URLs
*
* @param urlClass new CSS value.
*/
public void setUrlClass(String urlClass) {
this.urlClass = urlClass;
}
/**
* @return CSS class for auto-linked list URLs
*/
public String getListClass() {
return listClass;
}
/**
* Set the CSS class for auto-linked list URLs
*
* @param listClass new CSS value.
*/
public void setListClass(String listClass) {
this.listClass = listClass;
}
/**
* @return CSS class for auto-linked username URLs
*/
public String getUsernameClass() {
return usernameClass;
}
/**
* Set the CSS class for auto-linked username URLs
*
* @param usernameClass new CSS value.
*/
public void setUsernameClass(String usernameClass) {
this.usernameClass = usernameClass;
}
/**
* @return CSS class for auto-linked hashtag URLs
*/
public String getHashtagClass() {
return hashtagClass;
}
/**
* Set the CSS class for auto-linked hashtag URLs
*
* @param hashtagClass new CSS value.
*/
public void setHashtagClass(String hashtagClass) {
this.hashtagClass = hashtagClass;
}
/**
* @return the href value for username links (to which the username will be appended)
*/
public String getUsernameUrlBase() {
return usernameUrlBase;
}
/**
* Set the href base for username links.
*
* @param usernameUrlBase new href base value
*/
public void setUsernameUrlBase(String usernameUrlBase) {
this.usernameUrlBase = usernameUrlBase;
}
/**
* @return the href value for list links (to which the username/list will be appended)
*/
public String getListUrlBase() {
return listUrlBase;
}
/**
* Set the href base for list links.
*
* @param listUrlBase new href base value
*/
public void setListUrlBase(String listUrlBase) {
this.listUrlBase = listUrlBase;
}
/**
* @return the href value for hashtag links (to which the hashtag will be appended)
*/
public String getHashtagUrlBase() {
return hashtagUrlBase;
}
/**
* Set the href base for hashtag links.
*
* @param hashtagUrlBase new href base value
*/
public void setHashtagUrlBase(String hashtagUrlBase) {
this.hashtagUrlBase = hashtagUrlBase;
}
/**
* @return if the current URL links will include rel="nofollow" (true by default)
*/
public boolean isNoFollow() {
return noFollow;
}
/**
* Set if the current URL links will include rel="nofollow" (true by default)
*
* @param noFollow new noFollow value
*/
public void setNoFollow(boolean noFollow) {
this.noFollow = noFollow;
}
/**
* Set if the at mark '@' should be included in the link (false by default)
*
* @param noFollow new noFollow value
*/
public void setUsernameIncludeSymbol(boolean usernameIncludeSymbol) {
this.usernameIncludeSymbol = usernameIncludeSymbol;
}
}