package glaze.examples.twitter.api.text;
import glaze.examples.twitter.api.stream.Entities;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* A class for adding HTML links to hashtag, username and list references in
* Tweet text.
*/
public class Autolink
{
/** Default CSS class for auto-linked list URLs */
public static final String DEFAULT_LIST_CLASS = "tweet-url list-slug";
/** Default CSS class for auto-linked username URLs */
public static final String DEFAULT_USERNAME_CLASS = "tweet-url username";
/** Default CSS class for auto-linked hashtag URLs */
public static final String DEFAULT_HASHTAG_CLASS = "tweet-url hashtag";
/** Default CSS class for auto-linked cashtag URLs */
public static final String DEFAULT_CASHTAG_CLASS = "tweet-url cashtag";
/**
* 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";
/**
* Default href for cashtag links (the cashtag without the $ will be
* appended)
*/
public static final String DEFAULT_CASHTAG_URL_BASE = "https://twitter.com/#!/search?q=%24";
/** Default attribute for invisible span tag */
public static final String DEFAULT_INVISIBLE_TAG_ATTRS = "style='position:absolute;left:-9999px;'";
public static interface LinkAttributeModifier
{
public void modify(Entity entity, Map<String, String> attributes);
};
public static interface LinkTextModifier
{
public CharSequence modify(Entity entity, CharSequence text);
}
protected String urlClass = null;
protected String listClass;
protected String usernameClass;
protected String hashtagClass;
protected String cashtagClass;
protected String usernameUrlBase;
protected String listUrlBase;
protected String hashtagUrlBase;
protected String cashtagUrlBase;
protected String invisibleTagAttrs;
protected boolean noFollow = true;
protected boolean usernameIncludeSymbol = false;
protected String symbolTag = null;
protected String textWithSymbolTag = null;
protected String urlTarget = null;
protected LinkAttributeModifier linkAttributeModifier = null;
protected LinkTextModifier linkTextModifier = null;
private static CharSequence escapeHTML(CharSequence text)
{
StringBuilder builder = new StringBuilder(text.length() * 2);
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
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 = null;
listClass = DEFAULT_LIST_CLASS;
usernameClass = DEFAULT_USERNAME_CLASS;
hashtagClass = DEFAULT_HASHTAG_CLASS;
cashtagClass = DEFAULT_CASHTAG_CLASS;
usernameUrlBase = DEFAULT_USERNAME_URL_BASE;
listUrlBase = DEFAULT_LIST_URL_BASE;
hashtagUrlBase = DEFAULT_HASHTAG_URL_BASE;
cashtagUrlBase = DEFAULT_CASHTAG_URL_BASE;
invisibleTagAttrs = DEFAULT_INVISIBLE_TAG_ATTRS;
}
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 linkToText(Entity entity, CharSequence text, Map<String, String> attributes, StringBuilder builder)
{
if (noFollow) {
attributes.put("rel", "nofollow");
}
if (linkAttributeModifier != null) {
linkAttributeModifier.modify(entity, attributes);
}
if (linkTextModifier != null) {
text = linkTextModifier.modify(entity, text);
}
// append <a> tag
builder.append("<a");
for (Map.Entry<String, String> entry : attributes.entrySet()) {
builder.append(" ").append(escapeHTML(entry.getKey())).append("=\"").append(escapeHTML(entry.getValue())).append("\"");
}
builder.append(">").append(text).append("</a>");
}
private static final Pattern AT_SIGNS = Pattern.compile("[@\uFF20]");
public void linkToTextWithSymbol(Entity entity, CharSequence symbol, CharSequence text,
Map<String, String> attributes, StringBuilder builder)
{
CharSequence taggedSymbol = symbolTag == null || symbolTag.isEmpty() ? symbol
: String.format("<%s>%s</%s>", symbolTag, symbol, symbolTag);
text = escapeHTML(text);
CharSequence taggedText = textWithSymbolTag == null || textWithSymbolTag.isEmpty() ? text
: String.format("<%s>%s</%s>", textWithSymbolTag, text, textWithSymbolTag);
boolean includeSymbol = usernameIncludeSymbol || !AT_SIGNS.matcher(symbol).matches();
if (includeSymbol) {
linkToText(entity, taggedSymbol.toString() + taggedText, attributes, builder);
} else {
builder.append(taggedSymbol);
linkToText(entity, taggedText, attributes, builder);
}
}
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);
CharSequence hashtag = entity.getValue();
Map<String, String> attrs = new LinkedHashMap<String, String>();
attrs.put("href", hashtagUrlBase + hashtag);
attrs.put("title", "#" + hashtag);
attrs.put("class", hashtagClass);
linkToTextWithSymbol(entity, hashChar, hashtag, attrs, builder);
}
public void linkToCashtag(Entity entity, String text, StringBuilder builder)
{
CharSequence cashtag = entity.getValue();
Map<String, String> attrs = new LinkedHashMap<String, String>();
attrs.put("href", cashtagUrlBase + cashtag);
attrs.put("title", "$" + cashtag);
attrs.put("class", cashtagClass);
linkToTextWithSymbol(entity, "$", cashtag, attrs, builder);
}
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);
Map<String, String> attrs = new LinkedHashMap<String, String>();
if (entity.listSlug != null) {
mention += entity.listSlug;
attrs.put("class", listClass);
attrs.put("href", listUrlBase + mention);
} else {
attrs.put("class", usernameClass);
attrs.put("href", usernameUrlBase + mention);
}
linkToTextWithSymbol(entity, atChar, mention, attrs, builder);
}
public void linkToURL(Entity entity, String text, StringBuilder builder)
{
CharSequence url = entity.getValue();
CharSequence linkText = escapeHTML(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;
}
}
Map<String, String> attrs = new LinkedHashMap<String, String>();
attrs.put("href", url.toString());
if (urlClass != null) {
attrs.put("class", urlClass);
}
if (urlClass != null && !urlClass.isEmpty()) {
attrs.put("class", urlClass);
}
if (urlTarget != null && !urlTarget.isEmpty()) {
attrs.put("target", urlTarget);
}
linkToText(entity, linkText, attrs, builder);
}
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);
break;
case CASHTAG:
linkToCashtag(entity, text, builder);
break;
}
beginIndex = entity.end;
}
builder.append(text.subSequence(beginIndex, text.length()));
return builder.toString();
}
/**
* @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 CSS class for auto-linked cashtag URLs
*/
public String getCashtagClass()
{
return cashtagClass;
}
/**
* Set the CSS class for auto-linked cashtag URLs
*
* @param cashtagClass
* new CSS value.
*/
public void setCashtagClass(String cashtagClass)
{
this.cashtagClass = cashtagClass;
}
/**
* @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 the href value for cashtag links (to which the cashtag will be
* appended)
*/
public String getCashtagUrlBase()
{
return cashtagUrlBase;
}
/**
* Set the href base for cashtag links.
*
* @param cashtagUrlBase
* new href base value
*/
public void setCashtagUrlBase(String cashtagUrlBase)
{
this.cashtagUrlBase = cashtagUrlBase;
}
/**
* @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;
}
/**
* Set HTML tag to be applied around #/@/# symbols in
* hashtags/usernames/lists/cashtag
*
* @param tag
* HTML tag without bracket. e.g., "b" or "s"
*/
public void setSymbolTag(String tag)
{
this.symbolTag = tag;
}
/**
* Set HTML tag to be applied around text part of
* hashtags/usernames/lists/cashtag
*
* @param tag
* HTML tag without bracket. e.g., "b" or "s"
*/
public void setTextWithSymbolTag(String tag)
{
this.textWithSymbolTag = tag;
}
/**
* Set the value of the target attribute in auto-linked URLs
*
* @param target
* target value e.g., "_blank"
*/
public void setUrlTarget(String target)
{
this.urlTarget = target;
}
/**
* Set a modifier to modify attributes of a link based on an entity
*
* @param modifier
* LinkAttributeModifier instance
*/
public void setLinkAttributeModifier(LinkAttributeModifier modifier)
{
this.linkAttributeModifier = modifier;
}
/**
* Set a modifier to modify text of a link based on an entity
*
* @param modifier
* LinkTextModifier instance
*/
public void setLinkTextModifier(LinkTextModifier modifier)
{
this.linkTextModifier = modifier;
}
public String autoLinkEntities(String text, Entities entities)
{
return autoLinkEntities(text, entities.asList());
}
}