From 6acba46339f0cb89689733a84c55432e8bfedb12 Mon Sep 17 00:00:00 2001 From: RainLoop Team Date: Wed, 15 Oct 2014 13:20:55 +0400 Subject: [PATCH] Updated Autolinker lib Fixed short links detection (Closes #354) --- dev/Common/Links.js | 2 +- dev/Common/Utils.js | 5 +- dev/Screen/App/MailBox.js | 6 +- vendors/Autolinker/Autolinker.js | 1263 +++++++++++++++++++----- vendors/Autolinker/Autolinker.min.js | 4 +- vendors/Autolinker/README.md | 95 ++ vendors/Autolinker/_/Autolinker.js | 1020 +++++++++++++++++++ vendors/Autolinker/_/Autolinker.min.js | 10 + vendors/Autolinker/_/LICENSE | 22 + vendors/Autolinker/_/README.md | 255 +++++ 10 files changed, 2424 insertions(+), 258 deletions(-) create mode 100644 vendors/Autolinker/_/Autolinker.js create mode 100644 vendors/Autolinker/_/Autolinker.min.js create mode 100644 vendors/Autolinker/_/LICENSE create mode 100644 vendors/Autolinker/_/README.md diff --git a/dev/Common/Links.js b/dev/Common/Links.js index eecb9ecd4..c9631075a 100644 --- a/dev/Common/Links.js +++ b/dev/Common/Links.js @@ -147,7 +147,7 @@ */ Links.prototype.inbox = function () { - return this.sBase + 'mailbox/Inbox'; + return this.sBase + 'mailbox/INBOX'; }; /** diff --git a/dev/Common/Utils.js b/dev/Common/Utils.js index 453f61f4a..738292807 100644 --- a/dev/Common/Utils.js +++ b/dev/Common/Utils.js @@ -1593,7 +1593,10 @@ 'stripPrefix': false, 'urls': true, 'email': true, - 'twitter': false + 'twitter': false, + 'replaceFn': function (autolinker, match) { + return !(autolinker && match && 'url' === match.getType() && match.matchedText && 0 !== match.matchedText.indexOf('http')); + } }); return sHtml; diff --git a/dev/Screen/App/MailBox.js b/dev/Screen/App/MailBox.js index 94e125184..13474c05d 100644 --- a/dev/Screen/App/MailBox.js +++ b/dev/Screen/App/MailBox.js @@ -153,7 +153,7 @@ { var fNormP = function () { - return ['Inbox', 1, '', true]; + return ['INBOX', 1, '', true]; }, fNormS = function (oRequest, oVals) { oVals[0] = Utils.pString(oVals[0]); @@ -163,7 +163,7 @@ if ('' === oRequest) { - oVals[0] = 'Inbox'; + oVals[0] = 'INBOX'; oVals[1] = 1; } @@ -175,7 +175,7 @@ if ('' === oRequest) { - oVals[0] = 'Inbox'; + oVals[0] = 'INBOX'; } return [decodeURI(oVals[0]), 1, decodeURI(oVals[1]), false]; diff --git a/vendors/Autolinker/Autolinker.js b/vendors/Autolinker/Autolinker.js index 5b91fa54e..01eaecc78 100644 --- a/vendors/Autolinker/Autolinker.js +++ b/vendors/Autolinker/Autolinker.js @@ -1,6 +1,6 @@ /*! * Autolinker.js - * 0.11.2 + * 0.12.2 * * Copyright(c) 2014 Gregory Jacobs * MIT Licensed. http://www.opensource.org/licenses/mit-license.php @@ -49,6 +49,64 @@ * } ); * // produces: 'Joe went to yahoo.com' * + * + * ## 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() ); + * + * 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 '' + twitterHandle + ''; + * } + * } + * } ); + * + * + * 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} [config] The configuration options for the Autolinker instance, specified in an Object (map). */ @@ -115,51 +173,38 @@ * * 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" + * - 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" */ 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. + * + * 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 + * {@link Autolinker.match.Url URL}/{@link Autolinker.match.Email email}/{@link Autolinker.match.Twitter Twitter} + * match that the `replaceFn` is currently processing. + */ /** * @private - * @property {RegExp} htmlRegex + * @property {RegExp} htmlCharacterEntitiesRegex + * + * The regular expression that matches common HTML character entities. * - * 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. + * Ignoring & as it could be part of a query string -- handling it separately. */ - 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, ' 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 tag. Ex: ) - '(?:', nameEqualsValueRegex, '|', attrValueRegex.source + ')', - ')*', - - '\\s*/?', // any trailing spaces and optional '/' before the closing '>' - '>' - ].join( "" ), 'g' ); - } )(), + htmlCharacterEntitiesRegex: /( | |<|<|>|>)/gi, /** * @private @@ -169,10 +214,10 @@ * * 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 + * 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 + * 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 @@ -273,6 +318,22 @@ */ charBeforeProtocolRelMatchRegex : /^(.)?\/\//, + /** + * @private + * @property {Autolinker.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. + */ + + /** + * @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. @@ -281,97 +342,107 @@ * 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 + * 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. + * @return {String} The HTML, with URLs/emails/Twitter handles automatically linked. */ link : function( textOrHtml ) { - return this.processHtml( 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 tag + anchorTagStackCount++; + } else { // it's the end tag + anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous 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 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 tag, simply append the text - we do not want to autolink anything + // already within an ... tag + resultHtml.push( text ); + } + } + } ); + + return resultHtml.join( "" ); }, /** - * Lazily instantiates and returns the {@link #anchorTagBuilder} instance for this Autolinker instance. + * 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. + * + * 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 google.com * * @return {Autolinker.AnchorTagBuilder} */ - getAnchorTagBuilder : function() { - var anchorTagBuilder = this.anchorTagBuilder; + getTagBuilder : function() { + var tagBuilder = this.tagBuilder; - if( !anchorTagBuilder ) { - anchorTagBuilder = this.anchorTagBuilder = new Autolinker.AnchorTagBuilder( { + if( !tagBuilder ) { + tagBuilder = this.tagBuilder = new Autolinker.AnchorTagBuilder( { newWindow : this.newWindow, - stripPrefix : this.stripPrefix, truncate : this.truncate, className : this.className } ); } - return anchorTagBuilder; - }, - - - /** - * 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 - if( tagName === 'a' ) { - if( !isClosingTag ) { // it's the start tag - anchorTagStackCount++; - resultHtml.push( this.processTextNode( inBetweenTagsText ) ); - - } else { // it's the end tag - anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous tags by making sure the stack count never goes below 0 - if( anchorTagStackCount === 0 ) { - resultHtml.push( inBetweenTagsText ); // We hit the matching tag, simply add all of the text from the start tag to the end 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: google.com), - // 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( "" ); + return tagBuilder; }, @@ -385,13 +456,11 @@ */ processTextNode : function( text ) { var me = this, // for closure - invalidProtocolRelMatchRegex = this.invalidProtocolRelMatchRegex, - charBeforeProtocolRelMatchRegex = this.charBeforeProtocolRelMatchRegex, - anchorTagBuilder = this.getAnchorTagBuilder(); + 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 + 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 @@ -400,40 +469,34 @@ 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 object + 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. + // 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. 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 tag - } + // 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 tag } - if( twitterMatch ) { - prefixStr = twitterHandlePrefixWhitespaceChar; - match = new Autolinker.TwitterMatch( { twitterHandle: twitterHandle } ); + if( emailAddressMatch ) { + match = new Autolinker.match.Email( { matchedText: matchStr, email: emailAddressMatch } ); - } else if( emailAddressMatch ) { - match = new Autolinker.EmailMatch( { emailAddress: 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( { matchedText: matchStr, twitterHandle: twitterHandle } ); } else { // url match // If it's a protocol-relative '//' match, remove the character before the '//' (which the matcherRegex needed @@ -441,18 +504,23 @@ if( protocolRelativeMatch ) { var charBeforeMatch = protocolRelativeMatch.match( charBeforeProtocolRelMatchRegex )[ 1 ] || ""; - if( charBeforeMatch ) { - prefixStr = charBeforeMatch; // re-add the character before the '//' to what will be placed before the generated tag - matchStr = matchStr.slice( 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.UrlMatch( { url: matchStr, protocolRelativeMatch: protocolRelativeMatch } ); + match = new Autolinker.match.Url( { + matchedText : matchStr, + url : matchStr, + protocolRelativeMatch : protocolRelativeMatch, + stripPrefix : me.stripPrefix + } ); } - // wrap the match in an anchor tag - var anchorTag = anchorTagBuilder.createAnchorTag( match.getType(), match.getAnchorHref(), match.getAnchorText() ); - return prefixStr + anchorTag + suffixStr; + // Generate the replacement text for the match + var matchReturnVal = me.createMatchReturnVal( match, matchStr ); + return prefixStr + matchReturnVal + suffixStr; } ); }, @@ -491,6 +559,77 @@ } 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(); + } } }; @@ -520,6 +659,11 @@ return autolinker.link( text ); }; + + // Namespace for `match` classes + Autolinker.match = {}; + /*global Autolinker */ + /*jshint eqnull:true, boss:true */ /** * @class Autolinker.Util * @singleton @@ -583,51 +727,588 @@ 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 */ /** * @private + * @class Autolinker.HtmlParser + * @extends Object + * + * An HTML parser implementation 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 within text nodes, basically ignoring HTML tags. + */ + Autolinker.HtmlParser = Autolinker.Util.extend( Object, { + + /** + * @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, ' 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 tag. Ex: ) + '(?:', nameEqualsValueRegex, '|', attrValueRegex.source + ')', + ')*', + + '\\s*/?', // any trailing spaces and optional '/' before the closing '>' + '>' + ].join( "" ), 'g' ); + } )(), + + + /** + * 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. + * + * @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. + */ + 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 easily build/modify HTML tags programmatically. + * + * Autolinker uses this abstraction to create HTML tags, and then write them out as strings. You may also use + * this class in your code, especially within a {@link Autolinker#replaceFn replaceFn}. + * + * ## Examples + * + * Example instantiation: + * + * var tag = new Autolinker.HtmlTag( { + * tagName : 'a', + * attrs : { 'href': 'http://google.com', 'class': 'external-link' }, + * innerHtml : 'Google' + * } ); + * + * tag.toString(); // Google + * + * // Individual accessor methods + * tag.getTagName(); // 'a' + * tag.getAttr( 'href' ); // 'http://google.com' + * tag.hasClass( 'external-link' ); // true + * + * + * Using mutator methods (which may be used in combination with instantiation config properties): + * + * var tag = new Autolinker.HtmlTag(); + * tag.setTagName( 'a' ); + * tag.setAttr( 'href', 'http://google.com' ); + * tag.addClass( 'external-link' ); + * tag.setInnerHtml( 'Google' ); + * + * tag.getTagName(); // 'a' + * tag.getAttr( 'href' ); // 'http://google.com' + * tag.hasClass( 'external-link' ); // true + * + * tag.toString(); // Google + * + * + * ## Example use within a {@link Autolinker#replaceFn replaceFn} + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance, configured with the Match's href and anchor text + * tag.setAttr( 'rel', 'nofollow' ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test google.com + * + * + * ## Example use with a new tag for the replacement + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = new Autolinker.HtmlTag( { + * tagName : 'button', + * attrs : { 'title': 'Load URL: ' + match.getAnchorHref() }, + * innerHtml : 'Load URL: ' + match.getAnchorText() + * } ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test + */ + Autolinker.HtmlTag = Autolinker.Util.extend( Object, { + + /** + * @cfg {String} tagName + * + * The tag name. Ex: 'a', 'button', etc. + * + * Not required at instantiation time, but should be set using {@link #setTagName} before {@link #toString} + * is executed. + */ + + /** + * @cfg {Object.} 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 + }, + + + /** + * Sets the tag name that will be used to generate the tag with. + * + * @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. + * + * @return {String} + */ + 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; + + 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.} 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.} 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 ); + } + } + + 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 ); + } + } + + this.getAttrs()[ 'class' ] = classes.join( " " ); + return this; + }, + + + /** + * Convenience method to retrieve the CSS class(es) for the HtmlTag, which will each be separated by spaces when + * there are multiple. + * + * @return {String} + */ + 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 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(), '' ].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 ] + '"' ); + } + } + return attrsArr.join( " " ); + } + + } ); + /*global Autolinker */ + /*jshint sub:true */ + /** + * @protected * @class Autolinker.AnchorTagBuilder * @extends Object * - * Builds the anchor (<a>) tags for the Autolinker utility when a match is found. + * Builds anchor (<a>) tags for the Autolinker utility when a match is found. + * + * Normally this class is instantiated, configured, and used internally by an {@link Autolinker} instance, but may + * actually be retrieved in a {@link Autolinker#replaceFn replaceFn} to create {@link Autolinker.HtmlTag HtmlTag} instances + * which may be modified before returning from the {@link Autolinker#replaceFn replaceFn}. 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 google.com */ Autolinker.AnchorTagBuilder = Autolinker.Util.extend( Object, { /** * @cfg {Boolean} newWindow - * - * See {@link Autolinker#newWindow} for details. - */ - - /** - * @cfg {Boolean} stripPrefix - * - * See {@link Autolinker#stripPrefix} for details. + * @inheritdoc Autolinker#newWindow */ /** * @cfg {Number} truncate - * - * See {@link Autolinker#truncate} for details. + * @inheritdoc Autolinker#truncate */ /** * @cfg {String} className - * - * See {@link Autolinker#className} for details. + * @inheritdoc Autolinker#className */ - - /** - * @private - * @property {RegExp} urlPrefixRegex - * - * A regular expression used to remove the 'http://' or 'https://' and/or the 'www.' from URLs. - */ - urlPrefixRegex: /^(https?:\/\/)?(www\.)?/i, - /** * @constructor @@ -639,41 +1320,45 @@ /** - * 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 the matched URL/email/Twitter text, + * via its `match` object. * - * @param {"url"/"email"/"twitter"} matchType The type of match 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( matchType, anchorHref, anchorText ) { - var attributesStr = this.createAnchorAttrsStr( matchType, 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 '' + anchorText + ''; + return tag; }, /** - * 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 + * @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( matchType, 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( 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; }, @@ -697,53 +1382,19 @@ /** - * 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. + * Processes the `anchorText` by 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 ); - } - anchorText = this.removeTrailingSlash( anchorText ); // remove trailing slash, if there is one anchorText = this.doTruncate( anchorText ); return anchorText; }, - /** - * 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 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 - * 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. @@ -753,24 +1404,50 @@ * @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; + return Autolinker.Util.ellipsis( anchorText, this.truncate || Number.POSITIVE_INFINITY ); } } ); + /*global Autolinker */ /** - * @private * @abstract - * @class Autolinker.Match + * @class Autolinker.match.Match * - * Represents a match found in an input string which should be Autolinked. + * Represents a match found in an input string which should be Autolinked. A Match object is what is provided in a + * {@link Autolinker#replaceFn replaceFn}, and may be used to query for details about the match. + * + * 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() ); + * + * case 'email' : + * console.log( "email: ", match.getEmail() ); + * + * case 'twitter' : + * console.log( "twitter: ", match.getTwitterHandle() ); + * } + * } + * } ); + * + * See the {@link Autolinker} class for more details on using the {@link Autolinker#replaceFn replaceFn}. */ - Autolinker.Match = Autolinker.Util.extend( Object, { + Autolinker.match.Match = Autolinker.Util.extend( Object, { + + /** + * @cfg {String} matchedText (required) + * + * The original text that was matched. + */ + /** * @constructor @@ -789,6 +1466,16 @@ */ getType : Autolinker.Util.abstractMethod, + + /** + * Returns the original text that was matched. + * + * @return {String} + */ + getMatchedText : function() { + return this.matchedText; + }, + /** * Returns the anchor href that should be generated for the match. @@ -808,16 +1495,19 @@ getAnchorText : Autolinker.Util.abstractMethod } ); + /*global Autolinker */ /** - * @private - * @class Autolinker.EmailMatch + * @class Autolinker.match.Email + * @extends Autolinker.match.Match * * Represents a Email match found in an input string which should be Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more details. */ - Autolinker.EmailMatch = Autolinker.Util.extend( Autolinker.Match, { + Autolinker.match.Email = Autolinker.Util.extend( Autolinker.match.Match, { /** - * @cfg {String} emailAddress (required) + * @cfg {String} email (required) * * The email address that was matched. */ @@ -838,8 +1528,8 @@ * * @return {String} */ - getEmailAddress : function() { - return this.emailAddress; + getEmail : function() { + return this.email; }, @@ -849,7 +1539,7 @@ * @return {String} */ getAnchorHref : function() { - return 'mailto:' + this.emailAddress; + return 'mailto:' + this.email; }, @@ -859,17 +1549,20 @@ * @return {String} */ getAnchorText : function() { - return this.emailAddress; + return this.email; } } ); + /*global Autolinker */ /** - * @private - * @class Autolinker.TwitterMatch + * @class Autolinker.match.Twitter + * @extends Autolinker.match.Match * * Represents a Twitter match found in an input string which should be Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more details. */ - Autolinker.TwitterMatch = Autolinker.Util.extend( Autolinker.Match, { + Autolinker.match.Twitter = Autolinker.Util.extend( Autolinker.match.Match, { /** * @cfg {String} twitterHandle (required) @@ -918,13 +1611,16 @@ } } ); + /*global Autolinker */ /** - * @private - * @class Autolinker.TwitterMatch + * @class Autolinker.match.Url + * @extends Autolinker.match.Match * * Represents a Url match found in an input string which should be Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more details. */ - Autolinker.UrlMatch = Autolinker.Util.extend( Autolinker.Match, { + Autolinker.match.Url = Autolinker.Util.extend( Autolinker.match.Match, { /** * @cfg {String} url (required) @@ -939,6 +1635,19 @@ * and will be either http:// or https:// based on the protocol that the site is loaded under. */ + /** + * @cfg {Boolean} stripPrefix (required) + * @inheritdoc {@link Autolinker#stripPrefix} + */ + + + /** + * @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 @@ -993,7 +1702,9 @@ * @return {String} */ getAnchorHref : function() { - return this.getUrl(); + 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 }, @@ -1003,14 +1714,64 @@ * @return {String} */ getAnchorText : function() { - var url = this.getUrl(); + var anchorText = this.getUrl(); if( this.protocolRelativeMatch ) { // Strip off any protocol-relative '//' from the anchor text - url = url.replace( this.protocolRelativeRegex, '' ); + anchorText = this.stripProtocolRelativePrefix( anchorText ); } + if( this.stripPrefix ) { + anchorText = this.stripUrlPrefix( anchorText ); + } + anchorText = this.removeTrailingSlash( anchorText ); // remove trailing slash, if there is one - return url; + return anchorText; + }, + + + // --------------------------------------- + + // Utility Functionality + + /** + * 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, '' ); + }, + + + /** + * 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 + * 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; } } ); diff --git a/vendors/Autolinker/Autolinker.min.js b/vendors/Autolinker/Autolinker.min.js index 6b0a97c63..5974a6de3 100644 --- a/vendors/Autolinker/Autolinker.min.js +++ b/vendors/Autolinker/Autolinker.min.js @@ -1,10 +1,10 @@ /*! * Autolinker.js - * 0.11.2 + * 0.12.2 * * Copyright(c) 2014 Gregory Jacobs * MIT Licensed. http://www.opensource.org/licenses/mit-license.php * * https://github.com/gregjacobs/Autolinker.js */ -!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:"",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")}(),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(a){return this.processHtml(a)},getAnchorTagBuilder:function(){var b=this.anchorTagBuilder;return b||(b=this.anchorTagBuilder=new a.AnchorTagBuilder({newWindow:this.newWindow,stripPrefix:this.stripPrefix,truncate:this.truncate,className:this.className})),b},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(ey&&(b=b.substr(0,b.length-1),u=")")}if(n)t=o,m=new a.TwitterMatch({twitterHandle:p});else if(q)m=new a.EmailMatch({emailAddress:q});else{if(s){var A=s.match(d)[1]||"";A&&(t=A,b=b.slice(1))}m=new a.UrlMatch({url:b,protocolRelativeMatch:s})}var B=e.createAnchorTag(m.getType(),m.getAnchorHref(),m.getAnchorText());return t+B+u})},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}},a.link=function(b,c){var d=new a(c);return d.link(b)},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}},a.AnchorTagBuilder=a.Util.extend(Object,{urlPrefixRegex:/^(https?:\/\/)?(www\.)?/i,constructor:function(b){a.Util.assign(this,b)},createAnchorTag:function(a,b,c){var d=this.createAnchorAttrsStr(a,b);return c=this.processAnchorText(c),""+c+""},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.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.EmailMatch=a.Util.extend(a.Match,{getType:function(){return"email"},getEmailAddress:function(){return this.emailAddress},getAnchorHref:function(){return"mailto:"+this.emailAddress},getAnchorText:function(){return this.emailAddress}}),a.TwitterMatch=a.Util.extend(a.Match,{getType:function(){return"twitter"},getTwitterHandle:function(){return this.twitterHandle},getAnchorHref:function(){return"https://twitter.com/"+this.twitterHandle},getAnchorText:function(){return"@"+this.twitterHandle}}),a.UrlMatch=a.Util.extend(a.Match,{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(){return this.getUrl()},getAnchorText:function(){var a=this.getUrl();return this.protocolRelativeMatch&&(a=a.replace(this.protocolRelativeRegex,"")),a}}),a}); \ No newline at end of file +!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({matchedText:b,email:p});else if(m)n&&(s=n,b=b.slice(1)),l=new a.match.Twitter({matchedText:b,twitterHandle:o});else{if(r){var u=r.match(d)[1]||"";u&&(s=u,b=b.slice(1))}l=new a.match.Url({matchedText:b,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",this.getInnerHtml(),""].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,getMatchedText:function(){return this.matchedText},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}); \ No newline at end of file diff --git a/vendors/Autolinker/README.md b/vendors/Autolinker/README.md index 76f327c38..c6df0f15f 100644 --- a/vendors/Autolinker/README.md +++ b/vendors/Autolinker/README.md @@ -70,6 +70,9 @@ var autolinker = new Autolinker( [ options ] ); var linkedText = autolinker.link( textToAutoLink ); ``` +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). + #### Example: @@ -105,6 +108,10 @@ These are the options which may be specified for linking. These are specified by `true` to have email addresses auto-linked, `false` to skip auto-linking of email addresses. Defaults to `true`.

- **twitter** : Boolean
`true` to have Twitter handles auto-linked, `false` to skip auto-linking of Twitter handles. Defaults to `true`. +- **replaceFn** : Function
+ A function to use to programmatically make replacements of matches in the input string, one at a time. See the section + Custom Replacement Function for more details. + For example, if you wanted to disable links from opening in new windows, you could do: @@ -146,8 +153,96 @@ autolinker.link( "Go to www.google.com" ); ``` +## 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 '' + twitterHandle + ''; + } + } +} ); +``` + + +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. + + +## Full API Docs + +The full API docs for Autolinker may be referenced at: [http://gregjacobs.github.io/Autolinker.js/docs/](http://gregjacobs.github.io/Autolinker.js/docs/#!/api/Autolinker) + + ## Changelog: +### 0.12.3 + +- Add `Autolinker.match.Match#getMatchedText` method + +### 0.12.2 + +- Add documentation generation, and update inline documentation. + +### 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 - Allow Autolinker to link fully-capitalized URLs/Emails/Twitter handles. diff --git a/vendors/Autolinker/_/Autolinker.js b/vendors/Autolinker/_/Autolinker.js new file mode 100644 index 000000000..5b91fa54e --- /dev/null +++ b/vendors/Autolinker/_/Autolinker.js @@ -0,0 +1,1020 @@ +/*! + * Autolinker.js + * 0.11.2 + * + * Copyright(c) 2014 Gregory Jacobs + * MIT Licensed. http://www.opensource.org/licenses/mit-license.php + * + * https://github.com/gregjacobs/Autolinker.js + */ +/*global define, module */ +( 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 yahoo.com' + * + * + * 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 yahoo.com' + * + * @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 + + /** + * @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} 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} 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 ('..') 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..' + */ + + /** + * @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} 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, ' 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 tag. Ex: ) + '(?:', nameEqualsValueRegex, '|', attrValueRegex.source + ')', + ')*', + + '\\s*/?', // any trailing spaces and optional '/' before the closing '>' + '>' + ].join( "" ), 'g' ); + } )(), + + /** + * @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, + ')', + + '|', + + '(', // *** 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, + ')', + ')', + + 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 : /^(.)?\/\//, + + + /** + * 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 ); + }, + + + /** + * Lazily instantiates and returns the {@link #anchorTagBuilder} instance for this Autolinker instance. + * + * @return {Autolinker.AnchorTagBuilder} + */ + getAnchorTagBuilder : function() { + var anchorTagBuilder = this.anchorTagBuilder; + + if( !anchorTagBuilder ) { + anchorTagBuilder = this.anchorTagBuilder = new Autolinker.AnchorTagBuilder( { + newWindow : this.newWindow, + stripPrefix : this.stripPrefix, + truncate : this.truncate, + className : this.className + } ); + } + + return anchorTagBuilder; + }, + + + /** + * 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 + if( tagName === 'a' ) { + if( !isClosingTag ) { // it's the start tag + anchorTagStackCount++; + resultHtml.push( this.processTextNode( inBetweenTagsText ) ); + + } else { // it's the end tag + anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous tags by making sure the stack count never goes below 0 + if( anchorTagStackCount === 0 ) { + resultHtml.push( inBetweenTagsText ); // We hit the matching tag, simply add all of the text from the start tag to the end 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: google.com), + // 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 closure + invalidProtocolRelMatchRegex = this.invalidProtocolRelMatchRegex, + charBeforeProtocolRelMatchRegex = this.charBeforeProtocolRelMatchRegex, + anchorTagBuilder = this.getAnchorTagBuilder(); + + 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 object + + + // Return out with no changes for match types that are disabled (url, email, twitter), or for matches + // that are invalid. + 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. 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 tag + } + } + + + if( twitterMatch ) { + prefixStr = twitterHandlePrefixWhitespaceChar; + match = new Autolinker.TwitterMatch( { twitterHandle: twitterHandle } ); + + } else if( emailAddressMatch ) { + match = new Autolinker.EmailMatch( { emailAddress: emailAddressMatch } ); + + } 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 ) { + prefixStr = charBeforeMatch; // re-add the character before the '//' to what will be placed before the generated tag + matchStr = matchStr.slice( 1 ); + } + } + + match = new Autolinker.UrlMatch( { url: matchStr, protocolRelativeMatch: protocolRelativeMatch } ); + } + + // wrap the match in an anchor tag + var anchorTag = anchorTagBuilder.createAnchorTag( match.getType(), match.getAnchorHref(), match.getAnchorText() ); + return prefixStr + anchorTag + 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; + } + + }; + + + /** + * 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 google.com" + * + * @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 ); + }; + + /** + * @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; + } + + }; + /** + * @private + * @class Autolinker.AnchorTagBuilder + * @extends Object + * + * Builds the 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 {Boolean} stripPrefix + * + * See {@link Autolinker#stripPrefix} for details. + */ + + /** + * @cfg {Number} truncate + * + * See {@link Autolinker#truncate} for details. + */ + + /** + * @cfg {String} className + * + * See {@link Autolinker#className} 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, + + + /** + * @constructor + * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). + */ + constructor : function( cfg ) { + Autolinker.Util.assign( this, cfg ); + }, + + + /** + * Generates the actual anchor (<a>) tag to use in place of a source url/email/twitter link. + * + * @param {"url"/"email"/"twitter"} matchType The type of match 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( matchType, anchorHref, anchorText ) { + var attributesStr = this.createAnchorAttrsStr( matchType, anchorHref ); + anchorText = this.processAnchorText( anchorText ); + + return '' + anchorText + ''; + }, + + + /** + * Creates the string which will be the HTML attributes for the anchor (<a>) tag being generated. + * + * @private + * @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"` + */ + createAnchorAttrsStr : function( matchType, anchorHref ) { + var attrs = [ 'href="' + anchorHref + '"' ]; // we'll always have the `href` attribute + + var cssClass = this.createCssClass( matchType ); + if( cssClass ) { + attrs.push( 'class="' + cssClass + '"' ); + } + if( this.newWindow ) { + attrs.push( 'target="_blank"' ); + } + + return attrs.join( " " ); + }, + + + /** + * 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"} 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( matchType ) { + var className = this.className; + + if( !className ) + return ""; + else + return className + " " + className + "-" + matchType; // 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 ); + } + anchorText = this.removeTrailingSlash( anchorText ); // remove trailing slash, if there is one + anchorText = this.doTruncate( anchorText ); + + return anchorText; + }, + + + /** + * 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 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 + * 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; + + // 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; + } + + } ); + /** + * @private + * @abstract + * @class Autolinker.Match + * + * Represents a match found in an input string which should be Autolinked. + */ + Autolinker.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 + + } ); + /** + * @private + * @class Autolinker.EmailMatch + * + * Represents a Email match found in an input string which should be Autolinked. + */ + Autolinker.EmailMatch = Autolinker.Util.extend( Autolinker.Match, { + + /** + * @cfg {String} emailAddress (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} + */ + getEmailAddress : function() { + return this.emailAddress; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @return {String} + */ + getAnchorHref : function() { + return 'mailto:' + this.emailAddress; + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + return this.emailAddress; + } + + } ); + /** + * @private + * @class Autolinker.TwitterMatch + * + * Represents a Twitter match found in an input string which should be Autolinked. + */ + Autolinker.TwitterMatch = Autolinker.Util.extend( Autolinker.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; + } + + } ); + /** + * @private + * @class Autolinker.TwitterMatch + * + * Represents a Url match found in an input string which should be Autolinked. + */ + Autolinker.UrlMatch = Autolinker.Util.extend( Autolinker.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. + */ + + + /** + * @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() { + return this.getUrl(); + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + var url = this.getUrl(); + + if( this.protocolRelativeMatch ) { + // Strip off any protocol-relative '//' from the anchor text + url = url.replace( this.protocolRelativeRegex, '' ); + } + + return url; + } + + } ); + + return Autolinker; + +} ) ); \ No newline at end of file diff --git a/vendors/Autolinker/_/Autolinker.min.js b/vendors/Autolinker/_/Autolinker.min.js new file mode 100644 index 000000000..6b0a97c63 --- /dev/null +++ b/vendors/Autolinker/_/Autolinker.min.js @@ -0,0 +1,10 @@ +/*! + * Autolinker.js + * 0.11.2 + * + * Copyright(c) 2014 Gregory Jacobs + * MIT Licensed. http://www.opensource.org/licenses/mit-license.php + * + * https://github.com/gregjacobs/Autolinker.js + */ +!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:"",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")}(),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(a){return this.processHtml(a)},getAnchorTagBuilder:function(){var b=this.anchorTagBuilder;return b||(b=this.anchorTagBuilder=new a.AnchorTagBuilder({newWindow:this.newWindow,stripPrefix:this.stripPrefix,truncate:this.truncate,className:this.className})),b},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(ey&&(b=b.substr(0,b.length-1),u=")")}if(n)t=o,m=new a.TwitterMatch({twitterHandle:p});else if(q)m=new a.EmailMatch({emailAddress:q});else{if(s){var A=s.match(d)[1]||"";A&&(t=A,b=b.slice(1))}m=new a.UrlMatch({url:b,protocolRelativeMatch:s})}var B=e.createAnchorTag(m.getType(),m.getAnchorHref(),m.getAnchorText());return t+B+u})},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}},a.link=function(b,c){var d=new a(c);return d.link(b)},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}},a.AnchorTagBuilder=a.Util.extend(Object,{urlPrefixRegex:/^(https?:\/\/)?(www\.)?/i,constructor:function(b){a.Util.assign(this,b)},createAnchorTag:function(a,b,c){var d=this.createAnchorAttrsStr(a,b);return c=this.processAnchorText(c),""+c+""},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.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.EmailMatch=a.Util.extend(a.Match,{getType:function(){return"email"},getEmailAddress:function(){return this.emailAddress},getAnchorHref:function(){return"mailto:"+this.emailAddress},getAnchorText:function(){return this.emailAddress}}),a.TwitterMatch=a.Util.extend(a.Match,{getType:function(){return"twitter"},getTwitterHandle:function(){return this.twitterHandle},getAnchorHref:function(){return"https://twitter.com/"+this.twitterHandle},getAnchorText:function(){return"@"+this.twitterHandle}}),a.UrlMatch=a.Util.extend(a.Match,{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(){return this.getUrl()},getAnchorText:function(){var a=this.getUrl();return this.protocolRelativeMatch&&(a=a.replace(this.protocolRelativeRegex,"")),a}}),a}); \ No newline at end of file diff --git a/vendors/Autolinker/_/LICENSE b/vendors/Autolinker/_/LICENSE new file mode 100644 index 000000000..3ff145b9f --- /dev/null +++ b/vendors/Autolinker/_/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Gregory Jacobs (http://greg-jacobs.com) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendors/Autolinker/_/README.md b/vendors/Autolinker/_/README.md new file mode 100644 index 000000000..76f327c38 --- /dev/null +++ b/vendors/Autolinker/_/README.md @@ -0,0 +1,255 @@ +# Autolinker.js + +Because I had so much trouble finding a good autolinking implementation out in the wild, I decided to roll my own. It +seemed that everything I found out there was either an implementation that didn't cover every case, or was just limited +in one way or another. + +So, this utility attempts to handle everything. It: + +- Autolinks URLs, whether or not they start with the protocol (i.e. 'http://'). In other words, it will automatically link the + text "google.com", as well as "http://google.com". +- Will properly handle URLs with special characters +- Will properly handle URLs with query parameters or a named anchor (i.e. hash) +- Will autolink email addresses. +- Will autolink Twitter handles. +- Will properly handle HTML input. The utility will not change the `href` attribute inside anchor (<a>) tags (or any other + tag/attribute for that matter), and will not accidentally wrap the inner text of an anchor tag with a new one (which would cause + doubly-nested anchor tags). + +Hope that this utility helps you as well! + + +## Installation + +#### Download + +Simply clone or download the zip of the project, and link to either `dist/Autolinker.js` or `dist/Autolinker.min.js` with a script tag: + +```html + +``` + +#### Using with the [Bower](http://bower.io) package manager: + +Command line: + +```shell +bower install Autolinker.js --save +``` + +#### Using with [Node.js](http://nodejs.org) via [npm](https://www.npmjs.org/): + +Command Line: + +```shell +npm install autolinker --save +``` + +JavaScript: + +```javascript +var Autolinker = require( 'autolinker' ); +// note: npm wants an all-lowercase package name, but the utility is a class and should be +// aliased with a captial letter +``` + + +## Usage + +Using the static `link()` method: + +```javascript +var linkedText = Autolinker.link( textToAutolink[, options] ); +``` + +Using as a class: + +```javascript +var autolinker = new Autolinker( [ options ] ); + +var linkedText = autolinker.link( textToAutoLink ); +``` + + +#### Example: + +```javascript +var linkedText = Autolinker.link( "Check out google.com", { className: "myLink" } ); +// Produces: "Check out google.com" +``` + +## Options + +These are the options which may be specified for linking. These are specified by providing an Object as the second parameter to `Autolinker.link()`. These include: + +- **newWindow** : Boolean
+ `true` to have the links should open in a new window when clicked, `false` otherwise. Defaults to `true`.

+- **stripPrefix** : Boolean
+ `true` to have the 'http://' or 'https://' and/or the 'www.' stripped from the beginning of links, `false` otherwise. Defaults to `true`.

+- **truncate** : Number
+ 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 the number of characters, it will be truncated to this length by replacing the end of the string with a two period ellipsis ('..').

+ Example: a url like 'http://www.yahoo.com/some/long/path/to/a/file' truncated to 25 characters may look like this: 'yahoo.com/some/long/pat..'
+- **className** : String
+ A CSS class name to add to the generated anchor tags. 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"
+ +- **urls** : Boolean
+ `true` to have URLs auto-linked, `false` to skip auto-linking of URLs. Defaults to `true`.
+- **email** : Boolean
+ `true` to have email addresses auto-linked, `false` to skip auto-linking of email addresses. Defaults to `true`.

+- **twitter** : Boolean
+ `true` to have Twitter handles auto-linked, `false` to skip auto-linking of Twitter handles. Defaults to `true`. + +For example, if you wanted to disable links from opening in new windows, you could do: + +```javascript +var linkedText = Autolinker.link( "Check out google.com", { newWindow: false } ); +// Produces: "Check out google.com" +``` + +And if you wanted to truncate the length of URLs (while also not opening in a new window), you could do: + +```javascript +var linkedText = Autolinker.link( "http://www.yahoo.com/some/long/path/to/a/file", { truncate: 25, newWindow: false } ); +// Produces: "yahoo.com/some/long/pat.." +``` + +## More Examples +One could update an entire DOM element that has unlinked text to auto-link them as such: + +```javascript +var myTextEl = document.getElementById( 'text' ); +myTextEl.innerHTML = Autolinker.link( myTextEl.innerHTML ); +``` + +Using the same pre-configured Autolinker instance in multiple locations of a codebase (usually by dependency injection): + +```javascript +var autolinker = new Autolinker( { newWindow: false, truncate: 25 } ); + +//... + +autolinker.link( "Check out http://www.yahoo.com/some/long/path/to/a/file" ); +// Produces: "Check out yahoo.com/some/long/pat.." + +//... + +autolinker.link( "Go to www.google.com" ); +// Produces: "Go to google.com" + +``` + + +## Changelog: + +### 0.11.0 + +- Allow Autolinker to link fully-capitalized URLs/Emails/Twitter handles. + +### 0.10.1 + +- Added fix to not autolink strings like "version:1.0", which were accidentally being interpreted as a protocol:domain string. + +### 0.10.0 + +- Added support for protocol-relative URLs (ex: `//google.com`, which will effectively either have the `http://` or `https://` + protocol depending on the protocol that is hosting the website) + +### 0.9.4 + +- Fixed an issue where a string in the form of `abc:def` would be autolinked as a protocol and domain name URL. Autolinker now + requires the domain name to have at least one period in it to be considered. + +### 0.9.3 + +- Fixed an issue where Twitter handles wouldn't be autolinked if they existed as the sole entity within parenthesis or brackets + (thanks [@busticated](https://github.com/busticated) for pointing this out and providing unit tests) + +### 0.9.2 + +- Fixed an issue with nested tags within an existing <a> tag, where the nested tags' inner text would be accidentally + removed from the output (thanks [@mjsabin01](https://github.com/mjsabin01)) + +### 0.9.1 + +- Added a patch to attempt to better handle extraneous </a> tags in the input string if any exist. This is for when the + input may have some invalid markup (for instance, on sites which allow user comments, blog posts, etc.). + +### 0.9.0 + +- Added better support for the processing of existing HTML in the input string. Now handles namespaced tags, and attribute names + with dashes or any other Unicode character (thanks [@aziraphale](https://github.com/aziraphale)) + +### 0.8.0 + +- Added `className` option for easily styling produced links (thanks [@busticated](https://github.com/busticated)) +- Refactored into a JS class. Autolinker can now be instantiated using: + +```javascript +var autolinker = new Autolinker( { newWindow: false, truncate: 25 } ); + +autolinker.link( "Check out http://www.yahoo.com/some/long/path/to/a/file" ); +// Produces: "Check out yahoo.com/some/long/pat.." +``` + +This allows options to be set on a single instance, and used throughout a codebase by injecting the `autolinker` instance as a dependency to the modules/classes that use it. (Note: Autolinker may still be used with the static `Autolinker.link()` method as was previously available as well.) + +### 0.7.0 + +- Changed build system to Grunt. +- Added AMD and CommonJS module loading support (ex: RequireJS, and Node.js's module loader). +- Added command line Jasmine test runner (`grunt test`) +- Upgraded Jasmine from 1.3.1 to 2.0 +- Added license header to dist files. + +(Thanks to [@busticated](https://github.com/busticated)!) + +### 0.6.1 + +- Added LICENSE file to repository. + +### 0.6.0 + +- Added options for granular control of which types are linked (urls, email addresses, and/or twitter handles). + (thanks [@aziraphale](https://github.com/aziraphale)) + +### 0.5.0 + +- Simplified the path / query string / hash processing into a single regular expression instead of 3 separate ones. +- Added support for parenthesis in URLs, such as: `en.wikipedia.org/wiki/IANA_(disambiguation)` (thanks [@dandv](https://github.com/dandv)) +- Add all known top-level domains (TLDs) (thanks [@wouter0100](https://github.com/wouter0100)) + +### 0.4.0 + +Merged pull requests from [@afeld](https://github.com/afeld): + +- strip protocol and 'www.' by default - fixes #1 +- truncate URLs from the end +- make simpler regex for detecting prefix +- remove trailing slashes from URLs, and handle periods at the end of paths +- re-use domain+TLD regexes for email matching +- add .me and .io to list of TLDs + +Thanks Aidan :) + +### 0.3.1 + +- Fixed an issue with handling nested HTML tags within anchor tags. + +### 0.3 + +- Implemented the `truncate` option. + +### 0.2 + +- Implemented autolinking Twitter handles. + +### 0.1 + +* Initial implementation, which autolinks URLs and email addresses. Working on linking Twitter handles.