Skip to content
This repository has been archived by the owner on Sep 18, 2021. It is now read-only.

Commit

Permalink
Refactor twttr.txt.autoLinkEntities()
Browse files Browse the repository at this point in the history
  • Loading branch information
keita committed Feb 23, 2012
1 parent 53e88be commit 4b665b1
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 120 deletions.
11 changes: 10 additions & 1 deletion test/conformance.html
Expand Up @@ -119,6 +119,8 @@
}


var expected = document.createElement("div");
var result = document.createElement("div");
for (var suite in cases) {
(function(suite) {
module(suite);
Expand All @@ -128,7 +130,14 @@
var tester = getTester(suite, section);
test(section, function() {
for (var testCase in cases[suite][section]) {
same(tester(cases[suite][section][testCase]), cases[suite][section][testCase].expected, cases[suite][section][testCase].description);
if (suite == "autolink") {
// compare HTML tags w/o checking attribute order
result.innerHTML = tester(cases[suite][section][testCase]);
expected.innerHTML = cases[suite][section][testCase].expected;
ok(result.isEqualNode(expected), cases[suite][section][testCase].description);
} else {
same(tester(cases[suite][section][testCase]), cases[suite][section][testCase].expected, cases[suite][section][testCase].description);
}
}
});
}(section));
Expand Down
25 changes: 15 additions & 10 deletions test/tests.js
Expand Up @@ -98,22 +98,27 @@ test("twttr.txt.extract", function() {
});

test("twttr.txt.autolink", function() {
var expected = document.createElement("div");
var result = document.createElement("div");

// Username Overrides
ok(twttr.txt.autoLink("@tw", { before: "!" }).match(/!@<a[^>]+>tw<\/a>/), "Override before");
ok(twttr.txt.autoLink("@tw", { at: "!" }).match(/!<a[^>]+>tw<\/a>/), "Override at");
ok(twttr.txt.autoLink("@tw", { preChunk: "<b>" }).match(/@<a[^>]+><b>tw<\/a>/), "Override preChunk");
ok(twttr.txt.autoLink("@tw", { postChunk: "</b>" }).match(/@<a[^>]+>tw<\/b><\/a>/), "Override postChunk");
same(twttr.txt.autoLink("@tw", { usernameIncludeSymbol: true }), "<a class=\"tweet-url username\" data-screen-name=\"tw\" href=\"https://twitter.com/tw\" rel=\"nofollow\">@tw</a>",
"Include @ in the autolinked username");
expected.innerHTML = "<a class=\"tweet-url username\" data-screen-name=\"tw\" href=\"https://twitter.com/tw\" rel=\"nofollow\">@tw</a>";
result.innerHTML = twttr.txt.autoLink("@tw", { usernameIncludeSymbol: true });
ok(expected.isEqualNode(result), "Include @ in the autolinked username");
ok(!twttr.txt.autoLink("foo http://example.com", { usernameClass: 'custom-user' }).match(/custom-user/), "Override usernameClass should not be applied to URL");

// List Overrides
ok(twttr.txt.autoLink("@tw/somelist", { before: "!" }).match(/!@<a[^>]+>tw\/somelist<\/a>/), "Override list before");
ok(twttr.txt.autoLink("@tw/somelist", { at: "!" }).match(/!<a[^>]+>tw\/somelist<\/a>/), "Override list at");
ok(twttr.txt.autoLink("@tw/somelist", { preChunk: "<b>" }).match(/@<a[^>]+><b>tw\/somelist<\/a>/), "Override list preChunk");
ok(twttr.txt.autoLink("@tw/somelist", { postChunk: "</b>" }).match(/@<a[^>]+>tw\/somelist<\/b><\/a>/), "Override list postChunk");
same(twttr.txt.autoLink("@tw/somelist", { usernameIncludeSymbol: true }), "<a class=\"tweet-url list-slug\" href=\"https://twitter.com/tw/somelist\" rel=\"nofollow\">@tw/somelist</a>",
"Include @ in the autolinked list");
expected.innerHTML = "<a class=\"tweet-url list-slug\" href=\"https://twitter.com/tw/somelist\" rel=\"nofollow\">@tw/somelist</a>";
result.innerHTML = twttr.txt.autoLink("@tw/somelist", { usernameIncludeSymbol: true });
ok(expected.isEqualNode(result), "Include @ in the autolinked list");
ok(twttr.txt.autoLink("foo @tw/somelist", { listClass: 'custom-list' }).match(/custom-list/), "Override listClass");
ok(!twttr.txt.autoLink("foo @tw/somelist", { usernameClass: 'custom-user' }).match(/custom-user/), "Override usernameClass should not be applied to a List");

Expand All @@ -135,7 +140,7 @@ test("twttr.txt.autolink", function() {
]
}]
});
ok(autoLinkResult.match(/<a href="http:\/\/t.co\/0JG5Mcq"[^>]+>/), 'Use t.co URL as link target');
ok(autoLinkResult.match(/href="http:\/\/t.co\/0JG5Mcq"/), 'Use t.co URL as link target');
ok(autoLinkResult.match(/>blog.twitter.com\/2011\/05\/twitte.*…</), 'Use display url from url entities');
ok(autoLinkResult.match(/r-for-mac-update.html</), 'Include the tail of expanded_url');
ok(autoLinkResult.match(/>http:\/\//), 'Include the head of expanded_url');
Expand All @@ -151,17 +156,17 @@ test("twttr.txt.autolink", function() {

// urls with invalid character
var invalidChars = ['\u202A', '\u202B', '\u202C', '\u202D', '\u202E'];
for (i = 0; i < invalidChars.length; i++) {
for (var i = 0; i < invalidChars.length; i++) {
equal(twttr.txt.extractUrls("http://twitt" + invalidChars[i] + "er.com").length, 0, 'Should not extract URL with invalid character');
}

same(twttr.txt.autoLink("\uD801\uDC00 #hashtag \uD801\uDC00 @mention \uD801\uDC00 http://twitter.com"),
"\uD801\uDC00 <a href=\"https://twitter.com/#!/search?q=%23hashtag\" title=\"#hashtag\" class=\"tweet-url hashtag\" rel=\"nofollow\">#hashtag</a> \uD801\uDC00 @<a class=\"tweet-url username\" data-screen-name=\"mention\" href=\"https://twitter.com/mention\" rel=\"nofollow\">mention</a> \uD801\uDC00 <a href=\"http://twitter.com\" rel=\"nofollow\" >http://twitter.com</a>",
"Autolink hashtag/mentionURL w/ Supplementary character");
expected.innerHTML = "\uD801\uDC00 <a class=\"tweet-url hashtag\" href=\"https://twitter.com/#!/search?q=%23hashtag\" rel=\"nofollow\" title=\"#hashtag\">#hashtag</a> \uD801\uDC00 @<a class=\"tweet-url username\" data-screen-name=\"mention\" href=\"https://twitter.com/mention\" rel=\"nofollow\">mention</a> \uD801\uDC00 <a href=\"http://twitter.com\" rel=\"nofollow\">http://twitter.com</a>";
result.innerHTML = twttr.txt.autoLink("\uD801\uDC00 #hashtag \uD801\uDC00 @mention \uD801\uDC00 http://twitter.com");
ok(expected.isEqualNode(result), "Autolink hashtag/mentionURL w/ Supplementary character");
});

test("twttr.txt.extractMentionsOrListsWithIndices", function() {
var invalid_chars = ['!', '@', '#', '$', '%', '&', '*']
var invalid_chars = ['!', '@', '#', '$', '%', '&', '*'];

for (var i = 0; i < invalid_chars.length; i++) {
c = invalid_chars[i];
Expand Down
221 changes: 112 additions & 109 deletions twitter-text.js
Expand Up @@ -326,14 +326,13 @@ if (typeof twttr === "undefined" || twttr === null) {
var DEFAULT_USERNAME_CLASS = "username";
// Default CSS class for auto-linked hashtags (along with the url class)
var DEFAULT_HASHTAG_CLASS = "hashtag";
// HTML attribute for robot nofollow behavior (default)
var HTML_ATTR_NO_FOLLOW = " rel=\"nofollow\"";
// Options which should not be passed as HTML attributes
var OPTIONS_NOT_ATTRIBUTES = {'urlClass':true, 'listClass':true, 'usernameClass':true, 'hashtagClass':true,
'usernameUrlBase':true, 'listUrlBase':true, 'hashtagUrlBase':true,
'usernameUrlBlock':true, 'listUrlBlock':true, 'hashtagUrlBlock':true, 'linkUrlBlock':true,
'usernameIncludeSymbol':true, 'suppressLists':true, 'suppressNoFollow':true,
'suppressDataScreenName':true, 'urlEntities':true, 'before':true
'suppressDataScreenName':true, 'urlEntities':true, 'before':true,
'preChunk':true, 'postChunk':true, 'preText':true, 'postText':true
};
var BOOLEAN_ATTRIBUTES = {'disabled':true, 'readonly':true, 'multiple':true, 'checked':true};

Expand All @@ -349,62 +348,32 @@ if (typeof twttr === "undefined" || twttr === null) {
return r;
}

twttr.txt.autoLinkEntities = function(text, entities, options) {
options = clone(options || {});

if (!options.suppressNoFollow) {
options.rel = "nofollow";
}
if (options.urlClass) {
options["class"] = options.urlClass;
}
options.urlClass = options.urlClass || DEFAULT_URL_CLASS;
options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS;
options.hashtagUrlBase = options.hashtagUrlBase || "https://twitter.com/#!/search?q=%23";
options.urlClass = options.urlClass || DEFAULT_URL_CLASS;
options.listClass = options.listClass || DEFAULT_LIST_CLASS;
options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS;
options.usernameUrlBase = options.usernameUrlBase || "https://twitter.com/";
options.listUrlBase = options.listUrlBase || "https://twitter.com/";
options.before = options.before || "";
var extraHtml = options.suppressNoFollow ? "" : HTML_ATTR_NO_FOLLOW;

// remap url entities to hash
var urlEntities, i, len;
if(options.urlEntities) {
urlEntities = {};
for(i = 0, len = options.urlEntities.length; i < len; i++) {
urlEntities[options.urlEntities[i].url] = options.urlEntities[i];
}
twttr.txt.linkTo = function(text, attrs, options) {
var result = "<a";
for (var key in attrs) {
result += " " + twttr.txt.htmlEscape(key) + "=\"" + twttr.txt.htmlEscape(attrs[key]) + "\"";
}
result += ">" + (options.preChunk || "") + text + (options.postChunk || "") + "</a>";
return result;
};

var result = "";
var beginIndex = 0;
var htmlAttrs = null;

for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
result += text.substring(beginIndex, entity.indices[0]);

var replaceStr;
if (entity.url) {
if (htmlAttrs == null) {
htmlAttrs = twttr.txt.htmlAttrForOptions(options);
}

var url = entity.url;
var displayUrl = url;
var linkText = twttr.txt.htmlEscape(displayUrl);
// If the caller passed a urlEntities object (provided by a Twitter API
// response with include_entities=true), we use that to render the display_url
// for each URL instead of it's underlying t.co URL.
if (urlEntities && urlEntities[url] && urlEntities[url].display_url) {
var displayUrl = urlEntities[url].display_url;
var expandedUrl = urlEntities[url].expanded_url;
twttr.txt.linkToURL = function(entity, text, options) {
var url = entity.url;
var displayUrl = entity.url;
var linkText = twttr.txt.htmlEscape(displayUrl);

// If the caller passed a urlEntities object (provided by a Twitter API
// response with include_entities=true), we use that to render the display_url
// for each URL instead of it's underlying t.co URL.
if (options.urlEntities) {
for (var i = 0; i < options.urlEntities.length; i++) {
var urlEntity = options.urlEntities[i];
if (urlEntity.url === url) {
var displayUrl = urlEntity.display_url;
var expandedUrl = urlEntity.expanded_url;
if (!options.title) {
options.title = expandedUrl;
options.htmlAttrs.title = expandedUrl;
}

// 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.
//
Expand Down Expand Up @@ -436,7 +405,7 @@ if (typeof twttr === "undefined" || twttr === null) {
afterDisplayUrl: expandedUrl.substr(displayUrlIndex + displayUrlSansEllipses.length),
precedingEllipsis: displayUrl.match(/^…/) ? "…" : "",
followingEllipsis: displayUrl.match(/…$/) ? "…" : ""
}
};
$.each(v, function(index, value) {
v[index] = twttr.txt.htmlEscape(value);
});
Expand All @@ -463,78 +432,113 @@ if (typeof twttr === "undefined" || twttr === null) {
// …
// </span>
v['invisible'] = "style='font-size:0; line-height:0'";
linkText = stringSupplant("<span class='tco-ellipsis'>#{precedingEllipsis}<span #{invisible}>&nbsp;</span></span><span #{invisible}>#{beforeDisplayUrl}</span><span class='js-display-url'>#{displayUrlSansEllipses}</span><span #{invisible}>#{afterDisplayUrl}</span><span class='tco-ellipsis'><span #{invisible}>&nbsp;</span>#{followingEllipsis}</span>", v);
linkText = stringSupplant("<span class='tco-ellipsis'>#{precedingEllipsis}<span #{invisible}>&nbsp;</span></span>" +
"<span #{invisible}>#{beforeDisplayUrl}</span><span class='js-display-url'>#{displayUrlSansEllipses}</span>" +
"<span #{invisible}>#{afterDisplayUrl}</span><span class='tco-ellipsis'><span #{invisible}>&nbsp;</span>#{followingEllipsis}</span>", v);
}
}
}
}

var d = {
htmlAttrs: htmlAttrs,
url: twttr.txt.htmlEscape(url),
linkText: linkText
};
var htmlAttrs = clone(options.htmlAttrs || {});
if (options.urlClass != DEFAULT_URL_CLASS) {
htmlAttrs["class"] = options.urlClass;
}
htmlAttrs.href = url;

replaceStr = stringSupplant("<a href=\"#{url}\"#{htmlAttrs}>#{linkText}</a>", d);
} else if (entity.hashtag) {
var d = {
hash: text.substring(entity.indices[0], entity.indices[0] + 1),
preText: "",
text: twttr.txt.htmlEscape(entity.hashtag),
postText: "",
extraHtml: extraHtml
};
for (var k in options) {
if (options.hasOwnProperty(k)) {
d[k] = options[k];
}
}
return twttr.txt.linkTo(linkText, htmlAttrs, options);
};

replaceStr = stringSupplant("#{before}<a href=\"#{hashtagUrlBase}#{text}\" title=\"##{text}\" class=\"#{urlClass} #{hashtagClass}\"#{extraHtml}>#{hash}#{preText}#{text}#{postText}</a>", d);
} else if(entity.screenName) {
var at = text.substring(entity.indices[0], entity.indices[0] + 1);
var d = {
at: options.usernameIncludeSymbol ? "" : at,
at_before_user: options.usernameIncludeSymbol ? at : "",
user: twttr.txt.htmlEscape(entity.screenName),
slashListname: twttr.txt.htmlEscape(entity.listSlug),
extraHtml: extraHtml,
preChunk: "",
postChunk: ""
};
for (var k in options) {
if (options.hasOwnProperty(k)) {
d[k] = options[k];
}
}
twttr.txt.linkToHashtag = function(entity, text, options) {
var hash = options.hash || text.substring(entity.indices[0], entity.indices[0] + 1);
var hashtag = hash + (options.preText || "") + twttr.txt.htmlEscape(entity.hashtag) + (options.postText || "");

if (entity.listSlug && !options.suppressLists) {
// the link is a list
var list = d.chunk = stringSupplant("#{user}#{slashListname}", d);
d.list = twttr.txt.htmlEscape(list.toLowerCase());
replaceStr = stringSupplant("#{before}#{at}<a class=\"#{urlClass} #{listClass}\" href=\"#{listUrlBase}#{list}\"#{extraHtml}>#{preChunk}#{at_before_user}#{chunk}#{postChunk}</a>", d);
} else {
// this is a screen name
d.chunk = d.user;
d.dataScreenName = !options.suppressDataScreenName ? stringSupplant("data-screen-name=\"#{chunk}\" ", d) : "";
replaceStr = stringSupplant("#{before}#{at}<a class=\"#{urlClass} #{usernameClass}\" #{dataScreenName}href=\"#{usernameUrlBase}#{chunk}\"#{extraHtml}>#{preChunk}#{at_before_user}#{chunk}#{postChunk}</a>", d);
}
var htmlAttrs = clone(options.htmlAttrs || {});
htmlAttrs["class"] = options.urlClass + " " + options.hashtagClass;
htmlAttrs.title = "#" + entity.hashtag;
htmlAttrs.href = options.hashtagUrlBase + entity.hashtag;

return options.before + twttr.txt.linkTo(hashtag, htmlAttrs, options);
};

twttr.txt.linkToUsernameAndList = function(entity, text, options) {
var name = entity.screenName + entity.listSlug;
var at = text.substring(entity.indices[0], entity.indices[0] + 1);
var atBeforeUser = "";
if (options.usernameIncludeSymbol) {
atBeforeUser = at;
at = "";
}

var atmention = atBeforeUser + name;
var htmlAttrs = clone(options.htmlAttrs || {});
var href, cls;
if (entity.listSlug && !options.suppressLists) {
// the link is a list
href = options.listUrlBase + name.toLowerCase();
cls = options.urlClass + " " + options.listClass;
} else {
// this is a screen name
href = options.usernameUrlBase + name;
cls = options.urlClass + " " + options.usernameClass;
if (!options.suppressDataScreenName) {
htmlAttrs["data-screen-name"] = name;
}
}

htmlAttrs["class"] = cls;
htmlAttrs.href = href;

return options.before + (options.at || at) + twttr.txt.linkTo(atmention, htmlAttrs, options);
};

twttr.txt.autoLinkEntities = function(text, entities, options) {
options = clone(options || {});

options.urlClass = options.urlClass || DEFAULT_URL_CLASS;
options.hashtagClass = options.hashtagClass || DEFAULT_HASHTAG_CLASS;
options.hashtagUrlBase = options.hashtagUrlBase || "https://twitter.com/#!/search?q=%23";
options.urlClass = options.urlClass || DEFAULT_URL_CLASS;
options.listClass = options.listClass || DEFAULT_LIST_CLASS;
options.usernameClass = options.usernameClass || DEFAULT_USERNAME_CLASS;
options.usernameUrlBase = options.usernameUrlBase || "https://twitter.com/";
options.listUrlBase = options.listUrlBase || "https://twitter.com/";
options.before = options.before || "";

options.htmlAttrs = twttr.txt.htmlAttrsFromOptions(options);
if (!options.suppressNoFollow) {
options.htmlAttrs.rel = "nofollow";
}

var result = "";
var beginIndex = 0;
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
result += text.substring(beginIndex, entity.indices[0]);

if (entity.url) {
result += twttr.txt.linkToURL(entity, text, options);
} else if (entity.hashtag) {
result += twttr.txt.linkToHashtag(entity, text, options);
} else if(entity.screenName) {
result += twttr.txt.linkToUsernameAndList(entity, text, options);
}
result += replaceStr;
beginIndex = entity.indices[1];
}
result += text.substring(beginIndex, text.length);
return result;
};

twttr.txt.htmlAttrForOptions = function(options) {
var htmlAttrs = "";
twttr.txt.htmlAttrsFromOptions = function(options) {
var htmlAttrs = {};
for (var k in options) {
var v = options[k];
if (OPTIONS_NOT_ATTRIBUTES[k]) continue;
if (BOOLEAN_ATTRIBUTES[k]) {
v = v ? k : null;
}
if (v == null) continue;
htmlAttrs += stringSupplant(" #{k}=\"#{v}\" ", {k: twttr.txt.htmlEscape(k), v: twttr.txt.htmlEscape(v.toString())});
htmlAttrs[k] = v.toString();
}
return htmlAttrs;
};
Expand Down Expand Up @@ -856,7 +860,6 @@ if (typeof twttr === "undefined" || twttr === null) {
var tagName = options.tag || defaultHighlightTag,
tags = ["<" + tagName + ">", "</" + tagName + ">"],
chunks = twttr.txt.splitTags(text),
split,
i,
j,
result = "",
Expand Down

0 comments on commit 4b665b1

Please sign in to comment.