import { defaults, remove, splitAndCapture } from "./utils"; import { AnchorTagBuilder } from "./anchor-tag-builder"; import { Match } from "./match/match"; import { EmailMatch } from "./match/email-match"; import { HashtagMatch } from "./match/hashtag-match"; import { MentionMatch } from "./match/mention-match"; import { PhoneMatch } from "./match/phone-match"; import { UrlMatch } from "./match/url-match"; import { Matcher } from "./matcher/matcher"; import { HtmlTag } from "./html-tag"; import { EmailMatcher } from "./matcher/email-matcher"; import { UrlMatcher } from "./matcher/url-matcher"; import { HashtagMatcher } from "./matcher/hashtag-matcher"; import { PhoneMatcher } from "./matcher/phone-matcher"; import { MentionMatcher } from "./matcher/mention-matcher"; import { parseHtml } from './htmlParser/parse-html'; /** * @class Autolinker * @extends Object * * Utility class used to process a given string of text, and wrap the matches in * the appropriate anchor (<a>) tags to turn them into links. * * Any of the configuration options may be provided in an Object 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' * * * ## Custom Replacements of Links * * If the configuration options do not provide enough flexibility, a {@link #replaceFn} * may be provided to fully customize the output of Autolinker. This function is * called once for each URL/Email/Phone#/Hashtag/Mention (Twitter, Instagram, Soundcloud) * match that is encountered. * * For example: * * var input = "..."; // string with URLs, Email Addresses, Phone #s, Hashtags, and Mentions (Twitter, Instagram, Soundcloud) * * var linkedText = Autolinker.link( input, { * replaceFn : function( 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 = match.buildTag(); // returns an `Autolinker.HtmlTag` instance, which provides mutator methods for easy changes * tag.setAttr( 'rel', 'nofollow' ); * tag.addClass( 'external-link' ); * * return tag; * * } else { * return true; // let Autolinker perform its normal anchor tag replacement * } * * case 'email' : * var email = match.getEmail(); * console.log( "email: ", email ); * * if( email === "my@own.address" ) { * return false; // don't auto-link this particular email address; leave as-is * } else { * return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) * } * * case 'phone' : * var phoneNumber = match.getPhoneNumber(); * console.log( phoneNumber ); * * return '' + phoneNumber + ''; * * case 'hashtag' : * var hashtag = match.getHashtag(); * console.log( hashtag ); * * return '' + hashtag + ''; * * case 'mention' : * var mention = match.getMention(); * console.log( mention ); * * return '' + mention + ''; * } * } * } ); * * * 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. */ var Autolinker = /** @class */ (function () { /** * @method constructor * @param {Object} [cfg] The configuration options for the Autolinker instance, * specified in an Object (map). */ function Autolinker(cfg) { if (cfg === void 0) { cfg = {}; } /** * The Autolinker version number exposed on the instance itself. * * Ex: 0.25.1 */ this.version = Autolinker.version; /** * @cfg {Boolean/Object} [urls] * * `true` if URLs should be automatically linked, `false` if they should not * be. Defaults to `true`. * * Examples: * * urls: true * * // or * * urls: { * schemeMatches : true, * wwwMatches : true, * tldMatches : true * } * * As shown above, this option also accepts an Object form with 3 properties * to allow for more customization of what exactly gets linked. All default * to `true`: * * @cfg {Boolean} [urls.schemeMatches] `true` to match URLs found prefixed * with a scheme, i.e. `http://google.com`, or `other+scheme://google.com`, * `false` to prevent these types of matches. * @cfg {Boolean} [urls.wwwMatches] `true` to match urls found prefixed with * `'www.'`, i.e. `www.google.com`. `false` to prevent these types of * matches. Note that if the URL had a prefixed scheme, and * `schemeMatches` is true, it will still be linked. * @cfg {Boolean} [urls.tldMatches] `true` to match URLs with known top * level domains (.com, .net, etc.) that are not prefixed with a scheme or * `'www.'`. This option attempts to match anything that looks like a URL * in the given text. Ex: `google.com`, `asdf.org/?page=1`, etc. `false` * to prevent these types of matches. */ this.urls = {}; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Boolean} [email=true] * * `true` if email addresses should be automatically linked, `false` if they * should not be. */ this.email = true; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Boolean} [phone=true] * * `true` if Phone numbers ("(555)555-5555") should be automatically linked, * `false` if they should not be. */ this.phone = true; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Boolean/String} [hashtag=false] * * A string for the service name to have hashtags (ex: "#myHashtag") * auto-linked to. The currently-supported values are: * * - 'twitter' * - 'facebook' * - 'instagram' * * Pass `false` to skip auto-linking of hashtags. */ this.hashtag = false; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {String/Boolean} [mention=false] * * A string for the service name to have mentions (ex: "@myuser") * auto-linked to. The currently supported values are: * * - 'twitter' * - 'instagram' * - 'soundcloud' * * Defaults to `false` to skip auto-linking of mentions. */ this.mention = false; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Boolean} [newWindow=true] * * `true` if the links should open in a new window, `false` otherwise. */ this.newWindow = true; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Boolean/Object} [stripPrefix=true] * * `true` if 'http://' (or 'https://') and/or the 'www.' should be stripped * from the beginning of URL links' text, `false` otherwise. Defaults to * `true`. * * Examples: * * stripPrefix: true * * // or * * stripPrefix: { * scheme : true, * www : true * } * * As shown above, this option also accepts an Object form with 2 properties * to allow for more customization of what exactly is prevented from being * displayed. Both default to `true`: * * @cfg {Boolean} [stripPrefix.scheme] `true` to prevent the scheme part of * a URL match from being displayed to the user. Example: * `'http://google.com'` will be displayed as `'google.com'`. `false` to * not strip the scheme. NOTE: Only an `'http://'` or `'https://'` scheme * will be removed, so as not to remove a potentially dangerous scheme * (such as `'file://'` or `'javascript:'`) * @cfg {Boolean} [stripPrefix.www] www (Boolean): `true` to prevent the * `'www.'` part of a URL match from being displayed to the user. Ex: * `'www.google.com'` will be displayed as `'google.com'`. `false` to not * strip the `'www'`. */ this.stripPrefix = { scheme: true, www: true }; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Boolean} [stripTrailingSlash=true] * * `true` to remove the trailing slash from URL matches, `false` to keep * the trailing slash. * * Example when `true`: `http://google.com/` will be displayed as * `http://google.com`. */ this.stripTrailingSlash = true; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Boolean} [decodePercentEncoding=true] * * `true` to decode percent-encoded characters in URL matches, `false` to keep * the percent-encoded characters. * * Example when `true`: `https://en.wikipedia.org/wiki/San_Jos%C3%A9` will * be displayed as `https://en.wikipedia.org/wiki/San_José`. */ this.decodePercentEncoding = true; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Number/Object} [truncate=0] * * ## Number Form * * A number for how many characters matched text should be truncated to * inside the text of a link. If the matched text is over this number of * characters, it will be truncated to this length by adding a two period * ellipsis ('..') to the end of the string. * * For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' * truncated to 25 characters might look something like this: * 'yahoo.com/some/long/pat..' * * Example Usage: * * truncate: 25 * * * Defaults to `0` for "no truncation." * * * ## Object Form * * An Object may also be provided with two properties: `length` (Number) and * `location` (String). `location` may be one of the following: 'end' * (default), 'middle', or 'smart'. * * Example Usage: * * truncate: { length: 25, location: 'middle' } * * @cfg {Number} [truncate.length=0] How many characters to allow before * truncation will occur. Defaults to `0` for "no truncation." * @cfg {"end"/"middle"/"smart"} [truncate.location="end"] * * - 'end' (default): will truncate up to the number of characters, and then * add an ellipsis at the end. Ex: 'yahoo.com/some/long/pat..' * - 'middle': will truncate and add the ellipsis in the middle. Ex: * 'yahoo.com/s..th/to/a/file' * - 'smart': for URLs where the algorithm attempts to strip out unnecessary * parts first (such as the 'www.', then URL scheme, hash, etc.), * attempting to make the URL human-readable before looking for a good * point to insert the ellipsis if it is still too long. Ex: * 'yahoo.com/some..to/a/file'. For more details, see * {@link Autolinker.truncate.TruncateSmart}. */ this.truncate = { length: 0, location: 'end' }; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {String} className * * A CSS class name to add to the generated links. This class will be added * to all links, as well as this class plus match suffixes for styling * url/email/phone/hashtag/mention links differently. * * For example, if this config is provided as "myLink", then: * * - URL links will have the CSS classes: "myLink myLink-url" * - Email links will have the CSS classes: "myLink myLink-email", and * - Phone links will have the CSS classes: "myLink myLink-phone" * - Hashtag links will have the CSS classes: "myLink myLink-hashtag" * - Mention links will have the CSS classes: "myLink myLink-mention myLink-[type]" * where [type] is either "instagram", "twitter" or "soundcloud" */ this.className = ''; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Function} replaceFn * * A function to individually process each match found in the input string. * * See the class's description for usage. * * The `replaceFn` can be called with a different context object (`this` * reference) using the {@link #context} cfg. * * This function is called with the following parameter: * * @cfg {Autolinker.match.Match} replaceFn.match The Match instance which * can be used to retrieve information about the match that the `replaceFn` * is currently processing. See {@link Autolinker.match.Match} subclasses * for details. */ this.replaceFn = null; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Object} context * * The context object (`this` reference) to call the `replaceFn` with. * * Defaults to this Autolinker instance. */ this.context = undefined; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @cfg {Boolean} [sanitizeHtml=false] * * `true` to HTML-encode the start and end brackets of existing HTML tags found * in the input string. This will escape `<` and `>` characters to `<` and * `>`, respectively. * * Setting this to `true` will prevent XSS (Cross-site Scripting) attacks, * but will remove the significance of existing HTML tags in the input string. If * you would like to maintain the significance of existing HTML tags while also * making the output HTML string safe, leave this option as `false` and use a * tool like https://github.com/cure53/DOMPurify (or others) on the input string * before running Autolinker. */ this.sanitizeHtml = false; // default value just to get the above doc comment in the ES5 output and documentation generator /** * @private * @property {Autolinker.matcher.Matcher[]} matchers * * The {@link Autolinker.matcher.Matcher} instances for this Autolinker * instance. * * This is lazily created in {@link #getMatchers}. */ this.matchers = null; /** * @private * @property {Autolinker.AnchorTagBuilder} tagBuilder * * The AnchorTagBuilder instance used to build match replacement anchor tags. * Note: this is lazily instantiated in the {@link #getTagBuilder} method. */ this.tagBuilder = null; // Note: when `this.something` is used in the rhs of these assignments, // it refers to the default values set above the constructor this.urls = this.normalizeUrlsCfg(cfg.urls); this.email = typeof cfg.email === 'boolean' ? cfg.email : this.email; this.phone = typeof cfg.phone === 'boolean' ? cfg.phone : this.phone; this.hashtag = cfg.hashtag || this.hashtag; this.mention = cfg.mention || this.mention; this.newWindow = typeof cfg.newWindow === 'boolean' ? cfg.newWindow : this.newWindow; this.stripPrefix = this.normalizeStripPrefixCfg(cfg.stripPrefix); this.stripTrailingSlash = typeof cfg.stripTrailingSlash === 'boolean' ? cfg.stripTrailingSlash : this.stripTrailingSlash; this.decodePercentEncoding = typeof cfg.decodePercentEncoding === 'boolean' ? cfg.decodePercentEncoding : this.decodePercentEncoding; this.sanitizeHtml = cfg.sanitizeHtml || false; // Validate the value of the `mention` cfg var mention = this.mention; if (mention !== false && mention !== 'mastodon' && mention !== 'twitter' && mention !== 'instagram' && mention !== 'soundcloud') { throw new Error("invalid `mention` cfg - see docs"); } // Validate the value of the `hashtag` cfg var hashtag = this.hashtag; if (hashtag !== false && hashtag !== 'twitter' && hashtag !== 'facebook' && hashtag !== 'instagram') { throw new Error("invalid `hashtag` cfg - see docs"); } this.truncate = this.normalizeTruncateCfg(cfg.truncate); this.className = cfg.className || this.className; this.replaceFn = cfg.replaceFn || this.replaceFn; this.context = cfg.context || this; } /** * Automatically links URLs, Email addresses, Phone Numbers, Twitter handles, * Hashtags, and Mentions 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 * @param {String} textOrHtml The HTML or text to find matches within (depending * on if the {@link #urls}, {@link #email}, {@link #phone}, {@link #mention}, * {@link #hashtag}, and {@link #mention} options are enabled). * @param {Object} [options] Any of the configuration options for the Autolinker * class, specified in an Object (map). See the class description for an * example call. * @return {String} The HTML text, with matches automatically linked. */ Autolinker.link = function (textOrHtml, options) { var autolinker = new Autolinker(options); return autolinker.link(textOrHtml); }; /** * Parses the input `textOrHtml` looking for URLs, email addresses, phone * numbers, username handles, and hashtags (depending on the configuration * of the Autolinker instance), and returns an array of {@link Autolinker.match.Match} * objects describing those matches (without making any replacements). * * Note that if parsing multiple pieces of text, it is slightly more efficient * to create an Autolinker instance, and use the instance-level {@link #parse} * method. * * Example: * * var matches = Autolinker.parse( "Hello google.com, I am asdf@asdf.com", { * urls: true, * email: true * } ); * * console.log( matches.length ); // 2 * console.log( matches[ 0 ].getType() ); // 'url' * console.log( matches[ 0 ].getUrl() ); // 'google.com' * console.log( matches[ 1 ].getType() ); // 'email' * console.log( matches[ 1 ].getEmail() ); // 'asdf@asdf.com' * * @static * @param {String} textOrHtml The HTML or text to find matches within * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, * {@link #hashtag}, and {@link #mention} options are enabled). * @param {Object} [options] Any of the configuration options for the Autolinker * class, specified in an Object (map). See the class description for an * example call. * @return {Autolinker.match.Match[]} The array of Matches found in the * given input `textOrHtml`. */ Autolinker.parse = function (textOrHtml, options) { var autolinker = new Autolinker(options); return autolinker.parse(textOrHtml); }; /** * Normalizes the {@link #urls} config into an Object with 3 properties: * `schemeMatches`, `wwwMatches`, and `tldMatches`, all Booleans. * * See {@link #urls} config for details. * * @private * @param {Boolean/Object} urls * @return {Object} */ Autolinker.prototype.normalizeUrlsCfg = function (urls) { if (urls == null) urls = true; // default to `true` if (typeof urls === 'boolean') { return { schemeMatches: urls, wwwMatches: urls, tldMatches: urls }; } else { // object form return { schemeMatches: typeof urls.schemeMatches === 'boolean' ? urls.schemeMatches : true, wwwMatches: typeof urls.wwwMatches === 'boolean' ? urls.wwwMatches : true, tldMatches: typeof urls.tldMatches === 'boolean' ? urls.tldMatches : true }; } }; /** * Normalizes the {@link #stripPrefix} config into an Object with 2 * properties: `scheme`, and `www` - both Booleans. * * See {@link #stripPrefix} config for details. * * @private * @param {Boolean/Object} stripPrefix * @return {Object} */ Autolinker.prototype.normalizeStripPrefixCfg = function (stripPrefix) { if (stripPrefix == null) stripPrefix = true; // default to `true` if (typeof stripPrefix === 'boolean') { return { scheme: stripPrefix, www: stripPrefix }; } else { // object form return { scheme: typeof stripPrefix.scheme === 'boolean' ? stripPrefix.scheme : true, www: typeof stripPrefix.www === 'boolean' ? stripPrefix.www : true }; } }; /** * Normalizes the {@link #truncate} config into an Object with 2 properties: * `length` (Number), and `location` (String). * * See {@link #truncate} config for details. * * @private * @param {Number/Object} truncate * @return {Object} */ Autolinker.prototype.normalizeTruncateCfg = function (truncate) { if (typeof truncate === 'number') { return { length: truncate, location: 'end' }; } else { // object, or undefined/null return defaults(truncate || {}, { length: Number.POSITIVE_INFINITY, location: 'end' }); } }; /** * Parses the input `textOrHtml` looking for URLs, email addresses, phone * numbers, username handles, and hashtags (depending on the configuration * of the Autolinker instance), and returns an array of {@link Autolinker.match.Match} * objects describing those matches (without making any replacements). * * This method is used by the {@link #link} method, but can also be used to * simply do parsing of the input in order to discover what kinds of links * there are and how many. * * Example usage: * * var autolinker = new Autolinker( { * urls: true, * email: true * } ); * * var matches = autolinker.parse( "Hello google.com, I am asdf@asdf.com" ); * * console.log( matches.length ); // 2 * console.log( matches[ 0 ].getType() ); // 'url' * console.log( matches[ 0 ].getUrl() ); // 'google.com' * console.log( matches[ 1 ].getType() ); // 'email' * console.log( matches[ 1 ].getEmail() ); // 'asdf@asdf.com' * * @param {String} textOrHtml The HTML or text to find matches within * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, * {@link #hashtag}, and {@link #mention} options are enabled). * @return {Autolinker.match.Match[]} The array of Matches found in the * given input `textOrHtml`. */ Autolinker.prototype.parse = function (textOrHtml) { var _this = this; var skipTagNames = ['a', 'style', 'script'], skipTagsStackCount = 0, // used to only Autolink text outside of anchor/script/style tags. We don't want to autolink something that is already linked inside of an tag, for instance matches = []; // Find all matches within the `textOrHtml` (but not matches that are // already nested within ,