/**
* @class Autolinker
* @extends Object
*
* Utility class used to process a given string of text, and wrap the matches 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/Phone#/Twitter Handle/Hashtag match that is
* encountered.
*
* For example:
*
* var input = "..."; // string with URLs, Email Addresses, Phone #s, Twitter Handles, and Hashtags
*
* 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 'phone' :
* var phoneNumber = match.getPhoneNumber();
* console.log( phoneNumber );
*
* return '<a href="http://newplace.to.link.phone.numbers.to/">' + phoneNumber + '</a>';
*
* case 'twitter' :
* var twitterHandle = match.getTwitterHandle();
* console.log( twitterHandle );
*
* return '<a href="http://newplace.to.link.twitter.handles.to/">' + twitterHandle + '</a>';
*
* case 'hashtag' :
* var hashtag = match.getHashtag();
* console.log( hashtag );
*
* return '<a href="http://newplace.to.link.hashtag.handles.to/">' + hashtag + '</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.
* - An {@link Autolinker.HtmlTag} instance, which can be used to build/modify an HTML tag before writing out its HTML text.
*
* @constructor
* @param {Object} [cfg] 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.
// Validate the value of the `hashtag` cfg.
var hashtag = this.hashtag;
if( hashtag !== false && hashtag !== 'twitter' && hashtag !== 'facebook' && hashtag !== 'instagram' ) {
throw new Error( "invalid `hashtag` cfg - see docs" );
}
// Normalize the `truncate` option if provided
var truncate = this.truncate = this.truncate || {};
if( typeof truncate === 'number' ) {
this.truncate = { length: truncate, location: 'end' };
} else if( typeof truncate === 'object' ) {
this.truncate.length = truncate.length || Number.POSITIVE_INFINITY;
this.truncate.location = truncate.location || 'end';
}
};
Autolinker.prototype = {
constructor : Autolinker, // fix constructor property
/**
* @cfg {Boolean} urls
*
* `true` if miscellaneous URLs should be automatically linked, `false` if they should not be.
*/
urls : true,
/**
* @cfg {Boolean} email
*
* `true` if email addresses should be automatically linked, `false` if they should not be.
*/
email : true,
* @cfg {Boolean} twitter
*
* `true` if Twitter handles ("@example") should be automatically linked, `false` if they should not be.
*/
twitter : true,
/**
* @cfg {Boolean} phone
*
* `true` if Phone numbers ("(555)555-5555") should be automatically linked, `false` if they should not be.
*/
phone: true,
/**
* @cfg {Boolean/String} hashtag
*
* A string for the service name to have hashtags (ex: "#myHashtag")
* auto-linked to. The currently-supported values are:
*
* - 'twitter'
* - 'facebook'
* - 'instagram'
*
* Pass `false` to skip auto-linking of hashtags.
*/
hashtag : false,
/**
* @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 URL links' text, `false` otherwise.
*/
stripPrefix : true,
/**
* @cfg {Number/Object} truncate
*
* ## Number Form
*
* A number for how many characters matched text should be truncated to
* inside the text of a link. If the matched text 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..'
*
* Example Usage:
*
* truncate: 25
*
*
* ## Object Form
*
* An Object may also be provided with two properties: `length` (Number) and
* `location` (String). `location` may be one of the following: 'end'
* (default), 'middle', or 'smart'.
*
* Example Usage:
*
* truncate: { length: 25, location: 'middle' }
*
* @cfg {Number} truncate.length How many characters to allow before
* truncation will occur.
* @cfg {"end"/"middle"/"smart"} [truncate.location="end"]
*
* - 'end' (default): will truncate up to the number of characters, and then
* add an ellipsis at the end. Ex: 'yahoo.com/some/long/pat..'
* - 'middle': will truncate and add the ellipsis in the middle. Ex:
* 'yahoo.com/s..th/to/a/file'
* - 'smart': for URLs where the algorithm attempts to strip out unnecessary
* parts first (such as the 'www.', then URL scheme, hash, etc.),
* attempting to still make the URL human-readable before trying to find a
* good point to insert the ellipsis if it is still too long. Ex:
* 'yahoo.com/some..to/a/file'. For more details, see
* {@link Autolinker.truncate.TruncateSmart}.
*/
truncate : undefined,
/**
* @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 match suffixes for styling url/email/phone/twitter/hashtag links differently.
*
* For example, if this config is provided as "myLink", then:
*
* - URL links will have the CSS classes: "myLink myLink-url"
* - Email links will have the CSS classes: "myLink myLink-email", and
* - Twitter links will have the CSS classes: "myLink myLink-twitter"
* - Phone links will have the CSS classes: "myLink myLink-phone"
* - Hashtag links will have the CSS classes: "myLink myLink-hashtag"
*/
className : "",
/**
* @cfg {Function} replaceFn
*
* A function to individually process each match found in the input string.
*
* See the class's description for usage.
*
* This function is called with the following parameters:
*
* @cfg {Autolinker} replaceFn.autolinker The Autolinker instance, which may be used to retrieve child objects from (such
* as the instance's {@link #getTagBuilder tag builder}).
* @cfg {Autolinker.match.Match} replaceFn.match The Match instance which can be used to retrieve information about the
* match that the `replaceFn` is currently processing. See {@link Autolinker.match.Match} subclasses for details.
*/
/**
* @private
* @property {Autolinker.htmlParser.HtmlParser} htmlParser
*
* The HtmlParser instance used to skip over HTML tags, while finding text nodes to process. This is lazily instantiated
* in the {@link #getHtmlParser} method.
*/
htmlParser : undefined,
/**
* @private
* @property {Autolinker.matchParser.MatchParser} matchParser
*
* The MatchParser instance used to find matches in the text nodes of an input string passed to
* {@link #link}. This is lazily instantiated in the {@link #getMatchParser} method.
*/
matchParser : undefined,
/**
* @private
* @property {Autolinker.AnchorTagBuilder} tagBuilder
*
* The AnchorTagBuilder instance used to build match replacement anchor tags. Note: this is lazily instantiated
* in the {@link #getTagBuilder} method.
*/
tagBuilder : undefined,
/**
* Automatically links URLs, Email addresses, Phone numbers, Twitter
* handles, and Hashtags 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 autolink matches within
* (depending on if the {@link #urls}, {@link #email}, {@link #phone},
* {@link #twitter}, and {@link #hashtag} options are enabled).
* @return {String} The HTML, with matches automatically linked.
*/
link : function( textOrHtml ) {
if( !textOrHtml ) { return ""; } // handle `null` and `undefined`
var htmlParser = this.getHtmlParser(),
htmlNodes = htmlParser.parse( textOrHtml ),
anchorTagStackCount = 0, // used to only process text around anchor tags, and any inner text/html they may have
resultHtml = [];
for( var i = 0, len = htmlNodes.length; i < len; i++ ) {
var node = htmlNodes[ i ],
nodeType = node.getType(),
nodeText = node.getText();
if( nodeType === 'element' ) {
// Process HTML nodes in the input `textOrHtml`
if( node.getTagName() === 'a' ) {
if( !node.isClosing() ) { // 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( nodeText ); // now add the text of the tag itself verbatim
} else if( nodeType === 'entity' || nodeType === 'comment' ) {
resultHtml.push( nodeText ); // append HTML entity nodes (such as ' ') or HTML comments (such as '<!-- Comment -->') verbatim
} else {
// Process text nodes in the input `textOrHtml`
if( anchorTagStackCount === 0 ) {
// If we're not within an <a> tag, process the text node to linkify
var linkifiedStr = this.linkifyStr( nodeText );
resultHtml.push( linkifiedStr );
} 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( nodeText );
}
}
}
return resultHtml.join( "" );
},
/**
* Process the text that lies in between HTML tags, performing the anchor
* tag replacements for the matches, and returns the string with the
* replacements made.
*
* This method does the actual wrapping of matches with anchor tags.
*
* @private
* @param {String} str The string of text to auto-link.
* @return {String} The text with anchor tags auto-filled.
*/
linkifyStr : function( str ) {
return this.getMatchParser().replace( str, this.createMatchReturnVal, this );
},
/**
* Creates the return string value for a given match in the input string,
* for the {@link #linkifyStr} method.
*
* This method handles the {@link #replaceFn}, if one was provided.
*
* @private
* @param {Autolinker.match.Match} match The Match object that represents the match.
* @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 ) {
// 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 match.getMatchedText(); // no replacement for the match
} else if( replaceFnResult instanceof Autolinker.HtmlTag ) {
return replaceFnResult.toAnchorString();
} 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.toAnchorString();
}
},
/**
* Lazily instantiates and returns the {@link #htmlParser} instance for this Autolinker instance.
*
* @protected
* @return {Autolinker.htmlParser.HtmlParser}
*/
getHtmlParser : function() {
var htmlParser = this.htmlParser;
if( !htmlParser ) {
htmlParser = this.htmlParser = new Autolinker.htmlParser.HtmlParser();
}
return htmlParser;
},
/**
* Lazily instantiates and returns the {@link #matchParser} instance for this Autolinker instance.
*
* @protected
* @return {Autolinker.matchParser.MatchParser}
*/
getMatchParser : function() {
var matchParser = this.matchParser;
if( !matchParser ) {
matchParser = this.matchParser = new Autolinker.matchParser.MatchParser( {
urls : this.urls,
email : this.email,
twitter : this.twitter,
phone : this.phone,
hashtag : this.hashtag,
stripPrefix : this.stripPrefix
} );
}
return matchParser;
},
/**
* Returns the {@link #tagBuilder} instance for this Autolinker instance, lazily instantiating it
* if it does not yet exist.
*
* This method may be used in a {@link #replaceFn} to generate the {@link Autolinker.HtmlTag HtmlTag} instance that
* Autolinker would normally generate, and then allow for modifications before returning it. For example:
*
* var html = Autolinker.link( "Test google.com", {
* replaceFn : function( autolinker, match ) {
* var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance
* tag.setAttr( 'rel', 'nofollow' );
*
* return tag;
* }
* } );
*
* // generated html:
* // Test <a href="http://google.com" target="_blank" rel="nofollow">google.com</a>
*
* @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;
}
};
/**
* Automatically links URLs, Email addresses, Phone Numbers, Twitter handles,
* and Hashtags 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
* @param {String} textOrHtml The HTML or text to find matches within (depending
* on if the {@link #urls}, {@link #email}, {@link #phone}, {@link #twitter},
* and {@link #hashtag} options are enabled).
* @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 matches automatically linked.
*/
Autolinker.link = function( textOrHtml, options ) {
var autolinker = new Autolinker( options );
return autolinker.link( textOrHtml );
};
// Autolinker Namespaces
Autolinker.match = {};
Autolinker.htmlParser = {};
Autolinker.matchParser = {};
Autolinker.truncate = {};