Browse files

Refactor twttr.txt.autoLinkEntities()

  • Loading branch information...
1 parent 53e88be commit 4b665b142c6f0c4943a23dd356b35afa59bed6ec @keitaf keitaf committed Feb 22, 2012
Showing with 137 additions and 120 deletions.
  1. +10 −1 test/conformance.html
  2. +15 −10 test/tests.js
  3. +112 −109 twitter-text.js
View
11 test/conformance.html
@@ -119,6 +119,8 @@
}
+ var expected = document.createElement("div");
+ var result = document.createElement("div");
for (var suite in cases) {
(function(suite) {
module(suite);
@@ -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));
View
25 test/tests.js
@@ -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");
@@ -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');
@@ -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];
View
221 twitter-text.js
@@ -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};
@@ -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.
//
@@ -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);
});
@@ -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;
};
@@ -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 = "",

0 comments on commit 4b665b1

Please sign in to comment.