Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Allow for additional attributes #22

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions test_urlize.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ describe('Basic functionality', function () {
describe('convert_arguments', function () {
it('single argument', function () {
deepEqual(urlize.test.convert_arguments(['foo']), {
attrs: undefined,
autoescape: undefined,
nofollow: undefined,
target: undefined,
Expand Down Expand Up @@ -364,3 +365,22 @@ describe('Trimming', function () {
'<a href="http://www.example.com/">http://www.example.com/</a>');
});
});

describe('Additional HTML Attributes', function () {
it('add single additional attributes', function () {
equal(urlize('http://www.example.com/', {attrs: {position: 'left'}}),
'<a href="http://www.example.com/" position="left">http://www.example.com/</a>');
});
it('add several additional attributes', function () {
equal(urlize('http://www.example.com/', {attrs: {position: 'left', 'open-in-app': true}}),
'<a href="http://www.example.com/" position="left" open-in-app="true">http://www.example.com/</a>');
});
it('add additional attributes with illegal values', function () {
equal(urlize('http://www.example.com/', {attrs: {position: 'left', 'open-in-app': '"/true'}}),
'<a href="http://www.example.com/" position="left" open-in-app="&quot;&#47;true">http://www.example.com/</a>');
});
it('add additional attributes with illegal keys', function () {
equal(urlize('http://www.example.com/', {attrs: {position: 'left', '"open,-in-app': '"/true'}}),
'<a href="http://www.example.com/" position="left" open-in-app="&quot;&#47;true">http://www.example.com/</a>');
});
});
76 changes: 61 additions & 15 deletions urlize.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(function (root, factory) {
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define('urlize', [], factory);
Expand All @@ -9,7 +9,7 @@
// Browser globals (root is window)
root.urlize = factory(root.b);
}
}(this, function () {
}(this, function() {
// From http://blog.stevenlevithan.com/archives/cross-browser-split
// modified to not add itself to String.prototype.

Expand Down Expand Up @@ -47,13 +47,13 @@
var split;

// Avoid running twice; that would break the `nativeSplit` reference
split = split || function (undef) {
split = split || function(undef) {

var nativeSplit = String.prototype.split,
compliantExecNpcg = /()??/.exec("")[1] === undef, // NPCG: nonparticipating capturing group
self;

self = function (str, separator, limit) {
self = function(str, separator, limit) {
// If `separator` is not a regex, use `nativeSplit`
if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
return nativeSplit.call(str, separator, limit);
Expand Down Expand Up @@ -92,7 +92,7 @@
// Fix browsers whose `exec` methods don't consistently return `undefined` for
// nonparticipating capturing groups
if (!compliantExecNpcg && match.length > 1) {
match[0].replace(separator2, function () {
match[0].replace(separator2, function() {
for (var i = 1; i < arguments.length - 2; i++) {
if (arguments[i] === undef) {
match[i] = undef;
Expand Down Expand Up @@ -125,7 +125,36 @@

return self;
}();

RegExp.escape = function(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}

String.prototype.mapReplace = function (replacements) {
var regex = [];

for (var prop in replacements) {
regex.push(RegExp.escape(prop));
}

regex = new RegExp( regex.join('|'), "g" );

return this.replace(regex, function(match){
return replacements[match];
});
}

String.prototype.escapeHtml = function() {
return this.mapReplace({
'<': '&lt;',
'>': '&gt;',
' ': '&nbsp;',
'"': '&quot;',
"'": '&#8216;',
'/': '&#47;',
'=': '&#61;'
});
};

function startswith(string, prefix) {
return string.substr(0, prefix.length) == prefix;
Expand Down Expand Up @@ -172,9 +201,18 @@

var trailing_punctuation_django = ['.', ',', ':', ';'];
var trailing_punctuation_improved = ['.', ',', ':', ';', '.)'];
var wrapping_punctuation_django = [['(', ')'], ['<', '>'], ['&lt;', '&gt;']];
var wrapping_punctuation_improved = [['(', ')'], ['<', '>'], ['&lt;', '&gt;'],
['“', '”'], ['‘', '’']];
var wrapping_punctuation_django = [
['(', ')'],
['<', '>'],
['&lt;', '&gt;']
];
var wrapping_punctuation_improved = [
['(', ')'],
['<', '>'],
['&lt;', '&gt;'],
['“', '”'],
['‘', '’']
];
var word_split_re_django = /(\s+)/;
var word_split_re_improved = /([\s<>"]+)/;
var simple_url_re = /^https?:\/\/\w/i;
Expand Down Expand Up @@ -205,14 +243,15 @@

function convert_arguments(args) {
var options;
if (args.length == 2 && typeof (args[1]) == 'object') {
if (args.length == 2 && typeof(args[1]) == 'object') {
options = args[1];
} else {
options = {
nofollow: args[1],
autoescape: args[2],
trim_url_limit: args[3],
target: args[4]
target: args[4],
attrs: args[5]
};
}
if (!('django_compatible' in options)) options.django_compatible = true;
Expand All @@ -235,10 +274,10 @@
var word_split_re = options.django_compatible ? word_split_re_django : word_split_re_improved;
var trailing_punctuation = options.django_compatible ? trailing_punctuation_django : trailing_punctuation_improved;
var wrapping_punctuation = options.django_compatible ? wrapping_punctuation_django : wrapping_punctuation_improved;
var simple_url_2_re = new RegExp('^www\\.|^(?!http)\\w[^@]+\\.(' +
(options.top_level_domains || django_top_level_domains).join('|') +
')$',
"i");
var simple_url_2_re = new RegExp('^www\\.|^(?!http)\\w[^@]+\\.(' +
(options.top_level_domains || django_top_level_domains).join('|') +
')$',
"i");
var words = split(text, word_split_re);
for (var i = 0; i < words.length; i++) {
var word = words[i];
Expand Down Expand Up @@ -274,6 +313,12 @@
var nofollow_attr = options.nofollow ? ' rel="nofollow"' : '';
var target_attr = options.target ? ' target="' + options.target + '"' : '';

var other_attr = '';

for (attr in options.attrs) {
other_attr += options.attrs[attr] ? ' ' + attr.replace(/[^a-zA-Z0-9_.-]/gi, '') + '="' + String(options.attrs[attr]).escapeHtml() + '"' : '';
}

if (middle.match(simple_url_re)) url = smart_urlquote(middle);
else if (middle.match(simple_url_2_re)) url = smart_urlquote('http://' + middle);
else if (middle.indexOf(':') == -1 && middle.match(simple_email_re)) {
Expand All @@ -292,7 +337,7 @@
url = urlescape(url);
trimmed = htmlescape(trimmed, options);
}
middle = '<a href="' + url + '"' + nofollow_attr + target_attr + '>' + trimmed + '</a>';
middle = '<a href="' + url + '"' + nofollow_attr + target_attr + other_attr + '>' + trimmed + '</a>';
words[i] = lead + middle + trail;
} else {
if (safe_input) {
Expand All @@ -316,3 +361,4 @@

return urlize;
}));