autolinker
Advanced tools
Comparing version 0.11.2 to 0.12.1
/*! | ||
* Autolinker.js | ||
* 0.11.2 | ||
* 0.12.1 | ||
* | ||
@@ -11,5 +11,4 @@ * Copyright(c) 2014 Gregory Jacobs <greg@greg-jacobs.com> | ||
/*global define, module */ | ||
/*jshint undef:true, smarttabs:true */ | ||
// Set up Autolinker appropriately for the environment. | ||
( function( root, factory ) { | ||
if( typeof define === 'function' && define.amd ) { | ||
@@ -23,3 +22,3 @@ define( factory ); // Define as AMD module if an AMD loader is present (ex: RequireJS). | ||
}( this, function() { | ||
/** | ||
@@ -55,2 +54,49 @@ * @class Autolinker | ||
* | ||
* | ||
* ## Custom Replacements of Links | ||
* | ||
* If the configuration options do not provide enough flexibility, a {@link #replaceFn} may be provided to fully customize | ||
* the output of Autolinker. This function is called once for each URL/Email/Twitter handle match that is encountered. | ||
* | ||
* For example: | ||
* | ||
* var input = "..."; // string with URLs, Email Addresses, and Twitter Handles | ||
* | ||
* var linkedText = Autolinker.link( input, { | ||
* replaceFn : function( autolinker, match ) { | ||
* console.log( "href = ", match.getAnchorHref() ); | ||
* console.log( "text = ", match.getAnchorText() ); | ||
* | ||
* switch( match.getType() ) { | ||
* case 'url' : | ||
* console.log( "url: ", match.getUrl() ); | ||
* return true; // let Autolinker perform its normal anchor tag replacement | ||
* | ||
* case 'email' : | ||
* var email = match.getEmail(); | ||
* console.log( "email: ", email ); | ||
* | ||
* if( email === "my@own.address" ) { | ||
* return false; // don't auto-link this particular email address; leave as-is | ||
* } else { | ||
* return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) | ||
* } | ||
* | ||
* case 'twitter' : | ||
* var twitterHandle = match.getTwitterHandle(); | ||
* console.log( twitterHandle ); | ||
* | ||
* return '<a href="http://newplace.to.link.twitter.handles.to/">' + twitterHandle + '</a>'; | ||
* } | ||
* } | ||
* } ); | ||
* | ||
* | ||
* The function may return the following values: | ||
* | ||
* - `true` (Boolean): Allow Autolinker to replace the match as it normally would. | ||
* - `false` (Boolean): Do not replace the current match at all - leave as-is. | ||
* - Any string: If a string is returned from the function, the string will be used directly as the replacement HTML for | ||
* the match. | ||
* | ||
* @constructor | ||
@@ -60,7 +106,3 @@ * @param {Object} [config] The configuration options for the Autolinker instance, specified in an Object (map). | ||
var Autolinker = function( cfg ) { | ||
cfg = cfg || {}; | ||
// Assign the properties of `cfg` onto the Autolinker instance | ||
for( var prop in cfg ) | ||
if( cfg.hasOwnProperty( prop ) ) this[ prop ] = cfg[ prop ]; | ||
Autolinker.Util.assign( this, cfg ); // assign the properties of `cfg` onto the Autolinker instance. Prototype properties will be used for missing configs. | ||
}; | ||
@@ -73,46 +115,47 @@ | ||
/** | ||
* @cfg {Boolean} newWindow | ||
* @cfg {Boolean} urls | ||
* | ||
* `true` if the links should open in a new window, `false` otherwise. | ||
* `true` if miscellaneous URLs should be automatically linked, `false` if they should not be. | ||
*/ | ||
newWindow : true, | ||
urls : true, | ||
/** | ||
* @cfg {Boolean} stripPrefix | ||
* @cfg {Boolean} email | ||
* | ||
* `true` if 'http://' or 'https://' and/or the 'www.' should be stripped from the beginning of links, `false` otherwise. | ||
* `true` if email addresses should be automatically linked, `false` if they should not be. | ||
*/ | ||
stripPrefix : true, | ||
email : true, | ||
/** | ||
* @cfg {Number} truncate | ||
* @cfg {Boolean} twitter | ||
* | ||
* A number for how many characters long URLs/emails/twitter handles should be truncated to inside the text of | ||
* a link. If the URL/email/twitter is over this number of characters, it will be truncated to this length by | ||
* adding a two period ellipsis ('..') into the middle of the string. | ||
* | ||
* For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' truncated to 25 characters might look | ||
* something like this: 'http://www...th/to/a/file' | ||
* `true` if Twitter handles ("@example") should be automatically linked, `false` if they should not be. | ||
*/ | ||
twitter : true, | ||
/** | ||
* @cfg {Boolean} twitter | ||
* @cfg {Boolean} newWindow | ||
* | ||
* `true` if Twitter handles ("@example") should be automatically linked, `false` if they should not be. | ||
* `true` if the links should open in a new window, `false` otherwise. | ||
*/ | ||
twitter : true, | ||
newWindow : true, | ||
/** | ||
* @cfg {Boolean} email | ||
* @cfg {Boolean} stripPrefix | ||
* | ||
* `true` if email addresses should be automatically linked, `false` if they should not be. | ||
* `true` if 'http://' or 'https://' and/or the 'www.' should be stripped from the beginning of URL links' text, | ||
* `false` otherwise. | ||
*/ | ||
email : true, | ||
stripPrefix : true, | ||
/** | ||
* @cfg {Boolean} urls | ||
* @cfg {Number} truncate | ||
* | ||
* `true` if miscellaneous URLs should be automatically linked, `false` if they should not be. | ||
* A number for how many characters long URLs/emails/twitter handles should be truncated to inside the text of | ||
* a link. If the URL/email/twitter is over this number of characters, it will be truncated to this length by | ||
* adding a two period ellipsis ('..') to the end of the string. | ||
* | ||
* For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' truncated to 25 characters might look | ||
* something like this: 'yahoo.com/some/long/pat..' | ||
*/ | ||
urls : true, | ||
@@ -132,2 +175,10 @@ /** | ||
className : "", | ||
/** | ||
* @cfg {Function} replaceFn | ||
* | ||
* A function to individually process each URL/Email/Twitter match found in the input string. | ||
* | ||
* See the class's description for usage. | ||
*/ | ||
@@ -137,2 +188,12 @@ | ||
* @private | ||
* @property {RegExp} htmlCharacterEntitiesRegex | ||
* | ||
* The regular expression that matches common HTML character entities. | ||
* | ||
* Ignoring & as it could be part of a query string -- handling it separately. | ||
*/ | ||
htmlCharacterEntitiesRegex: /( | |<|<|>|>)/gi, | ||
/** | ||
* @private | ||
* @property {RegExp} matcherRegex | ||
@@ -222,14 +283,507 @@ * | ||
* @private | ||
* @property {RegExp} protocolRelativeRegex | ||
* @property {RegExp} invalidProtocolRelMatchRegex | ||
* | ||
* The regular expression used to find protocol-relative URLs. A protocol-relative URL is, for example, "//yahoo.com" | ||
* The regular expression used to check a potential protocol-relative URL match, coming from the {@link #matcherRegex}. | ||
* A protocol-relative URL is, for example, "//yahoo.com" | ||
* | ||
* This regular expression needs to match the character before the '//', in order to determine if we should actually | ||
* autolink a protocol-relative URL. For instance, we want to autolink something like "//google.com", but we | ||
* don't want to autolink something like "abc//google.com" | ||
* This regular expression is used in conjunction with the {@link #matcherRegex}, and checks to see if there is a word character | ||
* before the '//' in order to determine if we should actually autolink a protocol-relative URL. This is needed because there | ||
* is no negative look-behind in JavaScript regular expressions. | ||
* | ||
* For instance, we want to autolink something like "//google.com", but we don't want to autolink something | ||
* like "abc//google.com" | ||
*/ | ||
protocolRelativeRegex : /(.)?\/\//, | ||
invalidProtocolRelMatchRegex : /^[\w]\/\//, | ||
/** | ||
* @private | ||
* @property {RegExp} charBeforeProtocolRelMatchRegex | ||
* | ||
* The regular expression used to retrieve the character before a protocol-relative URL match. | ||
* | ||
* This is used in conjunction with the {@link #matcherRegex}, which needs to grab the character before a protocol-relative | ||
* '//' due to the lack of a negative look-behind in JavaScript regular expressions. The character before the match is stripped | ||
* from the URL. | ||
*/ | ||
charBeforeProtocolRelMatchRegex : /^(.)?\/\//, | ||
/** | ||
* @private | ||
* @property {Autolinker.AnchorTagBuilder} tagBuilder | ||
* | ||
* The AnchorTagBuilder instance used to build the URL/email/Twitter replacement anchor tags. This is lazily instantiated | ||
* in the {@link #getTagBuilder} method. | ||
*/ | ||
/** | ||
* Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. | ||
* Does not link URLs found within HTML tags. | ||
* | ||
* For instance, if given the text: `You should go to http://www.yahoo.com`, then the result | ||
* will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` | ||
* | ||
* This method finds the text around any HTML elements in the input `textOrHtml`, which will be the text that is processed. | ||
* Any original HTML elements will be left as-is, as well as the text that is already wrapped in anchor (<a>) tags. | ||
* | ||
* @param {String} textOrHtml The HTML or text to link URLs, email addresses, and Twitter handles within. | ||
* @return {String} The HTML, with URLs/emails/Twitter handles automatically linked. | ||
*/ | ||
link : function( textOrHtml ) { | ||
var me = this, // for closure | ||
htmlParser = this.getHtmlParser(), | ||
htmlCharacterEntitiesRegex = this.htmlCharacterEntitiesRegex, | ||
anchorTagStackCount = 0, // used to only process text around anchor tags, and any inner text/html they may have | ||
resultHtml = []; | ||
htmlParser.parse( textOrHtml, { | ||
// Process HTML nodes in the input `textOrHtml` | ||
processHtmlNode : function( tagText, tagName, isClosingTag ) { | ||
if( tagName === 'a' ) { | ||
if( !isClosingTag ) { // it's the start <a> tag | ||
anchorTagStackCount++; | ||
} else { // it's the end </a> tag | ||
anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous </a> tags by making sure the stack count never goes below 0 | ||
} | ||
} | ||
resultHtml.push( tagText ); // now add the text of the tag itself verbatim | ||
}, | ||
// Process text nodes in the input `textOrHtml` | ||
processTextNode : function( text ) { | ||
if( anchorTagStackCount === 0 ) { | ||
// If we're not within an <a> tag, process the text node | ||
var unescapedText = Autolinker.Util.splitAndCapture( text, htmlCharacterEntitiesRegex ); // split at HTML entities, but include the HTML entities in the results array | ||
for ( var i = 0, len = unescapedText.length; i < len; i++ ) { | ||
var textToProcess = unescapedText[ i ], | ||
processedTextNode = me.processTextNode( textToProcess ); | ||
resultHtml.push( processedTextNode ); | ||
} | ||
} else { | ||
// `text` is within an <a> tag, simply append the text - we do not want to autolink anything | ||
// already within an <a>...</a> tag | ||
resultHtml.push( text ); | ||
} | ||
} | ||
} ); | ||
return resultHtml.join( "" ); | ||
}, | ||
/** | ||
* Lazily instantiates and returns the {@link #htmlParser} instance for this Autolinker instance. | ||
* | ||
* @protected | ||
* @return {Autolinker.HtmlParser} | ||
*/ | ||
getHtmlParser : function() { | ||
var htmlParser = this.htmlParser; | ||
if( !htmlParser ) { | ||
htmlParser = this.htmlParser = new Autolinker.HtmlParser(); | ||
} | ||
return htmlParser; | ||
}, | ||
/** | ||
* Returns the {@link #tagBuilder} instance for this Autolinker instance, lazily instantiating it | ||
* if it does not yet exist. | ||
* | ||
* @return {Autolinker.AnchorTagBuilder} | ||
*/ | ||
getTagBuilder : function() { | ||
var tagBuilder = this.tagBuilder; | ||
if( !tagBuilder ) { | ||
tagBuilder = this.tagBuilder = new Autolinker.AnchorTagBuilder( { | ||
newWindow : this.newWindow, | ||
truncate : this.truncate, | ||
className : this.className | ||
} ); | ||
} | ||
return tagBuilder; | ||
}, | ||
/** | ||
* Process the text that lies inbetween HTML tags. This method does the actual wrapping of URLs with | ||
* anchor tags. | ||
* | ||
* @private | ||
* @param {String} text The text to auto-link. | ||
* @return {String} The text with anchor tags auto-filled. | ||
*/ | ||
processTextNode : function( text ) { | ||
var me = this, // for closure | ||
charBeforeProtocolRelMatchRegex = this.charBeforeProtocolRelMatchRegex; | ||
return text.replace( this.matcherRegex, function( matchStr, $1, $2, $3, $4, $5, $6, $7 ) { | ||
var twitterMatch = $1, | ||
twitterHandlePrefixWhitespaceChar = $2, // The whitespace char before the @ sign in a Twitter handle match. This is needed because of no lookbehinds in JS regexes. | ||
twitterHandle = $3, // The actual twitterUser (i.e the word after the @ sign in a Twitter handle match) | ||
emailAddressMatch = $4, // For both determining if it is an email address, and stores the actual email address | ||
urlMatch = $5, // The matched URL string | ||
protocolRelativeMatch = $6 || $7, // The '//' for a protocol-relative match, with the character that comes before the '//' | ||
prefixStr = "", // A string to use to prefix the anchor tag that is created. This is needed for the Twitter handle match | ||
suffixStr = "", // A string to suffix the anchor tag that is created. This is used if there is a trailing parenthesis that should not be auto-linked. | ||
match; // Will be an Autolinker.match.Match object | ||
// Return out with no changes for match types that are disabled (url, email, twitter), or for matches that are | ||
// invalid (false positives from the matcherRegex, which can't use look-behinds since they are unavailable in JS). | ||
if( !me.isValidMatch( twitterMatch, emailAddressMatch, urlMatch, protocolRelativeMatch ) ) { | ||
return matchStr; | ||
} | ||
// Handle a closing parenthesis at the end of the match, and exclude it if there is not a matching open parenthesis | ||
// in the match itself. | ||
if( me.matchHasUnbalancedClosingParen( matchStr ) ) { | ||
matchStr = matchStr.substr( 0, matchStr.length - 1 ); // remove the trailing ")" | ||
suffixStr = ")"; // this will be added after the generated <a> tag | ||
} | ||
if( emailAddressMatch ) { | ||
match = new Autolinker.match.Email( { email: emailAddressMatch } ); | ||
} else if( twitterMatch ) { | ||
// fix up the `matchStr` if there was a preceding whitespace char, which was needed to determine the match | ||
// itself (since there are no look-behinds in JS regexes) | ||
if( twitterHandlePrefixWhitespaceChar ) { | ||
prefixStr = twitterHandlePrefixWhitespaceChar; | ||
matchStr = matchStr.slice( 1 ); // remove the prefixed whitespace char from the match | ||
} | ||
match = new Autolinker.match.Twitter( { twitterHandle: twitterHandle } ); | ||
} else { // url match | ||
// If it's a protocol-relative '//' match, remove the character before the '//' (which the matcherRegex needed | ||
// to match due to the lack of a negative look-behind in JavaScript regular expressions) | ||
if( protocolRelativeMatch ) { | ||
var charBeforeMatch = protocolRelativeMatch.match( charBeforeProtocolRelMatchRegex )[ 1 ] || ""; | ||
if( charBeforeMatch ) { // fix up the `matchStr` if there was a preceding char before a protocol-relative match, which was needed to determine the match itself (since there are no look-behinds in JS regexes) | ||
prefixStr = charBeforeMatch; | ||
matchStr = matchStr.slice( 1 ); // remove the prefixed char from the match | ||
} | ||
} | ||
match = new Autolinker.match.Url( { url: matchStr, protocolRelativeMatch: protocolRelativeMatch, stripPrefix: me.stripPrefix } ); | ||
} | ||
// Generate the replacement text for the match | ||
var matchReturnVal = me.createMatchReturnVal( match, matchStr ); | ||
return prefixStr + matchReturnVal + suffixStr; | ||
} ); | ||
}, | ||
/** | ||
* Determines if a given match found by {@link #processTextNode} is valid. Will return `false` for: | ||
* | ||
* 1) Disabled link types (i.e. having a Twitter match, but {@link #twitter} matching is disabled) | ||
* 2) URL matches which do not have at least have one period ('.') in the domain name (effectively skipping over | ||
* matches like "abc:def") | ||
* 3) A protocol-relative url match (a URL beginning with '//') whose previous character is a word character | ||
* (effectively skipping over strings like "abc//google.com") | ||
* | ||
* Otherwise, returns `true`. | ||
* | ||
* @private | ||
* @param {String} twitterMatch The matched Twitter handle, if there was one. Will be empty string if the match is not a | ||
* Twitter match. | ||
* @param {String} emailAddressMatch The matched Email address, if there was one. Will be empty string if the match is not | ||
* an Email address match. | ||
* @param {String} urlMatch The matched URL, if there was one. Will be an empty string if the match is not a URL match. | ||
* @param {String} protocolRelativeMatch The protocol-relative string for a URL match (i.e. '//'), possibly with a preceding | ||
* character (ex, a space, such as: ' //', or a letter, such as: 'a//'). The match is invalid if there is a word character | ||
* preceding the '//'. | ||
* @return {Boolean} `true` if the match given is valid and should be processed, or `false` if the match is invalid and/or | ||
* should just not be processed (such as, if it's a Twitter match, but {@link #twitter} matching is disabled}. | ||
*/ | ||
isValidMatch : function( twitterMatch, emailAddressMatch, urlMatch, protocolRelativeMatch ) { | ||
if( | ||
( twitterMatch && !this.twitter ) || ( emailAddressMatch && !this.email ) || ( urlMatch && !this.urls ) || | ||
( urlMatch && urlMatch.indexOf( '.' ) === -1 ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL | ||
( urlMatch && /^[A-Za-z]{3,9}:/.test( urlMatch ) && !/:.*?[A-Za-z]/.test( urlMatch ) ) || // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0" | ||
( protocolRelativeMatch && this.invalidProtocolRelMatchRegex.test( protocolRelativeMatch ) ) // a protocol-relative match which has a word character in front of it (so we can skip something like "abc//google.com") | ||
) { | ||
return false; | ||
} | ||
return true; | ||
}, | ||
/** | ||
* Determines if a match found has an unmatched closing parenthesis. If so, this parenthesis will be removed | ||
* from the match itself, and appended after the generated anchor tag in {@link #processTextNode}. | ||
* | ||
* A match may have an extra closing parenthesis at the end of the match because the regular expression must include parenthesis | ||
* for URLs such as "wikipedia.com/something_(disambiguation)", which should be auto-linked. | ||
* | ||
* However, an extra parenthesis *will* be included when the URL itself is wrapped in parenthesis, such as in the case of | ||
* "(wikipedia.com/something_(disambiguation))". In this case, the last closing parenthesis should *not* be part of the URL | ||
* itself, and this method will return `true`. | ||
* | ||
* @private | ||
* @param {String} matchStr The full match string from the {@link #matcherRegex}. | ||
* @return {Boolean} `true` if there is an unbalanced closing parenthesis at the end of the `matchStr`, `false` otherwise. | ||
*/ | ||
matchHasUnbalancedClosingParen : function( matchStr ) { | ||
var lastChar = matchStr.charAt( matchStr.length - 1 ); | ||
if( lastChar === ')' ) { | ||
var openParensMatch = matchStr.match( /\(/g ), | ||
closeParensMatch = matchStr.match( /\)/g ), | ||
numOpenParens = ( openParensMatch && openParensMatch.length ) || 0, | ||
numCloseParens = ( closeParensMatch && closeParensMatch.length ) || 0; | ||
if( numOpenParens < numCloseParens ) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}, | ||
/** | ||
* Creates the return string value for a given match in the input string, for the {@link #processTextNode} method. | ||
* | ||
* This method handles the {@link #replaceFn}, if one was provided. | ||
* | ||
* @private | ||
* @param {Autolinker.match.Match} match The Match object that represents the match. | ||
* @param {String} matchStr The original match string, after having been preprocessed to fix match edge cases (see | ||
* the `prefixStr` and `suffixStr` vars in {@link #processTextNode}. | ||
* @return {String} The string that the `match` should be replaced with. This is usually the anchor tag string, but | ||
* may be the `matchStr` itself if the match is not to be replaced. | ||
*/ | ||
createMatchReturnVal : function( match, matchStr ) { | ||
// Handle a custom `replaceFn` being provided | ||
var replaceFnResult; | ||
if( this.replaceFn ) { | ||
replaceFnResult = this.replaceFn.call( this, this, match ); // Autolinker instance is the context, and the first arg | ||
} | ||
if( typeof replaceFnResult === 'string' ) { | ||
return replaceFnResult; // `replaceFn` returned a string, use that | ||
} else if( replaceFnResult === false ) { | ||
return matchStr; // no replacement for the match | ||
} else if( replaceFnResult instanceof Autolinker.HtmlTag ) { | ||
return replaceFnResult.toString(); | ||
} else { // replaceFnResult === true, or no/unknown return value from function | ||
// Perform Autolinker's default anchor tag generation | ||
var tagBuilder = this.getTagBuilder(), | ||
anchorTag = tagBuilder.build( match ); // returns an Autolinker.HtmlTag instance | ||
return anchorTag.toString(); | ||
} | ||
} | ||
}; | ||
/** | ||
* Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. | ||
* Does not link URLs found within HTML tags. | ||
* | ||
* For instance, if given the text: `You should go to http://www.yahoo.com`, then the result | ||
* will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` | ||
* | ||
* Example: | ||
* | ||
* var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); | ||
* // Produces: "Go to <a href="http://google.com">google.com</a>" | ||
* | ||
* @static | ||
* @method link | ||
* @param {String} html The HTML text to link URLs within. | ||
* @param {Object} [options] Any of the configuration options for the Autolinker class, specified in an Object (map). | ||
* See the class description for an example call. | ||
* @return {String} The HTML text, with URLs automatically linked | ||
*/ | ||
Autolinker.link = function( text, options ) { | ||
var autolinker = new Autolinker( options ); | ||
return autolinker.link( text ); | ||
}; | ||
// Namespace for `match` classes | ||
Autolinker.match = {}; | ||
/*global Autolinker */ | ||
/*jshint eqnull:true, boss:true */ | ||
/** | ||
* @class Autolinker.Util | ||
* @singleton | ||
* | ||
* A few utility methods for Autolinker. | ||
*/ | ||
Autolinker.Util = { | ||
/** | ||
* @property {Function} abstractMethod | ||
* | ||
* A function object which represents an abstract method. | ||
*/ | ||
abstractMethod : function() { throw "abstract"; }, | ||
/** | ||
* Assigns (shallow copies) the properties of `src` onto `dest`. | ||
* | ||
* @param {Object} dest The destination object. | ||
* @param {Object} src The source object. | ||
* @return {Object} The destination object. | ||
*/ | ||
assign : function( dest, src ) { | ||
for( var prop in src ) { | ||
if( src.hasOwnProperty( prop ) ) { | ||
dest[ prop ] = src[ prop ]; | ||
} | ||
} | ||
return dest; | ||
}, | ||
/** | ||
* Extends `superclass` to create a new subclass, adding the `protoProps` to the new subclass's prototype. | ||
* | ||
* @param {Function} superclass The constructor function for the superclass. | ||
* @param {Object} protoProps The methods/properties to add to the subclass's prototype. This may contain the | ||
* special property `constructor`, which will be used as the new subclass's constructor function. | ||
* @return {Function} The new subclass function. | ||
*/ | ||
extend : function( superclass, protoProps ) { | ||
var superclassProto = superclass.prototype; | ||
var F = function() {}; | ||
F.prototype = superclassProto; | ||
var subclass; | ||
if( protoProps.hasOwnProperty( 'constructor' ) ) { | ||
subclass = protoProps.constructor; | ||
} else { | ||
subclass = function() { superclassProto.constructor.apply( this, arguments ); }; | ||
} | ||
var subclassProto = subclass.prototype = new F(); // set up prototype chain | ||
subclassProto.constructor = subclass; // fix constructor property | ||
subclassProto.superclass = superclassProto; | ||
delete protoProps.constructor; // don't re-assign constructor property to the prototype, since a new function may have been created (`subclass`), which is now already there | ||
Autolinker.Util.assign( subclassProto, protoProps ); | ||
return subclass; | ||
}, | ||
/** | ||
* Truncates the `str` at `len - ellipsisChars.length`, and adds the `ellipsisChars` to the | ||
* end of the string (by default, two periods: '..'). If the `str` length does not exceed | ||
* `len`, the string will be returned unchanged. | ||
* | ||
* @param {String} str The string to truncate and add an ellipsis to. | ||
* @param {Number} truncateLen The length to truncate the string at. | ||
* @param {String} [ellipsisChars=..] The ellipsis character(s) to add to the end of `str` | ||
* when truncated. Defaults to '..' | ||
*/ | ||
ellipsis : function( str, truncateLen, ellipsisChars ) { | ||
if( str.length > truncateLen ) { | ||
ellipsisChars = ( ellipsisChars == null ) ? '..' : ellipsisChars; | ||
str = str.substring( 0, truncateLen - ellipsisChars.length ) + ellipsisChars; | ||
} | ||
return str; | ||
}, | ||
/** | ||
* Supports `Array.prototype.indexOf()` functionality for old IE (IE8 and below). | ||
* | ||
* @param {Array} arr The array to find an element of. | ||
* @param {*} element The element to find in the array, and return the index of. | ||
* @return {Number} The index of the `element`, or -1 if it was not found. | ||
*/ | ||
indexOf : function( arr, element ) { | ||
if( Array.prototype.indexOf ) { | ||
return arr.indexOf( element ); | ||
} else { | ||
for( var i = 0, len = arr.length; i < len; i++ ) { | ||
if( arr[ i ] === element ) return i; | ||
} | ||
return -1; | ||
} | ||
}, | ||
/** | ||
* Performs the functionality of what modern browsers do when `String.prototype.split()` is called | ||
* with a regular expression that contains capturing parenthesis. | ||
* | ||
* For example: | ||
* | ||
* // Modern browsers: | ||
* "a,b,c".split( /(,)/ ); // --> [ 'a', ',', 'b', ',', 'c' ] | ||
* | ||
* // Old IE (including IE8): | ||
* "a,b,c".split( /(,)/ ); // --> [ 'a', 'b', 'c' ] | ||
* | ||
* This method emulates the functionality of modern browsers for the old IE case. | ||
* | ||
* @param {String} str The string to split. | ||
* @param {RegExp} splitRegex The regular expression to split the input `str` on. The splitting | ||
* character(s) will be spliced into the array, as in the "modern browsers" example in the | ||
* description of this method. | ||
* Note #1: the supplied regular expression **must** have the 'g' flag specified. | ||
* Note #2: for simplicity's sake, the regular expression does not need | ||
* to contain capturing parenthesis - it will be assumed that any match has them. | ||
* @return {String[]} The split array of strings, with the splitting character(s) included. | ||
*/ | ||
splitAndCapture : function( str, splitRegex ) { | ||
if( !splitRegex.global ) throw new Error( "`splitRegex` must have the 'g' flag set" ); | ||
var result = [], | ||
lastIdx = 0, | ||
match; | ||
while( match = splitRegex.exec( str ) ) { | ||
result.push( str.substring( lastIdx, match.index ) ); | ||
result.push( match[ 0 ] ); // push the splitting char(s) | ||
lastIdx = match.index + match[ 0 ].length; | ||
} | ||
result.push( str.substring( lastIdx ) ); | ||
return result; | ||
} | ||
}; | ||
/*global Autolinker */ | ||
/** | ||
* @class Autolinker.HtmlParser | ||
* @extends Object | ||
* | ||
* An HTML parser which simply walks an HTML string and calls the provided visitor functions to process HTML | ||
* and text nodes. | ||
* | ||
* Autolinker uses this to only link URLs/emails/Twitter handles in text nodes, basically ignoring HTML tags. | ||
*/ | ||
Autolinker.HtmlParser = Autolinker.Util.extend( Object, { | ||
/** | ||
* @private | ||
* @property {RegExp} htmlRegex | ||
@@ -271,25 +825,119 @@ * | ||
} )(), | ||
/** | ||
* @private | ||
* @property {RegExp} urlPrefixRegex | ||
* Walks an HTML string, calling the `options.processHtmlNode` function for each HTML tag that is encountered, and calling | ||
* the `options.processTextNode` function when each text around HTML tags is encountered. | ||
* | ||
* A regular expression used to remove the 'http://' or 'https://' and/or the 'www.' from URLs. | ||
* @param {String} html The HTML to parse. | ||
* @param {Object} [options] An Object (map) which may contain the following properties: | ||
* | ||
* @param {Function} [options.processHtmlNode] A visitor function which allows processing of an encountered HTML node. | ||
* This function is called with the following arguments: | ||
* @param {String} [options.processHtmlNode.tagText] The HTML tag text that was found. | ||
* @param {String} [options.processHtmlNode.tagName] The tag name for the HTML tag that was found. Ex: 'a' for an anchor tag. | ||
* @param {String} [options.processHtmlNode.isClosingTag] `true` if the tag is a closing tag (ex: </a>), `false` otherwise. | ||
* | ||
* @param {Function} [options.processTextNode] A visitor function which allows processing of an encountered text node. | ||
* This function is called with the following arguments: | ||
* @param {String} [options.processTextNode.text] The text node that was matched. | ||
*/ | ||
urlPrefixRegex: /^(https?:\/\/)?(www\.)?/i, | ||
parse : function( html, options ) { | ||
options = options || {}; | ||
var processHtmlNodeVisitor = options.processHtmlNode || function() {}, | ||
processTextNodeVisitor = options.processTextNode || function() {}, | ||
htmlRegex = this.htmlRegex, | ||
currentResult, | ||
lastIndex = 0; | ||
// Loop over the HTML string, ignoring HTML tags, and processing the text that lies between them, | ||
// wrapping the URLs in anchor tags | ||
while( ( currentResult = htmlRegex.exec( html ) ) !== null ) { | ||
var tagText = currentResult[ 0 ], | ||
tagName = currentResult[ 2 ], | ||
isClosingTag = !!currentResult[ 1 ], | ||
inBetweenTagsText = html.substring( lastIndex, currentResult.index ); | ||
if( inBetweenTagsText ) { | ||
processTextNodeVisitor( inBetweenTagsText ); | ||
} | ||
processHtmlNodeVisitor( tagText, tagName, isClosingTag ); | ||
lastIndex = currentResult.index + tagText.length; | ||
} | ||
// Process any remaining text after the last HTML element. Will process all of the text if there were no HTML elements. | ||
if( lastIndex < html.length ) { | ||
var text = html.substring( lastIndex ); | ||
if( text ) { | ||
processTextNodeVisitor( text ); | ||
} | ||
} | ||
} | ||
} ); | ||
/*global Autolinker */ | ||
/*jshint boss:true */ | ||
/** | ||
* @class Autolinker.HtmlTag | ||
* @extends Object | ||
* | ||
* Represents an HTML tag, which can be used to build HTML tags piece-by-piece programmatically. | ||
*/ | ||
Autolinker.HtmlTag = Autolinker.Util.extend( Object, { | ||
/** | ||
* Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. | ||
* Does not link URLs found within HTML tags. | ||
* @cfg {String} tagName | ||
* | ||
* For instance, if given the text: `You should go to http://www.yahoo.com`, then the result | ||
* will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` | ||
* The tag name. Ex: 'a', 'button', etc. | ||
* | ||
* @method link | ||
* @param {String} textOrHtml The HTML or text to link URLs, email addresses, and Twitter handles within. | ||
* @return {String} The HTML, with URLs/emails/twitter handles automatically linked. | ||
* Not required at instantiation time, but should be set using {@link #setTagName} before {@link #toString} | ||
* is executed. | ||
*/ | ||
link : function( textOrHtml ) { | ||
return this.processHtml( textOrHtml ); | ||
/** | ||
* @cfg {Object.<String, String>} attrs | ||
* | ||
* An key/value Object (map) of attributes to create the tag with. The keys are the attribute names, and the | ||
* values are the attribute values. | ||
*/ | ||
/** | ||
* @cfg {String} innerHtml | ||
* | ||
* The inner HTML for the tag. | ||
* | ||
* Note the camel case name on `innerHtml`. Acronyms are camelCased in this utility (such as not to run into the acronym | ||
* naming inconsistency that the DOM developers created with `XMLHttpRequest`). You may alternatively use {@link #innerHTML} | ||
* if you prefer, but this one is recommended. | ||
*/ | ||
/** | ||
* @cfg {String} innerHTML | ||
* | ||
* Alias of {@link #innerHtml}, accepted for consistency with the browser DOM api, but prefer the camelCased version | ||
* for acronym names. | ||
*/ | ||
/** | ||
* @protected | ||
* @property {RegExp} whitespaceRegex | ||
* | ||
* Regular expression used to match whitespace in a string of CSS classes. | ||
*/ | ||
whitespaceRegex : /\s+/, | ||
/** | ||
* @constructor | ||
* @param {Object} [cfg] The configuration properties for this class, in an Object (map) | ||
*/ | ||
constructor : function( cfg ) { | ||
Autolinker.Util.assign( this, cfg ); | ||
this.innerHtml = this.innerHtml || this.innerHTML; // accept either the camelCased form or the fully capitalized acronym | ||
}, | ||
@@ -299,62 +947,132 @@ | ||
/** | ||
* Processes the given HTML to auto-link URLs/emails/Twitter handles. | ||
* Sets the tag name that will be used to generate the tag with. | ||
* | ||
* Finds the text around any HTML elements in the input `html`, which will be the text that is processed. | ||
* Any original HTML elements will be left as-is, as well as the text that is already wrapped in anchor tags. | ||
* @param {String} tagName | ||
* @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. | ||
*/ | ||
setTagName : function( tagName ) { | ||
this.tagName = tagName; | ||
return this; | ||
}, | ||
/** | ||
* Retrieves the tag name. | ||
* | ||
* @private | ||
* @method processHtml | ||
* @param {String} html The input text or HTML to process in order to auto-link. | ||
* @return {String} | ||
*/ | ||
processHtml : function( html ) { | ||
// Loop over the HTML string, ignoring HTML tags, and processing the text that lies between them, | ||
// wrapping the URLs in anchor tags | ||
var htmlRegex = this.htmlRegex, | ||
currentResult, | ||
inBetweenTagsText, | ||
lastIndex = 0, | ||
anchorTagStackCount = 0, | ||
resultHtml = []; | ||
getTagName : function() { | ||
return this.tagName || ""; | ||
}, | ||
/** | ||
* Sets an attribute on the HtmlTag. | ||
* | ||
* @param {String} attrName The attribute name to set. | ||
* @param {String} attrValue The attribute value to set. | ||
* @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. | ||
*/ | ||
setAttr : function( attrName, attrValue ) { | ||
var tagAttrs = this.getAttrs(); | ||
tagAttrs[ attrName ] = attrValue; | ||
while( ( currentResult = htmlRegex.exec( html ) ) !== null ) { | ||
var tagText = currentResult[ 0 ], | ||
tagName = currentResult[ 2 ], | ||
isClosingTag = !!currentResult[ 1 ]; | ||
inBetweenTagsText = html.substring( lastIndex, currentResult.index ); | ||
lastIndex = currentResult.index + tagText.length; | ||
// Process around anchor tags, and any inner text / html they may have | ||
if( tagName === 'a' ) { | ||
if( !isClosingTag ) { // it's the start <a> tag | ||
anchorTagStackCount++; | ||
resultHtml.push( this.processTextNode( inBetweenTagsText ) ); | ||
} else { // it's the end </a> tag | ||
anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous </a> tags by making sure the stack count never goes below 0 | ||
if( anchorTagStackCount === 0 ) { | ||
resultHtml.push( inBetweenTagsText ); // We hit the matching </a> tag, simply add all of the text from the start <a> tag to the end </a> tag without linking it | ||
} | ||
} | ||
} else if( anchorTagStackCount === 0 ) { // not within an anchor tag, link the "in between" text | ||
resultHtml.push( this.processTextNode( inBetweenTagsText ) ); | ||
} else { | ||
// if we have a tag that is in between anchor tags (ex: <a href="..."><b>google.com</b></a>), | ||
// just append the inner text | ||
resultHtml.push( inBetweenTagsText ); | ||
return this; | ||
}, | ||
/** | ||
* Retrieves an attribute from the HtmlTag. If the attribute does not exist, returns `undefined`. | ||
* | ||
* @param {String} name The attribute name to retrieve. | ||
* @return {String} The attribute's value, or `undefined` if it does not exist on the HtmlTag. | ||
*/ | ||
getAttr : function( attrName ) { | ||
return this.getAttrs()[ attrName ]; | ||
}, | ||
/** | ||
* Sets one or more attributes on the HtmlTag. | ||
* | ||
* @param {Object.<String, String>} attrs A key/value Object (map) of the attributes to set. | ||
* @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. | ||
*/ | ||
setAttrs : function( attrs ) { | ||
var tagAttrs = this.getAttrs(); | ||
Autolinker.Util.assign( tagAttrs, attrs ); | ||
return this; | ||
}, | ||
/** | ||
* Retrieves the attributes Object (map) for the HtmlTag. | ||
* | ||
* @return {Object.<String, String>} A key/value object of the attributes for the HtmlTag. | ||
*/ | ||
getAttrs : function() { | ||
return this.attrs || ( this.attrs = {} ); | ||
}, | ||
/** | ||
* Sets the provided `cssClass`, overwriting any current CSS classes on the HtmlTag. | ||
* | ||
* @param {String} cssClass One or more space-separated CSS classes to set (overwrite). | ||
* @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. | ||
*/ | ||
setClass : function( cssClass ) { | ||
return this.setAttr( 'class', cssClass ); | ||
}, | ||
/** | ||
* Convenience method to add one or more CSS classes to the HtmlTag. Will not add duplicate CSS classes. | ||
* | ||
* @param {String} cssClass One or more space-separated CSS classes to add. | ||
* @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. | ||
*/ | ||
addClass : function( cssClass ) { | ||
var classAttr = this.getClass(), | ||
whitespaceRegex = this.whitespaceRegex, | ||
indexOf = Autolinker.Util.indexOf, // to support IE8 and below | ||
classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), | ||
newClasses = cssClass.split( whitespaceRegex ), | ||
newClass; | ||
while( newClass = newClasses.shift() ) { | ||
if( indexOf( classes, newClass ) === -1 ) { | ||
classes.push( newClass ); | ||
} | ||
resultHtml.push( tagText ); // now add the text of the tag itself verbatim | ||
} | ||
// Process any remaining text after the last HTML element. Will process all of the text if there were no HTML elements. | ||
if( lastIndex < html.length ) { | ||
var processedTextNode = this.processTextNode( html.substring( lastIndex ) ); | ||
resultHtml.push( processedTextNode ); | ||
this.getAttrs()[ 'class' ] = classes.join( " " ); | ||
return this; | ||
}, | ||
/** | ||
* Convenience method to remove one or more CSS classes from the HtmlTag. | ||
* | ||
* @param {String} cssClass One or more space-separated CSS classes to remove. | ||
* @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. | ||
*/ | ||
removeClass : function( cssClass ) { | ||
var classAttr = this.getClass(), | ||
whitespaceRegex = this.whitespaceRegex, | ||
indexOf = Autolinker.Util.indexOf, // to support IE8 and below | ||
classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), | ||
removeClasses = cssClass.split( whitespaceRegex ), | ||
removeClass; | ||
while( classes.length && ( removeClass = removeClasses.shift() ) ) { | ||
var idx = indexOf( classes, removeClass ); | ||
if( idx !== -1 ) { | ||
classes.splice( idx, 1 ); | ||
} | ||
} | ||
return resultHtml.join( "" ); | ||
this.getAttrs()[ 'class' ] = classes.join( " " ); | ||
return this; | ||
}, | ||
@@ -364,101 +1082,119 @@ | ||
/** | ||
* Process the text that lies inbetween HTML tags. This method does the actual wrapping of URLs with | ||
* anchor tags. | ||
* Convenience method to retrieve the CSS class(es) for the HtmlTag, which will each be separated by spaces when | ||
* there are multiple. | ||
* | ||
* @private | ||
* @param {String} text The text to auto-link. | ||
* @return {String} The text with anchor tags auto-filled. | ||
* @return {String} | ||
*/ | ||
processTextNode : function( text ) { | ||
var me = this, // for closures | ||
matcherRegex = this.matcherRegex, | ||
enableTwitter = this.twitter, | ||
enableEmailAddresses = this.email, | ||
enableUrls = this.urls; | ||
getClass : function() { | ||
return this.getAttrs()[ 'class' ] || ""; | ||
}, | ||
/** | ||
* Convenience method to check if the tag has a CSS class or not. | ||
* | ||
* @param {String} cssClass The CSS class to check for. | ||
* @return {Boolean} `true` if the HtmlTag has the CSS class, `false` otherwise. | ||
*/ | ||
hasClass : function( cssClass ) { | ||
return ( ' ' + this.getClass() + ' ' ).indexOf( ' ' + cssClass + ' ' ) !== -1; | ||
}, | ||
/** | ||
* Sets the inner HTML for the tag. | ||
* | ||
* @param {String} html The inner HTML to set. | ||
* @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. | ||
*/ | ||
setInnerHtml : function( html ) { | ||
this.innerHtml = html; | ||
return text.replace( matcherRegex, function( matchStr, $1, $2, $3, $4, $5, $6, $7 ) { | ||
var twitterMatch = $1, | ||
twitterHandlePrefixWhitespaceChar = $2, // The whitespace char before the @ sign in a Twitter handle match. This is needed because of no lookbehinds in JS regexes | ||
twitterHandle = $3, // The actual twitterUser (i.e the word after the @ sign in a Twitter handle match) | ||
emailAddress = $4, // For both determining if it is an email address, and stores the actual email address | ||
urlMatch = $5, // The matched URL string | ||
protocolRelativeMatch = $6 || $7, // The '//' for a protocol-relative match, with the character that comes before the '//' | ||
prefixStr = "", // A string to use to prefix the anchor tag that is created. This is needed for the Twitter handle match | ||
suffixStr = ""; // A string to suffix the anchor tag that is created. This is used if there is a trailing parenthesis that should not be auto-linked. | ||
// Early exits with no replacements for: | ||
// 1) Disabled link types | ||
// 2) URL matches which do not have at least have one period ('.') in the domain name (effectively skipping over | ||
// matches like "abc:def") | ||
// 3) A protocol-relative url match (a URL beginning with '//') whose previous character is a word character | ||
// (effectively skipping over strings like "abc//google.com") | ||
if( | ||
( twitterMatch && !enableTwitter ) || ( emailAddress && !enableEmailAddresses ) || ( urlMatch && !enableUrls ) || | ||
( urlMatch && urlMatch.indexOf( '.' ) === -1 ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL | ||
( urlMatch && /^[A-Za-z]{3,9}:/.test( urlMatch ) && !/:.*?[A-Za-z]/.test( urlMatch ) ) || // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0" | ||
( protocolRelativeMatch && /^[\w]\/\//.test( protocolRelativeMatch ) ) // a protocol-relative match which has a word character in front of it (so we can skip something like "abc//google.com") | ||
) { | ||
return matchStr; | ||
return this; | ||
}, | ||
/** | ||
* Retrieves the inner HTML for the tag. | ||
* | ||
* @return {String} | ||
*/ | ||
getInnerHtml : function() { | ||
return this.innerHtml || ""; | ||
}, | ||
/** | ||
* Override of superclass method used to generate the HTML string for the tag. | ||
* | ||
* @return {String} | ||
*/ | ||
toString : function() { | ||
var tagName = this.getTagName(), | ||
attrsStr = this.buildAttrsStr(); | ||
attrsStr = ( attrsStr ) ? ' ' + attrsStr : ''; // prepend a space if there are actually attributes | ||
return [ '<', tagName, attrsStr, '>', this.getInnerHtml(), '</', tagName, '>' ].join( "" ); | ||
}, | ||
/** | ||
* Support method for {@link #toString}, returns the string space-separated key="value" pairs, used to populate | ||
* the stringified HtmlTag. | ||
* | ||
* @protected | ||
* @return {String} Example return: `attr1="value1" attr2="value2"` | ||
*/ | ||
buildAttrsStr : function() { | ||
if( !this.attrs ) return ""; // no `attrs` Object (map) has been set, return empty string | ||
var attrs = this.getAttrs(), | ||
attrsArr = []; | ||
for( var prop in attrs ) { | ||
if( attrs.hasOwnProperty( prop ) ) { | ||
attrsArr.push( prop + '="' + attrs[ prop ] + '"' ); | ||
} | ||
// Handle a closing parenthesis at the end of the match, and exclude it if there is not a matching open parenthesis | ||
// in the match. This handles cases like the string "wikipedia.com/something_(disambiguation)" (which should be auto- | ||
// linked, and when it is enclosed in parenthesis itself, such as: "(wikipedia.com/something_(disambiguation))" (in | ||
// which the outer parens should *not* be auto-linked. | ||
var lastChar = matchStr.charAt( matchStr.length - 1 ); | ||
if( lastChar === ')' ) { | ||
var openParensMatch = matchStr.match( /\(/g ), | ||
closeParensMatch = matchStr.match( /\)/g ), | ||
numOpenParens = ( openParensMatch && openParensMatch.length ) || 0, | ||
numCloseParens = ( closeParensMatch && closeParensMatch.length ) || 0; | ||
if( numOpenParens < numCloseParens ) { | ||
matchStr = matchStr.substr( 0, matchStr.length - 1 ); // remove the trailing ")" | ||
suffixStr = ")"; // this will be added after the <a> tag | ||
} | ||
} | ||
var anchorHref = matchStr, // initialize both of these | ||
anchorText = matchStr, // values as the full match | ||
linkType; | ||
// Process the urls that are found. We need to change URLs like "www.yahoo.com" to "http://www.yahoo.com" (or the browser | ||
// will try to direct the user to "http://current-domain.com/www.yahoo.com"), and we need to prefix 'mailto:' to email addresses. | ||
if( twitterMatch ) { | ||
linkType = 'twitter'; | ||
prefixStr = twitterHandlePrefixWhitespaceChar; | ||
anchorHref = 'https://twitter.com/' + twitterHandle; | ||
anchorText = '@' + twitterHandle; | ||
} else if( emailAddress ) { | ||
linkType = 'email'; | ||
anchorHref = 'mailto:' + emailAddress; | ||
anchorText = emailAddress; | ||
} else { // url match | ||
linkType = 'url'; | ||
if( protocolRelativeMatch ) { | ||
// Strip off any protocol-relative '//' from the anchor text (leaving the previous non-word character | ||
// intact, if there is one) | ||
var protocolRelRegex = new RegExp( "^" + me.protocolRelativeRegex.source ), // for this one, we want to only match at the beginning of the string | ||
charBeforeMatch = protocolRelativeMatch.match( protocolRelRegex )[ 1 ] || ""; | ||
prefixStr = charBeforeMatch + prefixStr; // re-add the character before the '//' to what will be placed before the <a> tag | ||
anchorHref = anchorHref.replace( protocolRelRegex, "//" ); // remove the char before the match for the href | ||
anchorText = anchorText.replace( protocolRelRegex, "" ); // remove both the char before the match and the '//' for the anchor text | ||
} else if( !/^[A-Za-z]{3,9}:/i.test( anchorHref ) ) { | ||
// url string doesn't begin with a protocol, assume http:// | ||
anchorHref = 'http://' + anchorHref; | ||
} | ||
} | ||
// wrap the match in an anchor tag | ||
var anchorTag = me.createAnchorTag( linkType, anchorHref, anchorText ); | ||
return prefixStr + anchorTag + suffixStr; | ||
} ); | ||
} | ||
return attrsArr.join( " " ); | ||
} | ||
} ); | ||
/*global Autolinker */ | ||
/*jshint sub:true */ | ||
/** | ||
* @protected | ||
* @class Autolinker.AnchorTagBuilder | ||
* @extends Object | ||
* | ||
* Builds anchor (<a>) tags for the Autolinker utility when a match is found. | ||
*/ | ||
Autolinker.AnchorTagBuilder = Autolinker.Util.extend( Object, { | ||
/** | ||
* @cfg {Boolean} newWindow | ||
* | ||
* See {@link Autolinker#newWindow} for details. | ||
*/ | ||
/** | ||
* @cfg {Number} truncate | ||
* | ||
* See {@link Autolinker#truncate} for details. | ||
*/ | ||
/** | ||
* @cfg {String} className | ||
* | ||
* See {@link Autolinker#className} for details. | ||
*/ | ||
/** | ||
* @constructor | ||
* @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). | ||
*/ | ||
constructor : function( cfg ) { | ||
Autolinker.Util.assign( this, cfg ); | ||
}, | ||
@@ -468,15 +1204,16 @@ | ||
/** | ||
* Generates the actual anchor (<a>) tag to use in place of a source url/email/twitter link. | ||
* Generates the actual anchor (<a>) tag to use in place of piece of source URL/email/Twitter text, | ||
* via its `match` object. | ||
* | ||
* @private | ||
* @param {"url"/"email"/"twitter"} linkType The type of link that an anchor tag is being generated for. | ||
* @param {String} anchorHref The href for the anchor tag. | ||
* @param {String} anchorText The anchor tag's text (i.e. what will be displayed). | ||
* @return {String} The full HTML for the anchor tag. | ||
* @param {Autolinker.match.Match} match The Match instance to generate an anchor tag from. | ||
* @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag. | ||
*/ | ||
createAnchorTag : function( linkType, anchorHref, anchorText ) { | ||
var attributesStr = this.createAnchorAttrsStr( linkType, anchorHref ); | ||
anchorText = this.processAnchorText( anchorText ); | ||
build : function( match ) { | ||
var tag = new Autolinker.HtmlTag( { | ||
tagName : 'a', | ||
attrs : this.createAttrs( match.getType(), match.getAnchorHref() ), | ||
innerHtml : this.processAnchorText( match.getAnchorText() ) | ||
} ); | ||
return '<a ' + attributesStr + '>' + anchorText + '</a>'; | ||
return tag; | ||
}, | ||
@@ -486,21 +1223,23 @@ | ||
/** | ||
* Creates the string which will be the HTML attributes for the anchor (<a>) tag being generated. | ||
* Creates the Object (map) of the HTML attributes for the anchor (<a>) tag being generated. | ||
* | ||
* @private | ||
* @param {"url"/"email"/"twitter"} linkType The type of link that an anchor tag is being generated for. | ||
* @protected | ||
* @param {"url"/"email"/"twitter"} matchType The type of match that an anchor tag is being generated for. | ||
* @param {String} href The href for the anchor tag. | ||
* @return {String} The anchor tag's attribute. Ex: `href="http://google.com" class="myLink myLink-url" target="_blank"` | ||
* @return {Object} A key/value Object (map) of the anchor tag's attributes. | ||
*/ | ||
createAnchorAttrsStr : function( linkType, anchorHref ) { | ||
var attrs = [ 'href="' + anchorHref + '"' ]; // we'll always have the `href` attribute | ||
createAttrs : function( matchType, anchorHref ) { | ||
var attrs = { | ||
'href' : anchorHref // we'll always have the `href` attribute | ||
}; | ||
var cssClass = this.createCssClass( linkType ); | ||
var cssClass = this.createCssClass( matchType ); | ||
if( cssClass ) { | ||
attrs.push( 'class="' + cssClass + '"' ); | ||
attrs[ 'class' ] = cssClass; | ||
} | ||
if( this.newWindow ) { | ||
attrs.push( 'target="_blank"' ); | ||
attrs[ 'target' ] = "_blank"; | ||
} | ||
return attrs.join( " " ); | ||
return attrs; | ||
}, | ||
@@ -510,11 +1249,11 @@ | ||
/** | ||
* Creates the CSS class that will be used for a given anchor tag, based on the `linkType` and the {@link #className} | ||
* Creates the CSS class that will be used for a given anchor tag, based on the `matchType` and the {@link #className} | ||
* config. | ||
* | ||
* @private | ||
* @param {"url"/"email"/"twitter"} linkType The type of link that an anchor tag is being generated for. | ||
* @param {"url"/"email"/"twitter"} matchType The type of match that an anchor tag is being generated for. | ||
* @return {String} The CSS class string for the link. Example return: "myLink myLink-url". If no {@link #className} | ||
* was configured, returns an empty string. | ||
*/ | ||
createCssClass : function( linkType ) { | ||
createCssClass : function( matchType ) { | ||
var className = this.className; | ||
@@ -525,3 +1264,3 @@ | ||
else | ||
return className + " " + className + "-" + linkType; // ex: "myLink myLink-url", "myLink myLink-email", or "myLink myLink-twitter" | ||
return className + " " + className + "-" + matchType; // ex: "myLink myLink-url", "myLink myLink-email", or "myLink myLink-twitter" | ||
}, | ||
@@ -539,2 +1278,287 @@ | ||
processAnchorText : function( anchorText ) { | ||
anchorText = this.doTruncate( anchorText ); | ||
return anchorText; | ||
}, | ||
/** | ||
* Performs the truncation of the `anchorText`, if the `anchorText` is longer than the {@link #truncate} option. | ||
* Truncates the text to 2 characters fewer than the {@link #truncate} option, and adds ".." to the end. | ||
* | ||
* @private | ||
* @param {String} text The anchor tag's text (i.e. what will be displayed). | ||
* @return {String} The truncated anchor text. | ||
*/ | ||
doTruncate : function( anchorText ) { | ||
return Autolinker.Util.ellipsis( anchorText, this.truncate || Number.POSITIVE_INFINITY ); | ||
} | ||
} ); | ||
/*global Autolinker */ | ||
/** | ||
* @private | ||
* @abstract | ||
* @class Autolinker.match.Match | ||
* | ||
* Represents a match found in an input string which should be Autolinked. | ||
*/ | ||
Autolinker.match.Match = Autolinker.Util.extend( Object, { | ||
/** | ||
* @constructor | ||
* @param {Object} cfg The configuration properties for the Match instance, specified in an Object (map). | ||
*/ | ||
constructor : function( cfg ) { | ||
Autolinker.Util.assign( this, cfg ); | ||
}, | ||
/** | ||
* Returns a string name for the type of match that this class represents. | ||
* | ||
* @abstract | ||
* @return {String} | ||
*/ | ||
getType : Autolinker.Util.abstractMethod, | ||
/** | ||
* Returns the anchor href that should be generated for the match. | ||
* | ||
* @abstract | ||
* @return {String} | ||
*/ | ||
getAnchorHref : Autolinker.Util.abstractMethod, | ||
/** | ||
* Returns the anchor text that should be generated for the match. | ||
* | ||
* @abstract | ||
* @return {String} | ||
*/ | ||
getAnchorText : Autolinker.Util.abstractMethod | ||
} ); | ||
/*global Autolinker */ | ||
/** | ||
* @private | ||
* @class Autolinker.match.Email | ||
* | ||
* Represents a Email match found in an input string which should be Autolinked. | ||
*/ | ||
Autolinker.match.Email = Autolinker.Util.extend( Autolinker.match.Match, { | ||
/** | ||
* @cfg {String} email (required) | ||
* | ||
* The email address that was matched. | ||
*/ | ||
/** | ||
* Returns a string name for the type of match that this class represents. | ||
* | ||
* @return {String} | ||
*/ | ||
getType : function() { | ||
return 'email'; | ||
}, | ||
/** | ||
* Returns the email address that was matched. | ||
* | ||
* @return {String} | ||
*/ | ||
getEmail : function() { | ||
return this.email; | ||
}, | ||
/** | ||
* Returns the anchor href that should be generated for the match. | ||
* | ||
* @return {String} | ||
*/ | ||
getAnchorHref : function() { | ||
return 'mailto:' + this.email; | ||
}, | ||
/** | ||
* Returns the anchor text that should be generated for the match. | ||
* | ||
* @return {String} | ||
*/ | ||
getAnchorText : function() { | ||
return this.email; | ||
} | ||
} ); | ||
/*global Autolinker */ | ||
/** | ||
* @private | ||
* @class Autolinker.match.Twitter | ||
* | ||
* Represents a Twitter match found in an input string which should be Autolinked. | ||
*/ | ||
Autolinker.match.Twitter = Autolinker.Util.extend( Autolinker.match.Match, { | ||
/** | ||
* @cfg {String} twitterHandle (required) | ||
* | ||
* The Twitter handle that was matched. | ||
*/ | ||
/** | ||
* Returns the type of match that this class represents. | ||
* | ||
* @return {String} | ||
*/ | ||
getType : function() { | ||
return 'twitter'; | ||
}, | ||
/** | ||
* Returns a string name for the type of match that this class represents. | ||
* | ||
* @return {String} | ||
*/ | ||
getTwitterHandle : function() { | ||
return this.twitterHandle; | ||
}, | ||
/** | ||
* Returns the anchor href that should be generated for the match. | ||
* | ||
* @return {String} | ||
*/ | ||
getAnchorHref : function() { | ||
return 'https://twitter.com/' + this.twitterHandle; | ||
}, | ||
/** | ||
* Returns the anchor text that should be generated for the match. | ||
* | ||
* @return {String} | ||
*/ | ||
getAnchorText : function() { | ||
return '@' + this.twitterHandle; | ||
} | ||
} ); | ||
/*global Autolinker */ | ||
/** | ||
* @private | ||
* @class Autolinker.match.Twitter | ||
* | ||
* Represents a Url match found in an input string which should be Autolinked. | ||
*/ | ||
Autolinker.match.Url = Autolinker.Util.extend( Autolinker.match.Match, { | ||
/** | ||
* @cfg {String} url (required) | ||
* | ||
* The url that was matched. | ||
*/ | ||
/** | ||
* @cfg {Boolean} protocolRelativeMatch (required) | ||
* | ||
* `true` if the URL is a protocol-relative match. A protocol-relative match is a URL that starts with '//', | ||
* and will be either http:// or https:// based on the protocol that the site is loaded under. | ||
*/ | ||
/** | ||
* @cfg {Boolean} stripPrefix (required) | ||
* | ||
* See {@link Autolinker#stripPrefix} for details. | ||
*/ | ||
/** | ||
* @private | ||
* @property {RegExp} urlPrefixRegex | ||
* | ||
* A regular expression used to remove the 'http://' or 'https://' and/or the 'www.' from URLs. | ||
*/ | ||
urlPrefixRegex: /^(https?:\/\/)?(www\.)?/i, | ||
/** | ||
* @private | ||
* @property {RegExp} protocolRelativeRegex | ||
* | ||
* The regular expression used to remove the protocol-relative '//' from the {@link #url} string, for purposes | ||
* of {@link #getAnchorText}. A protocol-relative URL is, for example, "//yahoo.com" | ||
*/ | ||
protocolRelativeRegex : /^\/\//, | ||
/** | ||
* @protected | ||
* @property {RegExp} checkForProtocolRegex | ||
* | ||
* A regular expression used to check if the {@link #url} is missing a protocol (in which case, 'http://' | ||
* will be added). | ||
*/ | ||
checkForProtocolRegex: /^[A-Za-z]{3,9}:/, | ||
/** | ||
* Returns a string name for the type of match that this class represents. | ||
* | ||
* @return {String} | ||
*/ | ||
getType : function() { | ||
return 'url'; | ||
}, | ||
/** | ||
* Returns the url that was matched, assuming the protocol to be 'http://' if the match | ||
* was missing a protocol. | ||
* | ||
* @return {String} | ||
*/ | ||
getUrl : function() { | ||
var url = this.url; | ||
// if the url string doesn't begin with a protocol, assume http:// | ||
if( !this.protocolRelativeMatch && !this.checkForProtocolRegex.test( url ) ) { | ||
url = this.url = 'http://' + url; | ||
} | ||
return url; | ||
}, | ||
/** | ||
* Returns the anchor href that should be generated for the match. | ||
* | ||
* @return {String} | ||
*/ | ||
getAnchorHref : function() { | ||
var url = this.getUrl(); | ||
return url.replace( /&/g, '&' ); // any &'s in the URL should be converted back to '&' if they were displayed as & in the source html | ||
}, | ||
/** | ||
* Returns the anchor text that should be generated for the match. | ||
* | ||
* @return {String} | ||
*/ | ||
getAnchorText : function() { | ||
var anchorText = this.getUrl(); | ||
if( this.protocolRelativeMatch ) { | ||
// Strip off any protocol-relative '//' from the anchor text | ||
anchorText = this.stripProtocolRelativePrefix( anchorText ); | ||
} | ||
if( this.stripPrefix ) { | ||
@@ -544,3 +1568,2 @@ anchorText = this.stripUrlPrefix( anchorText ); | ||
anchorText = this.removeTrailingSlash( anchorText ); // remove trailing slash, if there is one | ||
anchorText = this.doTruncate( anchorText ); | ||
@@ -551,2 +1574,6 @@ return anchorText; | ||
// --------------------------------------- | ||
// Utility Functionality | ||
/** | ||
@@ -566,5 +1593,18 @@ * Strips the URL prefix (such as "http://" or "https://") from the given text. | ||
/** | ||
* Removes any trailing slash from the given `anchorText`, in prepration for the text to be displayed. | ||
* Strips any protocol-relative '//' from the anchor text. | ||
* | ||
* @private | ||
* @param {String} text The text of the anchor that is being generated, for which to strip off the | ||
* protocol-relative prefix (such as stripping off "//") | ||
* @return {String} The `anchorText`, with the protocol-relative prefix stripped. | ||
*/ | ||
stripProtocolRelativePrefix : function( text ) { | ||
return text.replace( this.protocolRelativeRegex, '' ); | ||
}, | ||
/** | ||
* Removes any trailing slash from the given `anchorText`, in preparation for the text to be displayed. | ||
* | ||
* @private | ||
* @param {String} anchorText The text of the anchor that is being generated, for which to remove any trailing | ||
@@ -581,51 +1621,6 @@ * slash ('/') that may exist. | ||
/** | ||
* Performs the truncation of the `anchorText`, if the `anchorText` is longer than the {@link #truncate} option. | ||
* Truncates the text to 2 characters fewer than the {@link #truncate} option, and adds ".." to the end. | ||
* | ||
* @private | ||
* @param {String} text The anchor tag's text (i.e. what will be displayed). | ||
* @return {String} The truncated anchor text. | ||
*/ | ||
doTruncate : function( anchorText ) { | ||
var truncateLen = this.truncate; | ||
// Truncate the anchor text if it is longer than the provided 'truncate' option | ||
if( truncateLen && anchorText.length > truncateLen ) { | ||
anchorText = anchorText.substring( 0, truncateLen - 2 ) + '..'; | ||
} | ||
return anchorText; | ||
} | ||
}; | ||
/** | ||
* Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. | ||
* Does not link URLs found within HTML tags. | ||
* | ||
* For instance, if given the text: `You should go to http://www.yahoo.com`, then the result | ||
* will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` | ||
* | ||
* Example: | ||
* | ||
* var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); | ||
* // Produces: "Go to <a href="http://google.com">google.com</a>" | ||
* | ||
* @static | ||
* @method link | ||
* @param {String} html The HTML text to link URLs within. | ||
* @param {Object} [options] Any of the configuration options for the Autolinker class, specified in an Object (map). | ||
* See the class description for an example call. | ||
* @return {String} The HTML text, with URLs automatically linked | ||
*/ | ||
Autolinker.link = function( text, options ) { | ||
var autolinker = new Autolinker( options ); | ||
return autolinker.link( text ); | ||
}; | ||
} ); | ||
return Autolinker; | ||
} ) ); | ||
} ) ); |
/*! | ||
* Autolinker.js | ||
* 0.11.2 | ||
* 0.12.1 | ||
* | ||
@@ -10,2 +10,2 @@ * Copyright(c) 2014 Gregory Jacobs <greg@greg-jacobs.com> | ||
*/ | ||
!function(a,b){"function"==typeof define&&define.amd?define(b):"undefined"!=typeof exports?module.exports=b():a.Autolinker=b()}(this,function(){var a=function(a){a=a||{};for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b])};return a.prototype={constructor:a,newWindow:!0,stripPrefix:!0,twitter:!0,email:!0,urls:!0,className:"",matcherRegex:function(){var a=/(^|[^\w])@(\w{1,15})/,b=/(?:[\-;:&=\+\$,\w\.]+@)/,c=/(?:[A-Za-z]{3,9}:(?:\/\/)?)/,d=/(?:www\.)/,e=/[A-Za-z0-9\.\-]*[A-Za-z0-9\-]/,f=/\.(?:international|construction|contractors|enterprises|photography|productions|foundation|immobilien|industries|management|properties|technology|christmas|community|directory|education|equipment|institute|marketing|solutions|vacations|bargains|boutique|builders|catering|cleaning|clothing|computer|democrat|diamonds|graphics|holdings|lighting|partners|plumbing|supplies|training|ventures|academy|careers|company|cruises|domains|exposed|flights|florist|gallery|guitars|holiday|kitchen|neustar|okinawa|recipes|rentals|reviews|shiksha|singles|support|systems|agency|berlin|camera|center|coffee|condos|dating|estate|events|expert|futbol|kaufen|luxury|maison|monash|museum|nagoya|photos|repair|report|social|supply|tattoo|tienda|travel|viajes|villas|vision|voting|voyage|actor|build|cards|cheap|codes|dance|email|glass|house|mango|ninja|parts|photo|shoes|solar|today|tokyo|tools|watch|works|aero|arpa|asia|best|bike|blue|buzz|camp|club|cool|coop|farm|fish|gift|guru|info|jobs|kiwi|kred|land|limo|link|menu|mobi|moda|name|pics|pink|post|qpon|rich|ruhr|sexy|tips|vote|voto|wang|wien|wiki|zone|bar|bid|biz|cab|cat|ceo|com|edu|gov|int|kim|mil|net|onl|org|pro|pub|red|tel|uno|wed|xxx|xyz|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)\b/,g=/(?:[\-A-Za-z0-9+&@#\/%?=~_()|!:,.;]*[\-A-Za-z0-9+&@#\/%=~_()|])?/;return new RegExp(["(",a.source,")","|","(",b.source,e.source,f.source,")","|","(","(?:","(?:",c.source,e.source,")","|","(?:","(.?//)?",d.source,e.source,")","|","(?:","(.?//)?",e.source,f.source,")",")",g.source,")"].join(""),"gi")}(),protocolRelativeRegex:/(.)?\/\//,htmlRegex:function(){var a=/[0-9a-zA-Z:]+/,b=/[^\s\0"'>\/=\x01-\x1F\x7F]+/,c=/(?:".*?"|'.*?'|[^'"=<>`\s]+)/,d=b.source+"(?:\\s*=\\s*"+c.source+")?";return new RegExp(["<(?:!|(/))?","("+a.source+")","(?:","\\s+","(?:",d,"|",c.source+")",")*","\\s*/?",">"].join(""),"g")}(),urlPrefixRegex:/^(https?:\/\/)?(www\.)?/i,link:function(a){return this.processHtml(a)},processHtml:function(a){for(var b,c,d=this.htmlRegex,e=0,f=0,g=[];null!==(b=d.exec(a));){var h=b[0],i=b[2],j=!!b[1];c=a.substring(e,b.index),e=b.index+h.length,"a"===i?j?(f=Math.max(f-1,0),0===f&&g.push(c)):(f++,g.push(this.processTextNode(c))):g.push(0===f?this.processTextNode(c):c),g.push(h)}if(e<a.length){var k=this.processTextNode(a.substring(e));g.push(k)}return g.join("")},processTextNode:function(a){var b=this,c=this.matcherRegex,d=this.twitter,e=this.email,f=this.urls;return a.replace(c,function(a,c,g,h,i,j,k,l){var m=c,n=g,o=h,p=i,q=j,r=k||l,s="",t="";if(m&&!d||p&&!e||q&&!f||q&&-1===q.indexOf(".")||q&&/^[A-Za-z]{3,9}:/.test(q)&&!/:.*?[A-Za-z]/.test(q)||r&&/^[\w]\/\//.test(r))return a;var u=a.charAt(a.length-1);if(")"===u){var v=a.match(/\(/g),w=a.match(/\)/g),x=v&&v.length||0,y=w&&w.length||0;y>x&&(a=a.substr(0,a.length-1),t=")")}var z,A=a,B=a;if(m)z="twitter",s=n,A="https://twitter.com/"+o,B="@"+o;else if(p)z="email",A="mailto:"+p,B=p;else if(z="url",r){var C=new RegExp("^"+b.protocolRelativeRegex.source),D=r.match(C)[1]||"";s=D+s,A=A.replace(C,"//"),B=B.replace(C,"")}else/^[A-Za-z]{3,9}:/i.test(A)||(A="http://"+A);var E=b.createAnchorTag(z,A,B);return s+E+t})},createAnchorTag:function(a,b,c){var d=this.createAnchorAttrsStr(a,b);return c=this.processAnchorText(c),"<a "+d+">"+c+"</a>"},createAnchorAttrsStr:function(a,b){var c=['href="'+b+'"'],d=this.createCssClass(a);return d&&c.push('class="'+d+'"'),this.newWindow&&c.push('target="_blank"'),c.join(" ")},createCssClass:function(a){var b=this.className;return b?b+" "+b+"-"+a:""},processAnchorText:function(a){return this.stripPrefix&&(a=this.stripUrlPrefix(a)),a=this.removeTrailingSlash(a),a=this.doTruncate(a)},stripUrlPrefix:function(a){return a.replace(this.urlPrefixRegex,"")},removeTrailingSlash:function(a){return"/"===a.charAt(a.length-1)&&(a=a.slice(0,-1)),a},doTruncate:function(a){var b=this.truncate;return b&&a.length>b&&(a=a.substring(0,b-2)+".."),a}},a.link=function(b,c){var d=new a(c);return d.link(b)},a}); | ||
!function(a,b){"function"==typeof define&&define.amd?define(b):"undefined"!=typeof exports?module.exports=b():a.Autolinker=b()}(this,function(){var a=function(b){a.Util.assign(this,b)};return a.prototype={constructor:a,urls:!0,email:!0,twitter:!0,newWindow:!0,stripPrefix:!0,className:"",htmlCharacterEntitiesRegex:/( | |<|<|>|>)/gi,matcherRegex:function(){var a=/(^|[^\w])@(\w{1,15})/,b=/(?:[\-;:&=\+\$,\w\.]+@)/,c=/(?:[A-Za-z]{3,9}:(?:\/\/)?)/,d=/(?:www\.)/,e=/[A-Za-z0-9\.\-]*[A-Za-z0-9\-]/,f=/\.(?:international|construction|contractors|enterprises|photography|productions|foundation|immobilien|industries|management|properties|technology|christmas|community|directory|education|equipment|institute|marketing|solutions|vacations|bargains|boutique|builders|catering|cleaning|clothing|computer|democrat|diamonds|graphics|holdings|lighting|partners|plumbing|supplies|training|ventures|academy|careers|company|cruises|domains|exposed|flights|florist|gallery|guitars|holiday|kitchen|neustar|okinawa|recipes|rentals|reviews|shiksha|singles|support|systems|agency|berlin|camera|center|coffee|condos|dating|estate|events|expert|futbol|kaufen|luxury|maison|monash|museum|nagoya|photos|repair|report|social|supply|tattoo|tienda|travel|viajes|villas|vision|voting|voyage|actor|build|cards|cheap|codes|dance|email|glass|house|mango|ninja|parts|photo|shoes|solar|today|tokyo|tools|watch|works|aero|arpa|asia|best|bike|blue|buzz|camp|club|cool|coop|farm|fish|gift|guru|info|jobs|kiwi|kred|land|limo|link|menu|mobi|moda|name|pics|pink|post|qpon|rich|ruhr|sexy|tips|vote|voto|wang|wien|wiki|zone|bar|bid|biz|cab|cat|ceo|com|edu|gov|int|kim|mil|net|onl|org|pro|pub|red|tel|uno|wed|xxx|xyz|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)\b/,g=/(?:[\-A-Za-z0-9+&@#\/%?=~_()|!:,.;]*[\-A-Za-z0-9+&@#\/%=~_()|])?/;return new RegExp(["(",a.source,")","|","(",b.source,e.source,f.source,")","|","(","(?:","(?:",c.source,e.source,")","|","(?:","(.?//)?",d.source,e.source,")","|","(?:","(.?//)?",e.source,f.source,")",")",g.source,")"].join(""),"gi")}(),invalidProtocolRelMatchRegex:/^[\w]\/\//,charBeforeProtocolRelMatchRegex:/^(.)?\/\//,link:function(b){var c=this,d=this.getHtmlParser(),e=this.htmlCharacterEntitiesRegex,f=0,g=[];return d.parse(b,{processHtmlNode:function(a,b,c){"a"===b&&(c?f=Math.max(f-1,0):f++),g.push(a)},processTextNode:function(b){if(0===f)for(var d=a.Util.splitAndCapture(b,e),h=0,i=d.length;i>h;h++){var j=d[h],k=c.processTextNode(j);g.push(k)}else g.push(b)}}),g.join("")},getHtmlParser:function(){var b=this.htmlParser;return b||(b=this.htmlParser=new a.HtmlParser),b},getTagBuilder:function(){var b=this.tagBuilder;return b||(b=this.tagBuilder=new a.AnchorTagBuilder({newWindow:this.newWindow,truncate:this.truncate,className:this.className})),b},processTextNode:function(b){var c=this,d=this.charBeforeProtocolRelMatchRegex;return b.replace(this.matcherRegex,function(b,e,f,g,h,i,j,k){var l,m=e,n=f,o=g,p=h,q=i,r=j||k,s="",t="";if(!c.isValidMatch(m,p,q,r))return b;if(c.matchHasUnbalancedClosingParen(b)&&(b=b.substr(0,b.length-1),t=")"),p)l=new a.match.Email({email:p});else if(m)n&&(s=n,b=b.slice(1)),l=new a.match.Twitter({twitterHandle:o});else{if(r){var u=r.match(d)[1]||"";u&&(s=u,b=b.slice(1))}l=new a.match.Url({url:b,protocolRelativeMatch:r,stripPrefix:c.stripPrefix})}var v=c.createMatchReturnVal(l,b);return s+v+t})},isValidMatch:function(a,b,c,d){return a&&!this.twitter||b&&!this.email||c&&!this.urls||c&&-1===c.indexOf(".")||c&&/^[A-Za-z]{3,9}:/.test(c)&&!/:.*?[A-Za-z]/.test(c)||d&&this.invalidProtocolRelMatchRegex.test(d)?!1:!0},matchHasUnbalancedClosingParen:function(a){var b=a.charAt(a.length-1);if(")"===b){var c=a.match(/\(/g),d=a.match(/\)/g),e=c&&c.length||0,f=d&&d.length||0;if(f>e)return!0}return!1},createMatchReturnVal:function(b,c){var d;if(this.replaceFn&&(d=this.replaceFn.call(this,this,b)),"string"==typeof d)return d;if(d===!1)return c;if(d instanceof a.HtmlTag)return d.toString();var e=this.getTagBuilder(),f=e.build(b);return f.toString()}},a.link=function(b,c){var d=new a(c);return d.link(b)},a.match={},a.Util={abstractMethod:function(){throw"abstract"},assign:function(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return a},extend:function(b,c){var d=b.prototype,e=function(){};e.prototype=d;var f;f=c.hasOwnProperty("constructor")?c.constructor:function(){d.constructor.apply(this,arguments)};var g=f.prototype=new e;return g.constructor=f,g.superclass=d,delete c.constructor,a.Util.assign(g,c),f},ellipsis:function(a,b,c){return a.length>b&&(c=null==c?"..":c,a=a.substring(0,b-c.length)+c),a},indexOf:function(a,b){if(Array.prototype.indexOf)return a.indexOf(b);for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},splitAndCapture:function(a,b){if(!b.global)throw new Error("`splitRegex` must have the 'g' flag set");for(var c,d=[],e=0;c=b.exec(a);)d.push(a.substring(e,c.index)),d.push(c[0]),e=c.index+c[0].length;return d.push(a.substring(e)),d}},a.HtmlParser=a.Util.extend(Object,{htmlRegex:function(){var a=/[0-9a-zA-Z:]+/,b=/[^\s\0"'>\/=\x01-\x1F\x7F]+/,c=/(?:".*?"|'.*?'|[^'"=<>`\s]+)/,d=b.source+"(?:\\s*=\\s*"+c.source+")?";return new RegExp(["<(?:!|(/))?","("+a.source+")","(?:","\\s+","(?:",d,"|",c.source+")",")*","\\s*/?",">"].join(""),"g")}(),parse:function(a,b){b=b||{};for(var c,d=b.processHtmlNode||function(){},e=b.processTextNode||function(){},f=this.htmlRegex,g=0;null!==(c=f.exec(a));){var h=c[0],i=c[2],j=!!c[1],k=a.substring(g,c.index);k&&e(k),d(h,i,j),g=c.index+h.length}if(g<a.length){var l=a.substring(g);l&&e(l)}}}),a.HtmlTag=a.Util.extend(Object,{whitespaceRegex:/\s+/,constructor:function(b){a.Util.assign(this,b),this.innerHtml=this.innerHtml||this.innerHTML},setTagName:function(a){return this.tagName=a,this},getTagName:function(){return this.tagName||""},setAttr:function(a,b){var c=this.getAttrs();return c[a]=b,this},getAttr:function(a){return this.getAttrs()[a]},setAttrs:function(b){var c=this.getAttrs();return a.Util.assign(c,b),this},getAttrs:function(){return this.attrs||(this.attrs={})},setClass:function(a){return this.setAttr("class",a)},addClass:function(b){for(var c,d=this.getClass(),e=this.whitespaceRegex,f=a.Util.indexOf,g=d?d.split(e):[],h=b.split(e);c=h.shift();)-1===f(g,c)&&g.push(c);return this.getAttrs()["class"]=g.join(" "),this},removeClass:function(b){for(var c,d=this.getClass(),e=this.whitespaceRegex,f=a.Util.indexOf,g=d?d.split(e):[],h=b.split(e);g.length&&(c=h.shift());){var i=f(g,c);-1!==i&&g.splice(i,1)}return this.getAttrs()["class"]=g.join(" "),this},getClass:function(){return this.getAttrs()["class"]||""},hasClass:function(a){return-1!==(" "+this.getClass()+" ").indexOf(" "+a+" ")},setInnerHtml:function(a){return this.innerHtml=a,this},getInnerHtml:function(){return this.innerHtml||""},toString:function(){var a=this.getTagName(),b=this.buildAttrsStr();return b=b?" "+b:"",["<",a,b,">",this.getInnerHtml(),"</",a,">"].join("")},buildAttrsStr:function(){if(!this.attrs)return"";var a=this.getAttrs(),b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c+'="'+a[c]+'"');return b.join(" ")}}),a.AnchorTagBuilder=a.Util.extend(Object,{constructor:function(b){a.Util.assign(this,b)},build:function(b){var c=new a.HtmlTag({tagName:"a",attrs:this.createAttrs(b.getType(),b.getAnchorHref()),innerHtml:this.processAnchorText(b.getAnchorText())});return c},createAttrs:function(a,b){var c={href:b},d=this.createCssClass(a);return d&&(c["class"]=d),this.newWindow&&(c.target="_blank"),c},createCssClass:function(a){var b=this.className;return b?b+" "+b+"-"+a:""},processAnchorText:function(a){return a=this.doTruncate(a)},doTruncate:function(b){return a.Util.ellipsis(b,this.truncate||Number.POSITIVE_INFINITY)}}),a.match.Match=a.Util.extend(Object,{constructor:function(b){a.Util.assign(this,b)},getType:a.Util.abstractMethod,getAnchorHref:a.Util.abstractMethod,getAnchorText:a.Util.abstractMethod}),a.match.Email=a.Util.extend(a.match.Match,{getType:function(){return"email"},getEmail:function(){return this.email},getAnchorHref:function(){return"mailto:"+this.email},getAnchorText:function(){return this.email}}),a.match.Twitter=a.Util.extend(a.match.Match,{getType:function(){return"twitter"},getTwitterHandle:function(){return this.twitterHandle},getAnchorHref:function(){return"https://twitter.com/"+this.twitterHandle},getAnchorText:function(){return"@"+this.twitterHandle}}),a.match.Url=a.Util.extend(a.match.Match,{urlPrefixRegex:/^(https?:\/\/)?(www\.)?/i,protocolRelativeRegex:/^\/\//,checkForProtocolRegex:/^[A-Za-z]{3,9}:/,getType:function(){return"url"},getUrl:function(){var a=this.url;return this.protocolRelativeMatch||this.checkForProtocolRegex.test(a)||(a=this.url="http://"+a),a},getAnchorHref:function(){var a=this.getUrl();return a.replace(/&/g,"&")},getAnchorText:function(){var a=this.getUrl();return this.protocolRelativeMatch&&(a=this.stripProtocolRelativePrefix(a)),this.stripPrefix&&(a=this.stripUrlPrefix(a)),a=this.removeTrailingSlash(a)},stripUrlPrefix:function(a){return a.replace(this.urlPrefixRegex,"")},stripProtocolRelativePrefix:function(a){return a.replace(this.protocolRelativeRegex,"")},removeTrailingSlash:function(a){return"/"===a.charAt(a.length-1)&&(a=a.slice(0,-1)),a}}),a}); |
220
Gruntfile.js
@@ -0,72 +1,156 @@ | ||
/*global module */ | ||
module.exports = function(grunt) { | ||
'use strict'; | ||
'use strict'; | ||
var banner = createBanner(); | ||
// Project configuration. | ||
grunt.initConfig( { | ||
pkg: grunt.file.readJSON( 'package.json' ), | ||
connect: { | ||
server: { | ||
options: { | ||
hostname: '*', | ||
port: 3000, | ||
base: '.' | ||
} | ||
} | ||
}, | ||
jshint: { | ||
files: { | ||
src: [ 'src/**/*.js', 'tests/**/*.js' ] | ||
} | ||
}, | ||
jasmine: { | ||
dist: { | ||
options: { | ||
specs: 'tests/*Spec.js', | ||
}, | ||
src: 'dist/Autolinker.min.js' | ||
} | ||
}, | ||
concat: { | ||
development: { | ||
options: { | ||
banner : banner + createDistFileHeader(), | ||
footer : createDistFileFooter(), | ||
nonull : true, | ||
process : function( src, filepath ) { | ||
return '\t' + src.replace( /\n/g, '\n\t' ); // indent each source file, which is placed inside the UMD block | ||
} | ||
}, | ||
src: [ | ||
'src/umdBegin.js', | ||
'src/Autolinker.js', | ||
'src/Util.js', | ||
'src/HtmlParser.js', | ||
'src/HtmlTag.js', | ||
'src/AnchorTagBuilder.js', | ||
'src/match/Match.js', | ||
'src/match/Email.js', | ||
'src/match/Twitter.js', | ||
'src/match/Url.js', | ||
'src/umdEnd.js' | ||
], | ||
dest: 'dist/Autolinker.js', | ||
}, | ||
}, | ||
uglify: { | ||
production: { | ||
options: { | ||
banner: banner | ||
}, | ||
src: [ 'dist/Autolinker.js' ], | ||
dest: 'dist/Autolinker.min.js', | ||
} | ||
} | ||
} ); | ||
var banner = [ | ||
'/*!', | ||
' * Autolinker.js', | ||
' * <%= pkg.version %>', | ||
' *', | ||
' * Copyright(c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>', | ||
' * <%= pkg.license %>', | ||
' *', | ||
' * <%= pkg.homepage %>', | ||
' */\n' | ||
].join('\n'); | ||
// Plugins | ||
grunt.loadNpmTasks( 'grunt-contrib-connect' ); | ||
grunt.loadNpmTasks( 'grunt-contrib-jasmine' ); | ||
grunt.loadNpmTasks( 'grunt-contrib-concat' ); | ||
grunt.loadNpmTasks( 'grunt-contrib-uglify' ); | ||
grunt.loadNpmTasks( 'grunt-contrib-jshint' ); | ||
// Project configuration. | ||
grunt.initConfig({ | ||
pkg: grunt.file.readJSON('package.json'), | ||
connect: { | ||
server: { | ||
options: { | ||
hostname: '*', | ||
port: 3000, | ||
base: '.' | ||
} | ||
} | ||
}, | ||
jasmine: { | ||
src: 'src/Autolinker.js', | ||
options: { | ||
specs: 'tests/*Spec.js', | ||
} | ||
}, | ||
concat: { | ||
development: { | ||
options: { | ||
banner: banner | ||
}, | ||
src: ['src/Autolinker.js'], | ||
dest: 'dist/Autolinker.js', | ||
}, | ||
}, | ||
uglify: { | ||
production: { | ||
options: { | ||
banner: banner | ||
}, | ||
src: ['src/Autolinker.js'], | ||
dest: 'dist/Autolinker.min.js', | ||
} | ||
}, | ||
jshint: { | ||
files: { | ||
src: ['src/**/*.js', 'tests/**/*.js'] | ||
} | ||
} | ||
}); | ||
// Plugins | ||
grunt.loadNpmTasks('grunt-contrib-connect'); | ||
grunt.loadNpmTasks('grunt-contrib-jasmine'); | ||
grunt.loadNpmTasks('grunt-contrib-concat'); | ||
grunt.loadNpmTasks('grunt-contrib-uglify'); | ||
grunt.loadNpmTasks('grunt-contrib-jshint'); | ||
// Tasks | ||
grunt.registerTask('default', ['lint', 'test', 'build']); | ||
grunt.registerTask('lint', ['jshint']); | ||
grunt.registerTask('test', ['jasmine']); | ||
grunt.registerTask('build', ['concat:development', 'uglify:production']); | ||
grunt.registerTask('serve', ['connect:server:keepalive']); | ||
// Tasks | ||
grunt.registerTask( 'default', [ 'lint', 'build', 'doTest' ] ); | ||
grunt.registerTask( 'lint', [ 'jshint' ] ); | ||
grunt.registerTask( 'test', [ 'build', 'doTest' ] ); | ||
grunt.registerTask( 'doTest', [ 'jasmine' ] ); | ||
grunt.registerTask( 'build', [ 'concat:development', 'uglify:production' ] ); | ||
grunt.registerTask( 'serve', [ 'connect:server:keepalive' ] ); | ||
/** | ||
* Creates the banner comment with license header that is placed over the concatenated/minified files. | ||
* | ||
* @private | ||
* @return {String} | ||
*/ | ||
function createBanner() { | ||
return [ | ||
'/*!', | ||
' * Autolinker.js', | ||
' * <%= pkg.version %>', | ||
' *', | ||
' * Copyright(c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>', | ||
' * <%= pkg.license %>', | ||
' *', | ||
' * <%= pkg.homepage %>', | ||
' */\n' | ||
].join( "\n" ); | ||
} | ||
/** | ||
* Creates the UMD (Universal Module Definition) header, which defines Autolinker as one of the following when loaded: | ||
* | ||
* 1. An AMD module, if an AMD loader is available (such as RequireJS) | ||
* 2. A CommonJS module, if a CommonJS module environment is available (such as Node.js), or | ||
* 3. A global variable if the others are unavailable. | ||
* | ||
* This UMD header is combined with the UMD footer to create the distribution JavaScript file. | ||
* | ||
* @private | ||
* @return {String} | ||
*/ | ||
function createDistFileHeader() { | ||
return [ | ||
"/*global define, module */", | ||
"( function( root, factory ) {\n", | ||
"\tif( typeof define === 'function' && define.amd ) {", | ||
"\t\tdefine( factory ); // Define as AMD module if an AMD loader is present (ex: RequireJS).", | ||
"\t} else if( typeof exports !== 'undefined' ) {", | ||
"\t\tmodule.exports = factory(); // Define as CommonJS module for Node.js, if available.", | ||
"\t} else {", | ||
"\t\troot.Autolinker = factory(); // Finally, define as a browser global if no module loader.", | ||
"\t}", | ||
"}( this, function() {\n\n" | ||
].join( "\n" ); | ||
} | ||
/** | ||
* Creates the UMD (Universal Module Definition) footer. See {@link #createDistFileHeader} for details. | ||
* | ||
* @private | ||
* @return {String} | ||
*/ | ||
function createDistFileFooter() { | ||
var umdEnd = [ | ||
'\n\n\treturn Autolinker;\n', | ||
'} ) );' | ||
]; | ||
return umdEnd.join( "\n" ); | ||
} | ||
}; |
{ | ||
"name": "autolinker", | ||
"version": "0.11.2", | ||
"description": "Simple utility to automatically link the URLs, email addresses, and Twitter handles in a given block of text/HTML", | ||
"version": "0.12.1", | ||
"description": "Utility to automatically link the URLs, email addresses, and Twitter handles in a given block of text/HTML", | ||
"main": "dist/Autolinker.js", | ||
@@ -6,0 +6,0 @@ "directories": { |
@@ -73,2 +73,5 @@ # Autolinker.js | ||
Note: if using the same options to autolink multiple pieces of html/text, it is slightly more efficient to create a single | ||
Autolinker instance, and run the `link()` method repeatedly (i.e. use the "class" form above). | ||
@@ -109,3 +112,7 @@ #### Example: | ||
`true` to have Twitter handles auto-linked, `false` to skip auto-linking of Twitter handles. Defaults to `true`. | ||
- **replaceFn** : Function<br /> | ||
A function to use to programmatically make replacements of matches in the input string, one at a time. See the section | ||
<a href="#custom-replacement-function">Custom Replacement Function</a> for more details. | ||
For example, if you wanted to disable links from opening in new windows, you could do: | ||
@@ -151,4 +158,80 @@ | ||
## Custom Replacement Function | ||
A custom replacement function (`replaceFn`) may be provided to replace url/email/twitter matches on an individual basis, based | ||
on the return from this function. | ||
Full example, for purposes of documenting the API: | ||
```javascript | ||
var input = "..."; // string with URLs, Email Addresses, and Twitter Handles | ||
var linkedText = Autolinker.link( input, { | ||
replaceFn : function( autolinker, match ) { | ||
console.log( "href = ", match.getAnchorHref() ); | ||
console.log( "text = ", match.getAnchorText() ); | ||
switch( match.getType() ) { | ||
case 'url' : | ||
console.log( "url: ", match.getUrl() ); | ||
if( match.getUrl().indexOf( 'mysite.com' ) === -1 ) { | ||
var tag = autolinker.getTagBuilder().build( match ); // returns an `Autolinker.HtmlTag` instance, which provides mutator methods for easy changes | ||
tag.setAttr( 'rel', 'nofollow' ); | ||
tag.addClass( 'external-link' ); | ||
return tag; | ||
} else { | ||
return true; // let Autolinker perform its normal anchor tag replacement | ||
} | ||
case 'email' : | ||
var email = match.getEmail(); | ||
console.log( "email: ", email ); | ||
if( email === "my@own.address" ) { | ||
return false; // don't auto-link this particular email address; leave as-is | ||
} else { | ||
return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) | ||
} | ||
case 'twitter' : | ||
var twitterHandle = match.getTwitterHandle(); | ||
console.log( twitterHandle ); | ||
return '<a href="http://newplace.to.link.twitter.handles.to/">' + twitterHandle + '</a>'; | ||
} | ||
} | ||
} ); | ||
``` | ||
The function is provided two arguments: | ||
1. The Autolinker instance that is performing replacements. This can be used to query the options that the Autolinker | ||
instance is configured with, or to retrieve its TagBuilder instance (via `autolinker.getTagBuilder()`). | ||
2. An `Autolinker.match.Match` object which details the match that is to be replaced. | ||
A replacement of the match is made based on the return value of the function. The following return values may be provided: | ||
- No return value (`undefined`), or `true` (Boolean): Delegate back to Autolinker to replace the match as it normally would. | ||
- `false` (Boolean): Do not replace the current match at all - leave as-is. | ||
- Any String: If a string is returned from the function, the string will be used directly as the replacement HTML for | ||
the match. | ||
- An `Autolinker.HtmlTag` instance, which can be used to build/modify an HTML tag before writing out its HTML text. | ||
## Changelog: | ||
### 0.12.1 | ||
- Expose the `Autolinker.HtmlTag` class, and allow it to be used in the `replaceFn` | ||
### 0.12.0 | ||
- Add `replaceFn` option | ||
### 0.11.0 | ||
@@ -155,0 +238,0 @@ |
@@ -1,602 +0,595 @@ | ||
/*global define, module */ | ||
/*jshint undef:true, smarttabs:true */ | ||
// Set up Autolinker appropriately for the environment. | ||
( function( root, factory ) { | ||
if( typeof define === 'function' && define.amd ) { | ||
define( factory ); // Define as AMD module if an AMD loader is present (ex: RequireJS). | ||
} else if( typeof exports !== 'undefined' ) { | ||
module.exports = factory(); // Define as CommonJS module for Node.js, if available. | ||
} else { | ||
root.Autolinker = factory(); // Finally, define as a browser global if no module loader. | ||
} | ||
}( this, function() { | ||
/** | ||
* @class Autolinker | ||
* @extends Object | ||
* | ||
* Utility class used to process a given string of text, and wrap the URLs, email addresses, and Twitter handles in | ||
* the appropriate anchor (<a>) tags to turn them into links. | ||
* | ||
* Any of the configuration options may be provided in an Object (map) provided to the Autolinker constructor, which | ||
* will configure how the {@link #link link()} method will process the links. | ||
* | ||
* For example: | ||
* | ||
* var autolinker = new Autolinker( { | ||
* newWindow : false, | ||
* truncate : 30 | ||
* } ); | ||
* | ||
* var html = autolinker.link( "Joe went to www.yahoo.com" ); | ||
* // produces: 'Joe went to <a href="http://www.yahoo.com">yahoo.com</a>' | ||
* | ||
* | ||
* The {@link #static-link static link()} method may also be used to inline options into a single call, which may | ||
* be more convenient for one-off uses. For example: | ||
* | ||
* var html = Autolinker.link( "Joe went to www.yahoo.com", { | ||
* newWindow : false, | ||
* truncate : 30 | ||
* } ); | ||
* // produces: 'Joe went to <a href="http://www.yahoo.com">yahoo.com</a>' | ||
* | ||
* | ||
* ## Custom Replacements of Links | ||
* | ||
* If the configuration options do not provide enough flexibility, a {@link #replaceFn} may be provided to fully customize | ||
* the output of Autolinker. This function is called once for each URL/Email/Twitter handle match that is encountered. | ||
* | ||
* For example: | ||
* | ||
* var input = "..."; // string with URLs, Email Addresses, and Twitter Handles | ||
* | ||
* var linkedText = Autolinker.link( input, { | ||
* replaceFn : function( autolinker, match ) { | ||
* console.log( "href = ", match.getAnchorHref() ); | ||
* console.log( "text = ", match.getAnchorText() ); | ||
* | ||
* switch( match.getType() ) { | ||
* case 'url' : | ||
* console.log( "url: ", match.getUrl() ); | ||
* return true; // let Autolinker perform its normal anchor tag replacement | ||
* | ||
* case 'email' : | ||
* var email = match.getEmail(); | ||
* console.log( "email: ", email ); | ||
* | ||
* if( email === "my@own.address" ) { | ||
* return false; // don't auto-link this particular email address; leave as-is | ||
* } else { | ||
* return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) | ||
* } | ||
* | ||
* case 'twitter' : | ||
* var twitterHandle = match.getTwitterHandle(); | ||
* console.log( twitterHandle ); | ||
* | ||
* return '<a href="http://newplace.to.link.twitter.handles.to/">' + twitterHandle + '</a>'; | ||
* } | ||
* } | ||
* } ); | ||
* | ||
* | ||
* The function may return the following values: | ||
* | ||
* - `true` (Boolean): Allow Autolinker to replace the match as it normally would. | ||
* - `false` (Boolean): Do not replace the current match at all - leave as-is. | ||
* - Any string: If a string is returned from the function, the string will be used directly as the replacement HTML for | ||
* the match. | ||
* | ||
* @constructor | ||
* @param {Object} [config] The configuration options for the Autolinker instance, specified in an Object (map). | ||
*/ | ||
var Autolinker = function( cfg ) { | ||
Autolinker.Util.assign( this, cfg ); // assign the properties of `cfg` onto the Autolinker instance. Prototype properties will be used for missing configs. | ||
}; | ||
Autolinker.prototype = { | ||
constructor : Autolinker, // fix constructor property | ||
/** | ||
* @class Autolinker | ||
* @extends Object | ||
* @cfg {Boolean} urls | ||
* | ||
* Utility class used to process a given string of text, and wrap the URLs, email addresses, and Twitter handles in | ||
* the appropriate anchor (<a>) tags to turn them into links. | ||
* `true` if miscellaneous URLs should be automatically linked, `false` if they should not be. | ||
*/ | ||
urls : true, | ||
/** | ||
* @cfg {Boolean} email | ||
* | ||
* Any of the configuration options may be provided in an Object (map) provided to the Autolinker constructor, which | ||
* will configure how the {@link #link link()} method will process the links. | ||
* `true` if email addresses should be automatically linked, `false` if they should not be. | ||
*/ | ||
email : true, | ||
/** | ||
* @cfg {Boolean} twitter | ||
* | ||
* For example: | ||
* `true` if Twitter handles ("@example") should be automatically linked, `false` if they should not be. | ||
*/ | ||
twitter : true, | ||
/** | ||
* @cfg {Boolean} newWindow | ||
* | ||
* var autolinker = new Autolinker( { | ||
* newWindow : false, | ||
* truncate : 30 | ||
* } ); | ||
* | ||
* var html = autolinker.link( "Joe went to www.yahoo.com" ); | ||
* // produces: 'Joe went to <a href="http://www.yahoo.com">yahoo.com</a>' | ||
* `true` if the links should open in a new window, `false` otherwise. | ||
*/ | ||
newWindow : true, | ||
/** | ||
* @cfg {Boolean} stripPrefix | ||
* | ||
* `true` if 'http://' or 'https://' and/or the 'www.' should be stripped from the beginning of URL links' text, | ||
* `false` otherwise. | ||
*/ | ||
stripPrefix : true, | ||
/** | ||
* @cfg {Number} truncate | ||
* | ||
* The {@link #static-link static link()} method may also be used to inline options into a single call, which may | ||
* be more convenient for one-off uses. For example: | ||
* A number for how many characters long URLs/emails/twitter handles should be truncated to inside the text of | ||
* a link. If the URL/email/twitter is over this number of characters, it will be truncated to this length by | ||
* adding a two period ellipsis ('..') to the end of the string. | ||
* | ||
* var html = Autolinker.link( "Joe went to www.yahoo.com", { | ||
* newWindow : false, | ||
* truncate : 30 | ||
* } ); | ||
* // produces: 'Joe went to <a href="http://www.yahoo.com">yahoo.com</a>' | ||
* For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' truncated to 25 characters might look | ||
* something like this: 'yahoo.com/some/long/pat..' | ||
*/ | ||
/** | ||
* @cfg {String} className | ||
* | ||
* @constructor | ||
* @param {Object} [config] The configuration options for the Autolinker instance, specified in an Object (map). | ||
* A CSS class name to add to the generated links. This class will be added to all links, as well as this class | ||
* plus url/email/twitter suffixes for styling url/email/twitter links differently. | ||
* | ||
* For example, if this config is provided as "myLink", then: | ||
* | ||
* 1) URL links will have the CSS classes: "myLink myLink-url" | ||
* 2) Email links will have the CSS classes: "myLink myLink-email", and | ||
* 3) Twitter links will have the CSS classes: "myLink myLink-twitter" | ||
*/ | ||
var Autolinker = function( cfg ) { | ||
cfg = cfg || {}; | ||
className : "", | ||
// Assign the properties of `cfg` onto the Autolinker instance | ||
for( var prop in cfg ) | ||
if( cfg.hasOwnProperty( prop ) ) this[ prop ] = cfg[ prop ]; | ||
}; | ||
/** | ||
* @cfg {Function} replaceFn | ||
* | ||
* A function to individually process each URL/Email/Twitter match found in the input string. | ||
* | ||
* See the class's description for usage. | ||
*/ | ||
Autolinker.prototype = { | ||
constructor : Autolinker, // fix constructor property | ||
/** | ||
* @private | ||
* @property {RegExp} htmlCharacterEntitiesRegex | ||
* | ||
* The regular expression that matches common HTML character entities. | ||
* | ||
* Ignoring & as it could be part of a query string -- handling it separately. | ||
*/ | ||
htmlCharacterEntitiesRegex: /( | |<|<|>|>)/gi, | ||
/** | ||
* @private | ||
* @property {RegExp} matcherRegex | ||
* | ||
* The regular expression that matches URLs, email addresses, and Twitter handles. | ||
* | ||
* This regular expression has the following capturing groups: | ||
* | ||
* 1. Group that is used to determine if there is a Twitter handle match (i.e. @someTwitterUser). Simply check for its | ||
* existence to determine if there is a Twitter handle match. The next couple of capturing groups give information | ||
* about the Twitter handle match. | ||
* 2. The whitespace character before the @sign in a Twitter handle. This is needed because there are no lookbehinds in | ||
* JS regular expressions, and can be used to reconstruct the original string in a replace(). | ||
* 3. The Twitter handle itself in a Twitter match. If the match is '@someTwitterUser', the handle is 'someTwitterUser'. | ||
* 4. Group that matches an email address. Used to determine if the match is an email address, as well as holding the full | ||
* address. Ex: 'me@my.com' | ||
* 5. Group that matches a URL in the input text. Ex: 'http://google.com', 'www.google.com', or just 'google.com'. | ||
* This also includes a path, url parameters, or hash anchors. Ex: google.com/path/to/file?q1=1&q2=2#myAnchor | ||
* 6. A protocol-relative ('//') match for the case of a 'www.' prefixed URL. Will be an empty string if it is not a | ||
* protocol-relative match. We need to know the character before the '//' in order to determine if it is a valid match | ||
* or the // was in a string we don't want to auto-link. | ||
* 7. A protocol-relative ('//') match for the case of a known TLD prefixed URL. Will be an empty string if it is not a | ||
* protocol-relative match. See #6 for more info. | ||
*/ | ||
matcherRegex : (function() { | ||
var twitterRegex = /(^|[^\w])@(\w{1,15})/, // For matching a twitter handle. Ex: @gregory_jacobs | ||
emailRegex = /(?:[\-;:&=\+\$,\w\.]+@)/, // something@ for email addresses (a.k.a. local-part) | ||
protocolRegex = /(?:[A-Za-z]{3,9}:(?:\/\/)?)/, // match protocol, allow in format http:// or mailto: | ||
wwwRegex = /(?:www\.)/, // starting with 'www.' | ||
domainNameRegex = /[A-Za-z0-9\.\-]*[A-Za-z0-9\-]/, // anything looking at all like a domain, non-unicode domains, not ending in a period | ||
tldRegex = /\.(?:international|construction|contractors|enterprises|photography|productions|foundation|immobilien|industries|management|properties|technology|christmas|community|directory|education|equipment|institute|marketing|solutions|vacations|bargains|boutique|builders|catering|cleaning|clothing|computer|democrat|diamonds|graphics|holdings|lighting|partners|plumbing|supplies|training|ventures|academy|careers|company|cruises|domains|exposed|flights|florist|gallery|guitars|holiday|kitchen|neustar|okinawa|recipes|rentals|reviews|shiksha|singles|support|systems|agency|berlin|camera|center|coffee|condos|dating|estate|events|expert|futbol|kaufen|luxury|maison|monash|museum|nagoya|photos|repair|report|social|supply|tattoo|tienda|travel|viajes|villas|vision|voting|voyage|actor|build|cards|cheap|codes|dance|email|glass|house|mango|ninja|parts|photo|shoes|solar|today|tokyo|tools|watch|works|aero|arpa|asia|best|bike|blue|buzz|camp|club|cool|coop|farm|fish|gift|guru|info|jobs|kiwi|kred|land|limo|link|menu|mobi|moda|name|pics|pink|post|qpon|rich|ruhr|sexy|tips|vote|voto|wang|wien|wiki|zone|bar|bid|biz|cab|cat|ceo|com|edu|gov|int|kim|mil|net|onl|org|pro|pub|red|tel|uno|wed|xxx|xyz|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)\b/, // match our known top level domains (TLDs) | ||
// Allow optional path, query string, and hash anchor, not ending in the following characters: "!:,.;" | ||
// http://blog.codinghorror.com/the-problem-with-urls/ | ||
urlSuffixRegex = /(?:[\-A-Za-z0-9+&@#\/%?=~_()|!:,.;]*[\-A-Za-z0-9+&@#\/%=~_()|])?/; // note: optional part of the full regex | ||
/** | ||
* @cfg {Boolean} newWindow | ||
* | ||
* `true` if the links should open in a new window, `false` otherwise. | ||
*/ | ||
newWindow : true, | ||
/** | ||
* @cfg {Boolean} stripPrefix | ||
* | ||
* `true` if 'http://' or 'https://' and/or the 'www.' should be stripped from the beginning of links, `false` otherwise. | ||
*/ | ||
stripPrefix : true, | ||
/** | ||
* @cfg {Number} truncate | ||
* | ||
* A number for how many characters long URLs/emails/twitter handles should be truncated to inside the text of | ||
* a link. If the URL/email/twitter is over this number of characters, it will be truncated to this length by | ||
* adding a two period ellipsis ('..') into the middle of the string. | ||
* | ||
* For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' truncated to 25 characters might look | ||
* something like this: 'http://www...th/to/a/file' | ||
*/ | ||
/** | ||
* @cfg {Boolean} twitter | ||
* | ||
* `true` if Twitter handles ("@example") should be automatically linked, `false` if they should not be. | ||
*/ | ||
twitter : true, | ||
/** | ||
* @cfg {Boolean} email | ||
* | ||
* `true` if email addresses should be automatically linked, `false` if they should not be. | ||
*/ | ||
email : true, | ||
/** | ||
* @cfg {Boolean} urls | ||
* | ||
* `true` if miscellaneous URLs should be automatically linked, `false` if they should not be. | ||
*/ | ||
urls : true, | ||
/** | ||
* @cfg {String} className | ||
* | ||
* A CSS class name to add to the generated links. This class will be added to all links, as well as this class | ||
* plus url/email/twitter suffixes for styling url/email/twitter links differently. | ||
* | ||
* For example, if this config is provided as "myLink", then: | ||
* | ||
* 1) URL links will have the CSS classes: "myLink myLink-url" | ||
* 2) Email links will have the CSS classes: "myLink myLink-email", and | ||
* 3) Twitter links will have the CSS classes: "myLink myLink-twitter" | ||
*/ | ||
className : "", | ||
/** | ||
* @private | ||
* @property {RegExp} matcherRegex | ||
* | ||
* The regular expression that matches URLs, email addresses, and Twitter handles. | ||
* | ||
* This regular expression has the following capturing groups: | ||
* | ||
* 1. Group that is used to determine if there is a Twitter handle match (i.e. @someTwitterUser). Simply check for its | ||
* existence to determine if there is a Twitter handle match. The next couple of capturing groups give information | ||
* about the Twitter handle match. | ||
* 2. The whitespace character before the @sign in a Twitter handle. This is needed because there are no lookbehinds in | ||
* JS regular expressions, and can be used to reconstruct the original string in a replace(). | ||
* 3. The Twitter handle itself in a Twitter match. If the match is '@someTwitterUser', the handle is 'someTwitterUser'. | ||
* 4. Group that matches an email address. Used to determine if the match is an email address, as well as holding the full | ||
* address. Ex: 'me@my.com' | ||
* 5. Group that matches a URL in the input text. Ex: 'http://google.com', 'www.google.com', or just 'google.com'. | ||
* This also includes a path, url parameters, or hash anchors. Ex: google.com/path/to/file?q1=1&q2=2#myAnchor | ||
* 6. A protocol-relative ('//') match for the case of a 'www.' prefixed URL. Will be an empty string if it is not a | ||
* protocol-relative match. We need to know the character before the '//' in order to determine if it is a valid match | ||
* or the // was in a string we don't want to auto-link. | ||
* 7. A protocol-relative ('//') match for the case of a known TLD prefixed URL. Will be an empty string if it is not a | ||
* protocol-relative match. See #6 for more info. | ||
*/ | ||
matcherRegex : (function() { | ||
var twitterRegex = /(^|[^\w])@(\w{1,15})/, // For matching a twitter handle. Ex: @gregory_jacobs | ||
emailRegex = /(?:[\-;:&=\+\$,\w\.]+@)/, // something@ for email addresses (a.k.a. local-part) | ||
protocolRegex = /(?:[A-Za-z]{3,9}:(?:\/\/)?)/, // match protocol, allow in format http:// or mailto: | ||
wwwRegex = /(?:www\.)/, // starting with 'www.' | ||
domainNameRegex = /[A-Za-z0-9\.\-]*[A-Za-z0-9\-]/, // anything looking at all like a domain, non-unicode domains, not ending in a period | ||
tldRegex = /\.(?:international|construction|contractors|enterprises|photography|productions|foundation|immobilien|industries|management|properties|technology|christmas|community|directory|education|equipment|institute|marketing|solutions|vacations|bargains|boutique|builders|catering|cleaning|clothing|computer|democrat|diamonds|graphics|holdings|lighting|partners|plumbing|supplies|training|ventures|academy|careers|company|cruises|domains|exposed|flights|florist|gallery|guitars|holiday|kitchen|neustar|okinawa|recipes|rentals|reviews|shiksha|singles|support|systems|agency|berlin|camera|center|coffee|condos|dating|estate|events|expert|futbol|kaufen|luxury|maison|monash|museum|nagoya|photos|repair|report|social|supply|tattoo|tienda|travel|viajes|villas|vision|voting|voyage|actor|build|cards|cheap|codes|dance|email|glass|house|mango|ninja|parts|photo|shoes|solar|today|tokyo|tools|watch|works|aero|arpa|asia|best|bike|blue|buzz|camp|club|cool|coop|farm|fish|gift|guru|info|jobs|kiwi|kred|land|limo|link|menu|mobi|moda|name|pics|pink|post|qpon|rich|ruhr|sexy|tips|vote|voto|wang|wien|wiki|zone|bar|bid|biz|cab|cat|ceo|com|edu|gov|int|kim|mil|net|onl|org|pro|pub|red|tel|uno|wed|xxx|xyz|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)\b/, // match our known top level domains (TLDs) | ||
// Allow optional path, query string, and hash anchor, not ending in the following characters: "!:,.;" | ||
// http://blog.codinghorror.com/the-problem-with-urls/ | ||
urlSuffixRegex = /(?:[\-A-Za-z0-9+&@#\/%?=~_()|!:,.;]*[\-A-Za-z0-9+&@#\/%=~_()|])?/; // note: optional part of the full regex | ||
return new RegExp( [ | ||
'(', // *** Capturing group $1, which can be used to check for a twitter handle match. Use group $3 for the actual twitter handle though. $2 may be used to reconstruct the original string in a replace() | ||
// *** Capturing group $2, which matches the whitespace character before the '@' sign (needed because of no lookbehinds), and | ||
// *** Capturing group $3, which matches the actual twitter handle | ||
twitterRegex.source, | ||
')', | ||
return new RegExp( [ | ||
'(', // *** Capturing group $1, which can be used to check for a twitter handle match. Use group $3 for the actual twitter handle though. $2 may be used to reconstruct the original string in a replace() | ||
// *** Capturing group $2, which matches the whitespace character before the '@' sign (needed because of no lookbehinds), and | ||
// *** Capturing group $3, which matches the actual twitter handle | ||
twitterRegex.source, | ||
')', | ||
'|', | ||
'(', // *** Capturing group $4, which is used to determine an email match | ||
emailRegex.source, | ||
domainNameRegex.source, | ||
tldRegex.source, | ||
')', | ||
'|', | ||
'(', // *** Capturing group $5, which is used to match a URL | ||
'(?:', // parens to cover match for protocol (optional), and domain | ||
'(?:', // non-capturing paren for a protocol-prefixed url (ex: http://google.com) | ||
protocolRegex.source, | ||
domainNameRegex.source, | ||
')', | ||
'|', | ||
'(?:', // non-capturing paren for a 'www.' prefixed url (ex: www.google.com) | ||
'(.?//)?', // *** Capturing group $6 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character | ||
wwwRegex.source, | ||
domainNameRegex.source, | ||
')', | ||
'|', | ||
'(?:', // non-capturing paren for known a TLD url (ex: google.com) | ||
'(.?//)?', // *** Capturing group $7 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character | ||
domainNameRegex.source, | ||
tldRegex.source, | ||
')', | ||
'|', | ||
'(', // *** Capturing group $4, which is used to determine an email match | ||
emailRegex.source, | ||
domainNameRegex.source, | ||
tldRegex.source, | ||
')', | ||
'|', | ||
'(', // *** Capturing group $5, which is used to match a URL | ||
'(?:', // parens to cover match for protocol (optional), and domain | ||
'(?:', // non-capturing paren for a protocol-prefixed url (ex: http://google.com) | ||
protocolRegex.source, | ||
domainNameRegex.source, | ||
')', | ||
urlSuffixRegex.source, // match for path, query string, and/or hash anchor | ||
')' | ||
].join( "" ), 'gi' ); | ||
} )(), | ||
/** | ||
* @private | ||
* @property {RegExp} protocolRelativeRegex | ||
* | ||
* The regular expression used to find protocol-relative URLs. A protocol-relative URL is, for example, "//yahoo.com" | ||
* | ||
* This regular expression needs to match the character before the '//', in order to determine if we should actually | ||
* autolink a protocol-relative URL. For instance, we want to autolink something like "//google.com", but we | ||
* don't want to autolink something like "abc//google.com" | ||
*/ | ||
protocolRelativeRegex : /(.)?\/\//, | ||
/** | ||
* @private | ||
* @property {RegExp} htmlRegex | ||
* | ||
* The regular expression used to pull out HTML tags from a string. Handles namespaced HTML tags and | ||
* attribute names, as specified by http://www.w3.org/TR/html-markup/syntax.html. | ||
* | ||
* Capturing groups: | ||
* | ||
* 1. If it is an end tag, this group will have the '/'. | ||
* 2. The tag name. | ||
*/ | ||
htmlRegex : (function() { | ||
var tagNameRegex = /[0-9a-zA-Z:]+/, | ||
attrNameRegex = /[^\s\0"'>\/=\x01-\x1F\x7F]+/, // the unicode range accounts for excluding control chars, and the delete char | ||
attrValueRegex = /(?:".*?"|'.*?'|[^'"=<>`\s]+)/, // double quoted, single quoted, or unquoted attribute values | ||
nameEqualsValueRegex = attrNameRegex.source + '(?:\\s*=\\s*' + attrValueRegex.source + ')?'; // optional '=[value]' | ||
return new RegExp( [ | ||
'<(?:!|(/))?', // Beginning of a tag. Either '<' for a start tag, '</' for an end tag, or <! for the <!DOCTYPE ...> tag. The slash or an empty string is Capturing Group 1. | ||
// The tag name (Capturing Group 2) | ||
'(' + tagNameRegex.source + ')', | ||
'|', | ||
// Zero or more attributes following the tag name | ||
'(?:', | ||
'\\s+', // one or more whitespace chars before an attribute | ||
// Either: | ||
// A. tag="value", or | ||
// B. "value" alone (for <!DOCTYPE> tag. Ex: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">) | ||
'(?:', nameEqualsValueRegex, '|', attrValueRegex.source + ')', | ||
')*', | ||
'(?:', // non-capturing paren for a 'www.' prefixed url (ex: www.google.com) | ||
'(.?//)?', // *** Capturing group $6 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character | ||
wwwRegex.source, | ||
domainNameRegex.source, | ||
')', | ||
'\\s*/?', // any trailing spaces and optional '/' before the closing '>' | ||
'>' | ||
].join( "" ), 'g' ); | ||
} )(), | ||
/** | ||
* @private | ||
* @property {RegExp} urlPrefixRegex | ||
* | ||
* A regular expression used to remove the 'http://' or 'https://' and/or the 'www.' from URLs. | ||
*/ | ||
urlPrefixRegex: /^(https?:\/\/)?(www\.)?/i, | ||
'|', | ||
'(?:', // non-capturing paren for known a TLD url (ex: google.com) | ||
'(.?//)?', // *** Capturing group $7 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character | ||
domainNameRegex.source, | ||
tldRegex.source, | ||
')', | ||
')', | ||
urlSuffixRegex.source, // match for path, query string, and/or hash anchor | ||
')' | ||
].join( "" ), 'gi' ); | ||
} )(), | ||
/** | ||
* @private | ||
* @property {RegExp} invalidProtocolRelMatchRegex | ||
* | ||
* The regular expression used to check a potential protocol-relative URL match, coming from the {@link #matcherRegex}. | ||
* A protocol-relative URL is, for example, "//yahoo.com" | ||
* | ||
* This regular expression is used in conjunction with the {@link #matcherRegex}, and checks to see if there is a word character | ||
* before the '//' in order to determine if we should actually autolink a protocol-relative URL. This is needed because there | ||
* is no negative look-behind in JavaScript regular expressions. | ||
* | ||
* For instance, we want to autolink something like "//google.com", but we don't want to autolink something | ||
* like "abc//google.com" | ||
*/ | ||
invalidProtocolRelMatchRegex : /^[\w]\/\//, | ||
/** | ||
* @private | ||
* @property {RegExp} charBeforeProtocolRelMatchRegex | ||
* | ||
* The regular expression used to retrieve the character before a protocol-relative URL match. | ||
* | ||
* This is used in conjunction with the {@link #matcherRegex}, which needs to grab the character before a protocol-relative | ||
* '//' due to the lack of a negative look-behind in JavaScript regular expressions. The character before the match is stripped | ||
* from the URL. | ||
*/ | ||
charBeforeProtocolRelMatchRegex : /^(.)?\/\//, | ||
/** | ||
* @private | ||
* @property {Autolinker.AnchorTagBuilder} tagBuilder | ||
* | ||
* The AnchorTagBuilder instance used to build the URL/email/Twitter replacement anchor tags. This is lazily instantiated | ||
* in the {@link #getTagBuilder} method. | ||
*/ | ||
/** | ||
* Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. | ||
* Does not link URLs found within HTML tags. | ||
* | ||
* For instance, if given the text: `You should go to http://www.yahoo.com`, then the result | ||
* will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` | ||
* | ||
* This method finds the text around any HTML elements in the input `textOrHtml`, which will be the text that is processed. | ||
* Any original HTML elements will be left as-is, as well as the text that is already wrapped in anchor (<a>) tags. | ||
* | ||
* @param {String} textOrHtml The HTML or text to link URLs, email addresses, and Twitter handles within. | ||
* @return {String} The HTML, with URLs/emails/Twitter handles automatically linked. | ||
*/ | ||
link : function( textOrHtml ) { | ||
var me = this, // for closure | ||
htmlParser = this.getHtmlParser(), | ||
htmlCharacterEntitiesRegex = this.htmlCharacterEntitiesRegex, | ||
anchorTagStackCount = 0, // used to only process text around anchor tags, and any inner text/html they may have | ||
resultHtml = []; | ||
/** | ||
* Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. | ||
* Does not link URLs found within HTML tags. | ||
* | ||
* For instance, if given the text: `You should go to http://www.yahoo.com`, then the result | ||
* will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` | ||
* | ||
* @method link | ||
* @param {String} textOrHtml The HTML or text to link URLs, email addresses, and Twitter handles within. | ||
* @return {String} The HTML, with URLs/emails/twitter handles automatically linked. | ||
*/ | ||
link : function( textOrHtml ) { | ||
return this.processHtml( textOrHtml ); | ||
}, | ||
/** | ||
* Processes the given HTML to auto-link URLs/emails/Twitter handles. | ||
* | ||
* Finds the text around any HTML elements in the input `html`, which will be the text that is processed. | ||
* Any original HTML elements will be left as-is, as well as the text that is already wrapped in anchor tags. | ||
* | ||
* @private | ||
* @method processHtml | ||
* @param {String} html The input text or HTML to process in order to auto-link. | ||
* @return {String} | ||
*/ | ||
processHtml : function( html ) { | ||
// Loop over the HTML string, ignoring HTML tags, and processing the text that lies between them, | ||
// wrapping the URLs in anchor tags | ||
var htmlRegex = this.htmlRegex, | ||
currentResult, | ||
inBetweenTagsText, | ||
lastIndex = 0, | ||
anchorTagStackCount = 0, | ||
resultHtml = []; | ||
while( ( currentResult = htmlRegex.exec( html ) ) !== null ) { | ||
var tagText = currentResult[ 0 ], | ||
tagName = currentResult[ 2 ], | ||
isClosingTag = !!currentResult[ 1 ]; | ||
inBetweenTagsText = html.substring( lastIndex, currentResult.index ); | ||
lastIndex = currentResult.index + tagText.length; | ||
// Process around anchor tags, and any inner text / html they may have | ||
htmlParser.parse( textOrHtml, { | ||
// Process HTML nodes in the input `textOrHtml` | ||
processHtmlNode : function( tagText, tagName, isClosingTag ) { | ||
if( tagName === 'a' ) { | ||
if( !isClosingTag ) { // it's the start <a> tag | ||
anchorTagStackCount++; | ||
resultHtml.push( this.processTextNode( inBetweenTagsText ) ); | ||
} else { // it's the end </a> tag | ||
anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous </a> tags by making sure the stack count never goes below 0 | ||
if( anchorTagStackCount === 0 ) { | ||
resultHtml.push( inBetweenTagsText ); // We hit the matching </a> tag, simply add all of the text from the start <a> tag to the end </a> tag without linking it | ||
} | ||
} | ||
} else if( anchorTagStackCount === 0 ) { // not within an anchor tag, link the "in between" text | ||
resultHtml.push( this.processTextNode( inBetweenTagsText ) ); | ||
} else { | ||
// if we have a tag that is in between anchor tags (ex: <a href="..."><b>google.com</b></a>), | ||
// just append the inner text | ||
resultHtml.push( inBetweenTagsText ); | ||
} | ||
resultHtml.push( tagText ); // now add the text of the tag itself verbatim | ||
} | ||
}, | ||
// Process any remaining text after the last HTML element. Will process all of the text if there were no HTML elements. | ||
if( lastIndex < html.length ) { | ||
var processedTextNode = this.processTextNode( html.substring( lastIndex ) ); | ||
resultHtml.push( processedTextNode ); | ||
} | ||
return resultHtml.join( "" ); | ||
}, | ||
/** | ||
* Process the text that lies inbetween HTML tags. This method does the actual wrapping of URLs with | ||
* anchor tags. | ||
* | ||
* @private | ||
* @param {String} text The text to auto-link. | ||
* @return {String} The text with anchor tags auto-filled. | ||
*/ | ||
processTextNode : function( text ) { | ||
var me = this, // for closures | ||
matcherRegex = this.matcherRegex, | ||
enableTwitter = this.twitter, | ||
enableEmailAddresses = this.email, | ||
enableUrls = this.urls; | ||
return text.replace( matcherRegex, function( matchStr, $1, $2, $3, $4, $5, $6, $7 ) { | ||
var twitterMatch = $1, | ||
twitterHandlePrefixWhitespaceChar = $2, // The whitespace char before the @ sign in a Twitter handle match. This is needed because of no lookbehinds in JS regexes | ||
twitterHandle = $3, // The actual twitterUser (i.e the word after the @ sign in a Twitter handle match) | ||
emailAddress = $4, // For both determining if it is an email address, and stores the actual email address | ||
urlMatch = $5, // The matched URL string | ||
protocolRelativeMatch = $6 || $7, // The '//' for a protocol-relative match, with the character that comes before the '//' | ||
prefixStr = "", // A string to use to prefix the anchor tag that is created. This is needed for the Twitter handle match | ||
suffixStr = ""; // A string to suffix the anchor tag that is created. This is used if there is a trailing parenthesis that should not be auto-linked. | ||
// Early exits with no replacements for: | ||
// 1) Disabled link types | ||
// 2) URL matches which do not have at least have one period ('.') in the domain name (effectively skipping over | ||
// matches like "abc:def") | ||
// 3) A protocol-relative url match (a URL beginning with '//') whose previous character is a word character | ||
// (effectively skipping over strings like "abc//google.com") | ||
if( | ||
( twitterMatch && !enableTwitter ) || ( emailAddress && !enableEmailAddresses ) || ( urlMatch && !enableUrls ) || | ||
( urlMatch && urlMatch.indexOf( '.' ) === -1 ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL | ||
( urlMatch && /^[A-Za-z]{3,9}:/.test( urlMatch ) && !/:.*?[A-Za-z]/.test( urlMatch ) ) || // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0" | ||
( protocolRelativeMatch && /^[\w]\/\//.test( protocolRelativeMatch ) ) // a protocol-relative match which has a word character in front of it (so we can skip something like "abc//google.com") | ||
) { | ||
return matchStr; | ||
} | ||
// Handle a closing parenthesis at the end of the match, and exclude it if there is not a matching open parenthesis | ||
// in the match. This handles cases like the string "wikipedia.com/something_(disambiguation)" (which should be auto- | ||
// linked, and when it is enclosed in parenthesis itself, such as: "(wikipedia.com/something_(disambiguation))" (in | ||
// which the outer parens should *not* be auto-linked. | ||
var lastChar = matchStr.charAt( matchStr.length - 1 ); | ||
if( lastChar === ')' ) { | ||
var openParensMatch = matchStr.match( /\(/g ), | ||
closeParensMatch = matchStr.match( /\)/g ), | ||
numOpenParens = ( openParensMatch && openParensMatch.length ) || 0, | ||
numCloseParens = ( closeParensMatch && closeParensMatch.length ) || 0; | ||
// Process text nodes in the input `textOrHtml` | ||
processTextNode : function( text ) { | ||
if( anchorTagStackCount === 0 ) { | ||
// If we're not within an <a> tag, process the text node | ||
var unescapedText = Autolinker.Util.splitAndCapture( text, htmlCharacterEntitiesRegex ); // split at HTML entities, but include the HTML entities in the results array | ||
if( numOpenParens < numCloseParens ) { | ||
matchStr = matchStr.substr( 0, matchStr.length - 1 ); // remove the trailing ")" | ||
suffixStr = ")"; // this will be added after the <a> tag | ||
for ( var i = 0, len = unescapedText.length; i < len; i++ ) { | ||
var textToProcess = unescapedText[ i ], | ||
processedTextNode = me.processTextNode( textToProcess ); | ||
resultHtml.push( processedTextNode ); | ||
} | ||
} | ||
var anchorHref = matchStr, // initialize both of these | ||
anchorText = matchStr, // values as the full match | ||
linkType; | ||
// Process the urls that are found. We need to change URLs like "www.yahoo.com" to "http://www.yahoo.com" (or the browser | ||
// will try to direct the user to "http://current-domain.com/www.yahoo.com"), and we need to prefix 'mailto:' to email addresses. | ||
if( twitterMatch ) { | ||
linkType = 'twitter'; | ||
prefixStr = twitterHandlePrefixWhitespaceChar; | ||
anchorHref = 'https://twitter.com/' + twitterHandle; | ||
anchorText = '@' + twitterHandle; | ||
} else if( emailAddress ) { | ||
linkType = 'email'; | ||
anchorHref = 'mailto:' + emailAddress; | ||
anchorText = emailAddress; | ||
} else { // url match | ||
linkType = 'url'; | ||
if( protocolRelativeMatch ) { | ||
// Strip off any protocol-relative '//' from the anchor text (leaving the previous non-word character | ||
// intact, if there is one) | ||
var protocolRelRegex = new RegExp( "^" + me.protocolRelativeRegex.source ), // for this one, we want to only match at the beginning of the string | ||
charBeforeMatch = protocolRelativeMatch.match( protocolRelRegex )[ 1 ] || ""; | ||
prefixStr = charBeforeMatch + prefixStr; // re-add the character before the '//' to what will be placed before the <a> tag | ||
anchorHref = anchorHref.replace( protocolRelRegex, "//" ); // remove the char before the match for the href | ||
anchorText = anchorText.replace( protocolRelRegex, "" ); // remove both the char before the match and the '//' for the anchor text | ||
} else if( !/^[A-Za-z]{3,9}:/i.test( anchorHref ) ) { | ||
// url string doesn't begin with a protocol, assume http:// | ||
anchorHref = 'http://' + anchorHref; | ||
} | ||
} else { | ||
// `text` is within an <a> tag, simply append the text - we do not want to autolink anything | ||
// already within an <a>...</a> tag | ||
resultHtml.push( text ); | ||
} | ||
// wrap the match in an anchor tag | ||
var anchorTag = me.createAnchorTag( linkType, anchorHref, anchorText ); | ||
return prefixStr + anchorTag + suffixStr; | ||
} ); | ||
}, | ||
} | ||
} ); | ||
return resultHtml.join( "" ); | ||
}, | ||
/** | ||
* Lazily instantiates and returns the {@link #htmlParser} instance for this Autolinker instance. | ||
* | ||
* @protected | ||
* @return {Autolinker.HtmlParser} | ||
*/ | ||
getHtmlParser : function() { | ||
var htmlParser = this.htmlParser; | ||
/** | ||
* Generates the actual anchor (<a>) tag to use in place of a source url/email/twitter link. | ||
* | ||
* @private | ||
* @param {"url"/"email"/"twitter"} linkType The type of link that an anchor tag is being generated for. | ||
* @param {String} anchorHref The href for the anchor tag. | ||
* @param {String} anchorText The anchor tag's text (i.e. what will be displayed). | ||
* @return {String} The full HTML for the anchor tag. | ||
*/ | ||
createAnchorTag : function( linkType, anchorHref, anchorText ) { | ||
var attributesStr = this.createAnchorAttrsStr( linkType, anchorHref ); | ||
anchorText = this.processAnchorText( anchorText ); | ||
return '<a ' + attributesStr + '>' + anchorText + '</a>'; | ||
}, | ||
if( !htmlParser ) { | ||
htmlParser = this.htmlParser = new Autolinker.HtmlParser(); | ||
} | ||
return htmlParser; | ||
}, | ||
/** | ||
* Returns the {@link #tagBuilder} instance for this Autolinker instance, lazily instantiating it | ||
* if it does not yet exist. | ||
* | ||
* @return {Autolinker.AnchorTagBuilder} | ||
*/ | ||
getTagBuilder : function() { | ||
var tagBuilder = this.tagBuilder; | ||
/** | ||
* Creates the string which will be the HTML attributes for the anchor (<a>) tag being generated. | ||
* | ||
* @private | ||
* @param {"url"/"email"/"twitter"} linkType The type of link that an anchor tag is being generated for. | ||
* @param {String} href The href for the anchor tag. | ||
* @return {String} The anchor tag's attribute. Ex: `href="http://google.com" class="myLink myLink-url" target="_blank"` | ||
*/ | ||
createAnchorAttrsStr : function( linkType, anchorHref ) { | ||
var attrs = [ 'href="' + anchorHref + '"' ]; // we'll always have the `href` attribute | ||
if( !tagBuilder ) { | ||
tagBuilder = this.tagBuilder = new Autolinker.AnchorTagBuilder( { | ||
newWindow : this.newWindow, | ||
truncate : this.truncate, | ||
className : this.className | ||
} ); | ||
} | ||
return tagBuilder; | ||
}, | ||
/** | ||
* Process the text that lies inbetween HTML tags. This method does the actual wrapping of URLs with | ||
* anchor tags. | ||
* | ||
* @private | ||
* @param {String} text The text to auto-link. | ||
* @return {String} The text with anchor tags auto-filled. | ||
*/ | ||
processTextNode : function( text ) { | ||
var me = this, // for closure | ||
charBeforeProtocolRelMatchRegex = this.charBeforeProtocolRelMatchRegex; | ||
return text.replace( this.matcherRegex, function( matchStr, $1, $2, $3, $4, $5, $6, $7 ) { | ||
var twitterMatch = $1, | ||
twitterHandlePrefixWhitespaceChar = $2, // The whitespace char before the @ sign in a Twitter handle match. This is needed because of no lookbehinds in JS regexes. | ||
twitterHandle = $3, // The actual twitterUser (i.e the word after the @ sign in a Twitter handle match) | ||
emailAddressMatch = $4, // For both determining if it is an email address, and stores the actual email address | ||
urlMatch = $5, // The matched URL string | ||
protocolRelativeMatch = $6 || $7, // The '//' for a protocol-relative match, with the character that comes before the '//' | ||
prefixStr = "", // A string to use to prefix the anchor tag that is created. This is needed for the Twitter handle match | ||
suffixStr = "", // A string to suffix the anchor tag that is created. This is used if there is a trailing parenthesis that should not be auto-linked. | ||
match; // Will be an Autolinker.match.Match object | ||
var cssClass = this.createCssClass( linkType ); | ||
if( cssClass ) { | ||
attrs.push( 'class="' + cssClass + '"' ); | ||
// Return out with no changes for match types that are disabled (url, email, twitter), or for matches that are | ||
// invalid (false positives from the matcherRegex, which can't use look-behinds since they are unavailable in JS). | ||
if( !me.isValidMatch( twitterMatch, emailAddressMatch, urlMatch, protocolRelativeMatch ) ) { | ||
return matchStr; | ||
} | ||
if( this.newWindow ) { | ||
attrs.push( 'target="_blank"' ); | ||
// Handle a closing parenthesis at the end of the match, and exclude it if there is not a matching open parenthesis | ||
// in the match itself. | ||
if( me.matchHasUnbalancedClosingParen( matchStr ) ) { | ||
matchStr = matchStr.substr( 0, matchStr.length - 1 ); // remove the trailing ")" | ||
suffixStr = ")"; // this will be added after the generated <a> tag | ||
} | ||
return attrs.join( " " ); | ||
}, | ||
/** | ||
* Creates the CSS class that will be used for a given anchor tag, based on the `linkType` and the {@link #className} | ||
* config. | ||
* | ||
* @private | ||
* @param {"url"/"email"/"twitter"} linkType The type of link that an anchor tag is being generated for. | ||
* @return {String} The CSS class string for the link. Example return: "myLink myLink-url". If no {@link #className} | ||
* was configured, returns an empty string. | ||
*/ | ||
createCssClass : function( linkType ) { | ||
var className = this.className; | ||
if( !className ) | ||
return ""; | ||
else | ||
return className + " " + className + "-" + linkType; // ex: "myLink myLink-url", "myLink myLink-email", or "myLink myLink-twitter" | ||
}, | ||
/** | ||
* Processes the `anchorText` by stripping the URL prefix (if {@link #stripPrefix} is `true`), removing | ||
* any trailing slash, and truncating the text according to the {@link #truncate} config. | ||
* | ||
* @private | ||
* @param {String} anchorText The anchor tag's text (i.e. what will be displayed). | ||
* @return {String} The processed `anchorText`. | ||
*/ | ||
processAnchorText : function( anchorText ) { | ||
if( this.stripPrefix ) { | ||
anchorText = this.stripUrlPrefix( anchorText ); | ||
if( emailAddressMatch ) { | ||
match = new Autolinker.match.Email( { email: emailAddressMatch } ); | ||
} else if( twitterMatch ) { | ||
// fix up the `matchStr` if there was a preceding whitespace char, which was needed to determine the match | ||
// itself (since there are no look-behinds in JS regexes) | ||
if( twitterHandlePrefixWhitespaceChar ) { | ||
prefixStr = twitterHandlePrefixWhitespaceChar; | ||
matchStr = matchStr.slice( 1 ); // remove the prefixed whitespace char from the match | ||
} | ||
match = new Autolinker.match.Twitter( { twitterHandle: twitterHandle } ); | ||
} else { // url match | ||
// If it's a protocol-relative '//' match, remove the character before the '//' (which the matcherRegex needed | ||
// to match due to the lack of a negative look-behind in JavaScript regular expressions) | ||
if( protocolRelativeMatch ) { | ||
var charBeforeMatch = protocolRelativeMatch.match( charBeforeProtocolRelMatchRegex )[ 1 ] || ""; | ||
if( charBeforeMatch ) { // fix up the `matchStr` if there was a preceding char before a protocol-relative match, which was needed to determine the match itself (since there are no look-behinds in JS regexes) | ||
prefixStr = charBeforeMatch; | ||
matchStr = matchStr.slice( 1 ); // remove the prefixed char from the match | ||
} | ||
} | ||
match = new Autolinker.match.Url( { url: matchStr, protocolRelativeMatch: protocolRelativeMatch, stripPrefix: me.stripPrefix } ); | ||
} | ||
anchorText = this.removeTrailingSlash( anchorText ); // remove trailing slash, if there is one | ||
anchorText = this.doTruncate( anchorText ); | ||
return anchorText; | ||
}, | ||
// Generate the replacement text for the match | ||
var matchReturnVal = me.createMatchReturnVal( match, matchStr ); | ||
return prefixStr + matchReturnVal + suffixStr; | ||
} ); | ||
}, | ||
/** | ||
* Determines if a given match found by {@link #processTextNode} is valid. Will return `false` for: | ||
* | ||
* 1) Disabled link types (i.e. having a Twitter match, but {@link #twitter} matching is disabled) | ||
* 2) URL matches which do not have at least have one period ('.') in the domain name (effectively skipping over | ||
* matches like "abc:def") | ||
* 3) A protocol-relative url match (a URL beginning with '//') whose previous character is a word character | ||
* (effectively skipping over strings like "abc//google.com") | ||
* | ||
* Otherwise, returns `true`. | ||
* | ||
* @private | ||
* @param {String} twitterMatch The matched Twitter handle, if there was one. Will be empty string if the match is not a | ||
* Twitter match. | ||
* @param {String} emailAddressMatch The matched Email address, if there was one. Will be empty string if the match is not | ||
* an Email address match. | ||
* @param {String} urlMatch The matched URL, if there was one. Will be an empty string if the match is not a URL match. | ||
* @param {String} protocolRelativeMatch The protocol-relative string for a URL match (i.e. '//'), possibly with a preceding | ||
* character (ex, a space, such as: ' //', or a letter, such as: 'a//'). The match is invalid if there is a word character | ||
* preceding the '//'. | ||
* @return {Boolean} `true` if the match given is valid and should be processed, or `false` if the match is invalid and/or | ||
* should just not be processed (such as, if it's a Twitter match, but {@link #twitter} matching is disabled}. | ||
*/ | ||
isValidMatch : function( twitterMatch, emailAddressMatch, urlMatch, protocolRelativeMatch ) { | ||
if( | ||
( twitterMatch && !this.twitter ) || ( emailAddressMatch && !this.email ) || ( urlMatch && !this.urls ) || | ||
( urlMatch && urlMatch.indexOf( '.' ) === -1 ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL | ||
( urlMatch && /^[A-Za-z]{3,9}:/.test( urlMatch ) && !/:.*?[A-Za-z]/.test( urlMatch ) ) || // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0" | ||
( protocolRelativeMatch && this.invalidProtocolRelMatchRegex.test( protocolRelativeMatch ) ) // a protocol-relative match which has a word character in front of it (so we can skip something like "abc//google.com") | ||
) { | ||
return false; | ||
} | ||
return true; | ||
}, | ||
/** | ||
* Determines if a match found has an unmatched closing parenthesis. If so, this parenthesis will be removed | ||
* from the match itself, and appended after the generated anchor tag in {@link #processTextNode}. | ||
* | ||
* A match may have an extra closing parenthesis at the end of the match because the regular expression must include parenthesis | ||
* for URLs such as "wikipedia.com/something_(disambiguation)", which should be auto-linked. | ||
* | ||
* However, an extra parenthesis *will* be included when the URL itself is wrapped in parenthesis, such as in the case of | ||
* "(wikipedia.com/something_(disambiguation))". In this case, the last closing parenthesis should *not* be part of the URL | ||
* itself, and this method will return `true`. | ||
* | ||
* @private | ||
* @param {String} matchStr The full match string from the {@link #matcherRegex}. | ||
* @return {Boolean} `true` if there is an unbalanced closing parenthesis at the end of the `matchStr`, `false` otherwise. | ||
*/ | ||
matchHasUnbalancedClosingParen : function( matchStr ) { | ||
var lastChar = matchStr.charAt( matchStr.length - 1 ); | ||
/** | ||
* Strips the URL prefix (such as "http://" or "https://") from the given text. | ||
* | ||
* @private | ||
* @param {String} text The text of the anchor that is being generated, for which to strip off the | ||
* url prefix (such as stripping off "http://") | ||
* @return {String} The `anchorText`, with the prefix stripped. | ||
*/ | ||
stripUrlPrefix : function( text ) { | ||
return text.replace( this.urlPrefixRegex, '' ); | ||
}, | ||
/** | ||
* Removes any trailing slash from the given `anchorText`, in prepration for the text to be displayed. | ||
* | ||
* @private | ||
* @param {String} anchorText The text of the anchor that is being generated, for which to remove any trailing | ||
* slash ('/') that may exist. | ||
* @return {String} The `anchorText`, with the trailing slash removed. | ||
*/ | ||
removeTrailingSlash : function( anchorText ) { | ||
if( anchorText.charAt( anchorText.length - 1 ) === '/' ) { | ||
anchorText = anchorText.slice( 0, -1 ); | ||
} | ||
return anchorText; | ||
}, | ||
/** | ||
* Performs the truncation of the `anchorText`, if the `anchorText` is longer than the {@link #truncate} option. | ||
* Truncates the text to 2 characters fewer than the {@link #truncate} option, and adds ".." to the end. | ||
* | ||
* @private | ||
* @param {String} text The anchor tag's text (i.e. what will be displayed). | ||
* @return {String} The truncated anchor text. | ||
*/ | ||
doTruncate : function( anchorText ) { | ||
var truncateLen = this.truncate; | ||
if( lastChar === ')' ) { | ||
var openParensMatch = matchStr.match( /\(/g ), | ||
closeParensMatch = matchStr.match( /\)/g ), | ||
numOpenParens = ( openParensMatch && openParensMatch.length ) || 0, | ||
numCloseParens = ( closeParensMatch && closeParensMatch.length ) || 0; | ||
// Truncate the anchor text if it is longer than the provided 'truncate' option | ||
if( truncateLen && anchorText.length > truncateLen ) { | ||
anchorText = anchorText.substring( 0, truncateLen - 2 ) + '..'; | ||
if( numOpenParens < numCloseParens ) { | ||
return true; | ||
} | ||
return anchorText; | ||
} | ||
return false; | ||
}, | ||
}; | ||
/** | ||
* Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. | ||
* Does not link URLs found within HTML tags. | ||
* Creates the return string value for a given match in the input string, for the {@link #processTextNode} method. | ||
* | ||
* For instance, if given the text: `You should go to http://www.yahoo.com`, then the result | ||
* will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` | ||
* This method handles the {@link #replaceFn}, if one was provided. | ||
* | ||
* Example: | ||
* | ||
* var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); | ||
* // Produces: "Go to <a href="http://google.com">google.com</a>" | ||
* | ||
* @static | ||
* @method link | ||
* @param {String} html The HTML text to link URLs within. | ||
* @param {Object} [options] Any of the configuration options for the Autolinker class, specified in an Object (map). | ||
* See the class description for an example call. | ||
* @return {String} The HTML text, with URLs automatically linked | ||
* @private | ||
* @param {Autolinker.match.Match} match The Match object that represents the match. | ||
* @param {String} matchStr The original match string, after having been preprocessed to fix match edge cases (see | ||
* the `prefixStr` and `suffixStr` vars in {@link #processTextNode}. | ||
* @return {String} The string that the `match` should be replaced with. This is usually the anchor tag string, but | ||
* may be the `matchStr` itself if the match is not to be replaced. | ||
*/ | ||
Autolinker.link = function( text, options ) { | ||
var autolinker = new Autolinker( options ); | ||
return autolinker.link( text ); | ||
}; | ||
createMatchReturnVal : function( match, matchStr ) { | ||
// Handle a custom `replaceFn` being provided | ||
var replaceFnResult; | ||
if( this.replaceFn ) { | ||
replaceFnResult = this.replaceFn.call( this, this, match ); // Autolinker instance is the context, and the first arg | ||
} | ||
if( typeof replaceFnResult === 'string' ) { | ||
return replaceFnResult; // `replaceFn` returned a string, use that | ||
} else if( replaceFnResult === false ) { | ||
return matchStr; // no replacement for the match | ||
} else if( replaceFnResult instanceof Autolinker.HtmlTag ) { | ||
return replaceFnResult.toString(); | ||
} else { // replaceFnResult === true, or no/unknown return value from function | ||
// Perform Autolinker's default anchor tag generation | ||
var tagBuilder = this.getTagBuilder(), | ||
anchorTag = tagBuilder.build( match ); // returns an Autolinker.HtmlTag instance | ||
return anchorTag.toString(); | ||
} | ||
} | ||
return Autolinker; | ||
} ) ); | ||
}; | ||
/** | ||
* Automatically links URLs, email addresses, and Twitter handles found in the given chunk of HTML. | ||
* Does not link URLs found within HTML tags. | ||
* | ||
* For instance, if given the text: `You should go to http://www.yahoo.com`, then the result | ||
* will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` | ||
* | ||
* Example: | ||
* | ||
* var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); | ||
* // Produces: "Go to <a href="http://google.com">google.com</a>" | ||
* | ||
* @static | ||
* @method link | ||
* @param {String} html The HTML text to link URLs within. | ||
* @param {Object} [options] Any of the configuration options for the Autolinker class, specified in an Object (map). | ||
* See the class description for an example call. | ||
* @return {String} The HTML text, with URLs automatically linked | ||
*/ | ||
Autolinker.link = function( text, options ) { | ||
var autolinker = new Autolinker( options ); | ||
return autolinker.link( text ); | ||
}; | ||
// Namespace for `match` classes | ||
Autolinker.match = {}; |
@@ -447,2 +447,8 @@ /*global Autolinker, _, describe, beforeEach, afterEach, it, expect */ | ||
it( "should properly link an email address in parenthesis", function() { | ||
var result = autolinker.link( "Joe's email is (joe@joe.com)" ); | ||
expect( result ).toBe( 'Joe\'s email is (<a href="mailto:joe@joe.com">joe@joe.com</a>)' ); | ||
} ); | ||
it( "should NOT automatically link any old word with an @ character in it", function() { | ||
@@ -615,2 +621,21 @@ var result = autolinker.link( "Hi there@stuff" ); | ||
it( "should NOT modify the email address with other tags when inside another anchor", function() { | ||
var input = [ | ||
'<div>First name: Subin</div>', | ||
'<div>Surname: Sundar</div>', | ||
'<div>', | ||
'Email: ', | ||
'<a href="mailto:subin.sundar@yo.in">', | ||
'<font color="blue"><u>s</u></font>', | ||
'<font color="blue"><u>ubin</u></font>', | ||
'<font color="blue"><u>.sundar@yo.in</u></font>', | ||
'</a>', | ||
'</div>' | ||
].join( "" ); | ||
var result = autolinker.link( input ); | ||
expect( result ).toBe( input ); | ||
} ); | ||
it( "should allow the full range of HTML attribute name characters as specified in the W3C HTML syntax document (http://www.w3.org/TR/html-markup/syntax.html)", function() { | ||
@@ -723,3 +748,27 @@ // Note: We aren't actually expecting the HTML to be modified by this test | ||
} ); | ||
it( "should handle after a url and not treat it as a query string", function() { | ||
var html = "<p>Joe went to yahoo.com and google.com today</p>"; | ||
var result = autolinker.link( html ); | ||
expect( result ).toBe('<p>Joe went to <a href="http://yahoo.com">yahoo.com</a> and <a href="http://google.com">google.com</a> today</p>'); | ||
} ); | ||
it( "should handle HTML entities like within a non-autolinked part of a text node, properly appending it to the output", function() { | ||
var html = "Joe went to yahoo.com and used HTML entities like > and < google.com"; | ||
var result = autolinker.link( html ); | ||
expect( result ).toBe( 'Joe went to <a href="http://yahoo.com">yahoo.com</a> and used HTML entities like > and < <a href="http://google.com">google.com</a>'); | ||
} ); | ||
it( "should handle & inside a url and not ignore it", function() { | ||
var html = "<p>Joe went to example.com?arg=1&arg=2&arg=3</p>"; | ||
var result = autolinker.link( html ); | ||
expect( result ).toBe( '<p>Joe went to <a href="http://example.com?arg=1&arg=2&arg=3">example.com?arg=1&arg=2&arg=3</a></p>' ); | ||
} ); | ||
} ); | ||
@@ -763,2 +812,8 @@ | ||
it( "should remove the prefix by default", function() { | ||
var result = Autolinker.link( "Test http://www.url.com", { newWindow: false } ); | ||
expect( result ).toBe( 'Test <a href="http://www.url.com">url.com</a>' ); | ||
} ); | ||
} ); | ||
@@ -868,3 +923,123 @@ | ||
describe( "`replaceFn` option", function() { | ||
var returnTrueFn = function() { return true; }, | ||
returnFalseFn = function() { return false; }; | ||
it( "should replace the match as Autolinker would normally do when `true` is returned from the `replaceFn`", function() { | ||
var result = Autolinker.link( "Website: asdf.com, Email: asdf@asdf.com, Twitter: @asdf", { | ||
newWindow : false, // just to suppress the target="_blank" from the output for this test | ||
replaceFn : returnTrueFn | ||
} ); | ||
expect( result ).toBe( [ | ||
'Website: <a href="http://asdf.com">asdf.com</a>', | ||
'Email: <a href="mailto:asdf@asdf.com">asdf@asdf.com</a>', | ||
'Twitter: <a href="https://twitter.com/asdf">@asdf</a>' | ||
].join( ", " ) ); | ||
} ); | ||
it( "should replace the match as Autolinker would normally do when there is no return value (i.e. `undefined` is returned) from the `replaceFn`", function() { | ||
var result = Autolinker.link( "Website: asdf.com, Email: asdf@asdf.com, Twitter: @asdf", { | ||
newWindow : false, // just to suppress the target="_blank" from the output for this test | ||
replaceFn : function() {} // no return value (`undefined` is returned) | ||
} ); | ||
expect( result ).toBe( [ | ||
'Website: <a href="http://asdf.com">asdf.com</a>', | ||
'Email: <a href="mailto:asdf@asdf.com">asdf@asdf.com</a>', | ||
'Twitter: <a href="https://twitter.com/asdf">@asdf</a>' | ||
].join( ", " ) ); | ||
} ); | ||
it( "should leave the match as-is when `false` is returned from the `replaceFn`", function() { | ||
var result = Autolinker.link( "Website: asdf.com, Email: asdf@asdf.com, Twitter: @asdf", { | ||
replaceFn : returnFalseFn | ||
} ); | ||
expect( result ).toBe( [ | ||
'Website: asdf.com', | ||
'Email: asdf@asdf.com', | ||
'Twitter: @asdf' | ||
].join( ", " ) ); | ||
} ); | ||
it( "should use a string returned from the `replaceFn` as the HTML that is replaced in the input", function() { | ||
var result = Autolinker.link( "Website: asdf.com, Email: asdf@asdf.com, Twitter: @asdf", { | ||
replaceFn : function() { return "test"; } | ||
} ); | ||
expect( result ).toBe( "Website: test, Email: test, Twitter: test" ); | ||
} ); | ||
it( "should allow an Autolinker.HtmlTag instance to be returned from the `replaceFn`, and use that as the HTML to be replaced from the input", function() { | ||
var result = Autolinker.link( "Website: asdf.com", { | ||
newWindow : false, | ||
replaceFn : function( autolinker, match ) { | ||
var tag = autolinker.getTagBuilder().build( match ); | ||
tag.setInnerHtml( 'asdf!' ); // just to check that we're replacing with the returned `tag` instance | ||
return tag; | ||
} | ||
} ); | ||
expect( result ).toBe( 'Website: <a href="http://asdf.com">asdf!</a>' ); | ||
} ); | ||
it( "should allow an Autolinker.HtmlTag instance to be modified before being returned from the `replaceFn`", function() { | ||
var result = Autolinker.link( "Website: asdf.com", { | ||
newWindow : false, | ||
replaceFn : function( autolinker, match ) { | ||
var tag = autolinker.getTagBuilder().build( match ); | ||
tag.addClass( 'test' ); | ||
tag.addClass( 'test2' ); | ||
tag.setAttr( 'rel', 'nofollow' ); | ||
return tag; | ||
} | ||
} ); | ||
expect( result ).toBe( 'Website: <a href="http://asdf.com" class="test test2" rel="nofollow">asdf.com</a>' ); | ||
} ); | ||
describe( 'special cases which check the `prefixStr` and `suffixStr` vars in the code', function() { | ||
it( "should leave the match as-is when the `replaceFn` returns `false` for a Twitter match", function() { | ||
var result = Autolinker.link( "@asdf", { replaceFn: returnFalseFn } ); | ||
expect( result ).toBe( "@asdf" ); | ||
var result2 = Autolinker.link( "Twitter: @asdf", { replaceFn: returnFalseFn } ); | ||
expect( result2 ).toBe( "Twitter: @asdf" ); | ||
} ); | ||
it( "should leave the match as-is when the `replaceFn` returns `false`, and the URL was wrapped in parenthesis", function() { | ||
var result = Autolinker.link( "Go to (yahoo.com) my friend", { replaceFn: returnFalseFn } ); | ||
expect( result ).toBe( "Go to (yahoo.com) my friend" ); | ||
var result2 = Autolinker.link( "Go to en.wikipedia.org/wiki/IANA_(disambiguation) my friend", { replaceFn: returnFalseFn } ); | ||
expect( result2 ).toBe( "Go to en.wikipedia.org/wiki/IANA_(disambiguation) my friend" ); | ||
var result3 = Autolinker.link( "Go to (en.wikipedia.org/wiki/IANA_(disambiguation)) my friend", { replaceFn: returnFalseFn } ); | ||
expect( result3 ).toBe( "Go to (en.wikipedia.org/wiki/IANA_(disambiguation)) my friend" ); | ||
} ); | ||
it( "should leave the match as-is when the `replaceFn` returns `false`, and the URL was a protocol-relative match", function() { | ||
var result = Autolinker.link( "Go to //yahoo.com my friend", { replaceFn: returnFalseFn } ); | ||
expect( result ).toBe( "Go to //yahoo.com my friend" ); | ||
} ); | ||
} ); | ||
} ); | ||
} ); | ||
} ); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
215151
23
3888
339
1