diff --git a/package.json b/package.json index 145d2f02..fb595589 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@react-navigation/native": "^5.8.6", "@react-navigation/stack": "^5.12.3", "@reduxjs/toolkit": "^1.4.0", - "autolinker": "^3.14.2", + "autolinker": "./src/modules/autolinker", "expo": "~39.0.4", "expo-app-auth": "~9.2.0", "expo-av": "~8.6.0", @@ -59,4 +59,4 @@ "typescript": "~3.9.2" }, "private": true -} \ No newline at end of file +} diff --git a/src/modules/autolinker/anchor-tag-builder.d.ts b/src/modules/autolinker/anchor-tag-builder.d.ts new file mode 100644 index 00000000..09a5965d --- /dev/null +++ b/src/modules/autolinker/anchor-tag-builder.d.ts @@ -0,0 +1,120 @@ +import { Match } from "./match/match"; +import { HtmlTag } from "./html-tag"; +import { TruncateConfigObj } from "./autolinker"; +/** + * @protected + * @class Autolinker.AnchorTagBuilder + * @extends Object + * + * 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 used indirectly 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( match ) { + * var tag = match.buildTag(); // returns an {@link Autolinker.HtmlTag} instance + * tag.setAttr( 'rel', 'nofollow' ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test google.com + */ +export declare class AnchorTagBuilder { + /** + * @cfg {Boolean} newWindow + * @inheritdoc Autolinker#newWindow + */ + private readonly newWindow; + /** + * @cfg {Object} truncate + * @inheritdoc Autolinker#truncate + */ + private readonly truncate; + /** + * @cfg {String} className + * @inheritdoc Autolinker#className + */ + private readonly className; + /** + * @method constructor + * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). + */ + constructor(cfg?: AnchorTagBuilderCfg); + /** + * Generates the actual anchor (<a>) tag to use in place of the + * matched text, via its `match` object. + * + * @param {Autolinker.match.Match} match The Match instance to generate an + * anchor tag from. + * @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag. + */ + build(match: Match): HtmlTag; + /** + * Creates the Object (map) of the HTML attributes for the anchor (<a>) + * tag being generated. + * + * @protected + * @param {Autolinker.match.Match} match The Match instance to generate an + * anchor tag from. + * @return {Object} A key/value Object (map) of the anchor tag's attributes. + */ + protected createAttrs(match: Match): { + [attrName: string]: string; + }; + /** + * Creates the CSS class that will be used for a given anchor tag, based on + * the `matchType` and the {@link #className} config. + * + * Example returns: + * + * - "" // no {@link #className} + * - "myLink myLink-url" // url match + * - "myLink myLink-email" // email match + * - "myLink myLink-phone" // phone match + * - "myLink myLink-hashtag" // hashtag match + * - "myLink myLink-mention myLink-twitter" // mention match with Twitter service + * + * @protected + * @param {Autolinker.match.Match} match The Match instance to generate an + * anchor tag from. + * @return {String} The CSS class string for the link. Example return: + * "myLink myLink-url". If no {@link #className} was configured, returns + * an empty string. + */ + protected createCssClass(match: Match): string; + /** + * 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`. + */ + private processAnchorText; + /** + * Performs the truncation of the `anchorText` based on the {@link #truncate} + * option. If the `anchorText` is longer than the length specified by the + * {@link #truncate} option, the truncation is performed based on the + * `location` property. See {@link #truncate} for details. + * + * @private + * @param {String} anchorText The anchor tag's text (i.e. what will be + * displayed). + * @return {String} The truncated anchor text. + */ + private doTruncate; +} +export interface AnchorTagBuilderCfg { + newWindow?: boolean; + truncate?: TruncateConfigObj; + className?: string; +} diff --git a/src/modules/autolinker/anchor-tag-builder.js b/src/modules/autolinker/anchor-tag-builder.js new file mode 100644 index 00000000..b44caaab --- /dev/null +++ b/src/modules/autolinker/anchor-tag-builder.js @@ -0,0 +1,176 @@ +import { HtmlTag } from "./html-tag"; +import { truncateSmart } from "./truncate/truncate-smart"; +import { truncateMiddle } from "./truncate/truncate-middle"; +import { truncateEnd } from "./truncate/truncate-end"; +/** + * @protected + * @class Autolinker.AnchorTagBuilder + * @extends Object + * + * 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 used indirectly 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( match ) { + * var tag = match.buildTag(); // returns an {@link Autolinker.HtmlTag} instance + * tag.setAttr( 'rel', 'nofollow' ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test google.com + */ +var AnchorTagBuilder = /** @class */ (function () { + /** + * @method constructor + * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). + */ + function AnchorTagBuilder(cfg) { + if (cfg === void 0) { cfg = {}; } + /** + * @cfg {Boolean} newWindow + * @inheritdoc Autolinker#newWindow + */ + this.newWindow = false; // default value just to get the above doc comment in the ES5 output and documentation generator + /** + * @cfg {Object} truncate + * @inheritdoc Autolinker#truncate + */ + this.truncate = {}; // default value just to get the above doc comment in the ES5 output and documentation generator + /** + * @cfg {String} className + * @inheritdoc Autolinker#className + */ + this.className = ''; // default value just to get the above doc comment in the ES5 output and documentation generator + this.newWindow = cfg.newWindow || false; + this.truncate = cfg.truncate || {}; + this.className = cfg.className || ''; + } + /** + * Generates the actual anchor (<a>) tag to use in place of the + * matched text, via its `match` object. + * + * @param {Autolinker.match.Match} match The Match instance to generate an + * anchor tag from. + * @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag. + */ + AnchorTagBuilder.prototype.build = function (match) { + return new HtmlTag({ + tagName: 'a', + attrs: this.createAttrs(match), + innerHtml: this.processAnchorText(match.getAnchorText()) + }); + }; + /** + * Creates the Object (map) of the HTML attributes for the anchor (<a>) + * tag being generated. + * + * @protected + * @param {Autolinker.match.Match} match The Match instance to generate an + * anchor tag from. + * @return {Object} A key/value Object (map) of the anchor tag's attributes. + */ + AnchorTagBuilder.prototype.createAttrs = function (match) { + var attrs = { + 'href': match.getAnchorHref() // we'll always have the `href` attribute + }; + var cssClass = this.createCssClass(match); + if (cssClass) { + attrs['class'] = cssClass; + } + if (this.newWindow) { + attrs['target'] = "_blank"; + attrs['rel'] = "noopener noreferrer"; // Issue #149. See https://mathiasbynens.github.io/rel-noopener/ + } + if (this.truncate) { + if (this.truncate.length && this.truncate.length < match.getAnchorText().length) { + attrs['title'] = match.getAnchorHref(); + } + } + return attrs; + }; + /** + * Creates the CSS class that will be used for a given anchor tag, based on + * the `matchType` and the {@link #className} config. + * + * Example returns: + * + * - "" // no {@link #className} + * - "myLink myLink-url" // url match + * - "myLink myLink-email" // email match + * - "myLink myLink-phone" // phone match + * - "myLink myLink-hashtag" // hashtag match + * - "myLink myLink-mention myLink-twitter" // mention match with Twitter service + * + * @protected + * @param {Autolinker.match.Match} match The Match instance to generate an + * anchor tag from. + * @return {String} The CSS class string for the link. Example return: + * "myLink myLink-url". If no {@link #className} was configured, returns + * an empty string. + */ + AnchorTagBuilder.prototype.createCssClass = function (match) { + var className = this.className; + if (!className) { + return ""; + } + else { + var returnClasses = [className], cssClassSuffixes = match.getCssClassSuffixes(); + for (var i = 0, len = cssClassSuffixes.length; i < len; i++) { + returnClasses.push(className + '-' + cssClassSuffixes[i]); + } + return returnClasses.join(' '); + } + }; + /** + * 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`. + */ + AnchorTagBuilder.prototype.processAnchorText = function (anchorText) { + anchorText = this.doTruncate(anchorText); + return anchorText; + }; + /** + * Performs the truncation of the `anchorText` based on the {@link #truncate} + * option. If the `anchorText` is longer than the length specified by the + * {@link #truncate} option, the truncation is performed based on the + * `location` property. See {@link #truncate} for details. + * + * @private + * @param {String} anchorText The anchor tag's text (i.e. what will be + * displayed). + * @return {String} The truncated anchor text. + */ + AnchorTagBuilder.prototype.doTruncate = function (anchorText) { + var truncate = this.truncate; + if (!truncate || !truncate.length) + return anchorText; + var truncateLength = truncate.length, truncateLocation = truncate.location; + if (truncateLocation === 'smart') { + return truncateSmart(anchorText, truncateLength); + } + else if (truncateLocation === 'middle') { + return truncateMiddle(anchorText, truncateLength); + } + else { + return truncateEnd(anchorText, truncateLength); + } + }; + return AnchorTagBuilder; +}()); +export { AnchorTagBuilder }; + +//# sourceMappingURL=anchor-tag-builder.js.map diff --git a/src/modules/autolinker/anchor-tag-builder.js.map b/src/modules/autolinker/anchor-tag-builder.js.map new file mode 100644 index 00000000..45b7327f --- /dev/null +++ b/src/modules/autolinker/anchor-tag-builder.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/anchor-tag-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH;IAqBC;;;OAGG;IACH,0BAAa,GAA6B;QAA7B,oBAAA,EAAA,QAA6B;QAvB1C;;;WAGG;QACc,cAAS,GAAY,KAAK,CAAC,CAAE,gGAAgG;QAE9I;;;WAGG;QACc,aAAQ,GAAsB,EAAE,CAAC,CAAE,gGAAgG;QAEpJ;;;WAGG;QACc,cAAS,GAAW,EAAE,CAAC,CAAE,gGAAgG;QAQzI,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,IAAI,KAAK,CAAC;QACxC,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC;IACtC,CAAC;IAGD;;;;;;;OAOG;IACH,gCAAK,GAAL,UAAO,KAAY;QAClB,OAAO,IAAI,OAAO,CAAE;YACnB,OAAO,EAAK,GAAG;YACf,KAAK,EAAO,IAAI,CAAC,WAAW,CAAE,KAAK,CAAE;YACrC,SAAS,EAAG,IAAI,CAAC,iBAAiB,CAAE,KAAK,CAAC,aAAa,EAAE,CAAE;SAC3D,CAAE,CAAC;IACL,CAAC;IAGD;;;;;;;;OAQG;IACO,sCAAW,GAArB,UAAuB,KAAY;QAClC,IAAI,KAAK,GAAiC;YACzC,MAAM,EAAG,KAAK,CAAC,aAAa,EAAE,CAAE,yCAAyC;SACzE,CAAC;QAEF,IAAI,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAE,KAAK,CAAE,CAAC;QAC5C,IAAI,QAAQ,EAAG;YACd,KAAK,CAAE,OAAO,CAAE,GAAG,QAAQ,CAAC;SAC5B;QACD,IAAI,IAAI,CAAC,SAAS,EAAG;YACpB,KAAK,CAAE,QAAQ,CAAE,GAAG,QAAQ,CAAC;YAC7B,KAAK,CAAE,KAAK,CAAE,GAAG,qBAAqB,CAAC,CAAE,gEAAgE;SACzG;QAED,IAAI,IAAI,CAAC,QAAQ,EAAG;YACnB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,aAAa,EAAE,CAAC,MAAM,EAAG;gBACjF,KAAK,CAAE,OAAO,CAAE,GAAG,KAAK,CAAC,aAAa,EAAE,CAAC;aACzC;SACD;QAED,OAAO,KAAK,CAAC;IACd,CAAC;IAGD;;;;;;;;;;;;;;;;;;;OAmBG;IACO,yCAAc,GAAxB,UAA0B,KAAY;QACrC,IAAI,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAE/B,IAAI,CAAC,SAAS,EAAG;YAChB,OAAO,EAAE,CAAC;SAEV;aAAM;YACN,IAAI,aAAa,GAAG,CAAE,SAAS,CAAE,EAChC,gBAAgB,GAAG,KAAK,CAAC,mBAAmB,EAAE,CAAC;YAEhD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,gBAAgB,CAAC,MAAM,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAG;gBAC7D,aAAa,CAAC,IAAI,CAAE,SAAS,GAAG,GAAG,GAAG,gBAAgB,CAAE,CAAC,CAAE,CAAE,CAAC;aAC9D;YACD,OAAO,aAAa,CAAC,IAAI,CAAE,GAAG,CAAE,CAAC;SACjC;IACF,CAAC;IAGD;;;;;;;;OAQG;IACK,4CAAiB,GAAzB,UAA2B,UAAkB;QAC5C,UAAU,GAAG,IAAI,CAAC,UAAU,CAAE,UAAU,CAAE,CAAC;QAE3C,OAAO,UAAU,CAAC;IACnB,CAAC;IAGD;;;;;;;;;;OAUG;IACK,qCAAU,GAAlB,UAAoB,UAAkB;QACrC,IAAI,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7B,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,MAAM;YAAG,OAAO,UAAU,CAAC;QAEtD,IAAI,cAAc,GAAG,QAAQ,CAAC,MAAM,EACnC,gBAAgB,GAAG,QAAQ,CAAC,QAAQ,CAAC;QAEtC,IAAI,gBAAgB,KAAK,OAAO,EAAG;YAClC,OAAO,aAAa,CAAE,UAAU,EAAE,cAAc,CAAE,CAAC;SAEnD;aAAM,IAAI,gBAAgB,KAAK,QAAQ,EAAG;YAC1C,OAAO,cAAc,CAAE,UAAU,EAAE,cAAc,CAAE,CAAC;SAEpD;aAAM;YACN,OAAO,WAAW,CAAE,UAAU,EAAE,cAAc,CAAE,CAAC;SACjD;IACF,CAAC;IACF,uBAAC;AAAD,CApKA,AAoKC,IAAA","file":"anchor-tag-builder.js","sourcesContent":["import { Match } from \"./match/match\";\nimport { HtmlTag } from \"./html-tag\";\nimport { TruncateConfigObj } from \"./autolinker\";\nimport { truncateSmart } from \"./truncate/truncate-smart\";\nimport { truncateMiddle } from \"./truncate/truncate-middle\";\nimport { truncateEnd } from \"./truncate/truncate-end\";\n\n/**\n * @protected\n * @class Autolinker.AnchorTagBuilder\n * @extends Object\n *\n * Builds anchor (<a>) tags for the Autolinker utility when a match is\n * found.\n *\n * Normally this class is instantiated, configured, and used internally by an\n * {@link Autolinker} instance, but may actually be used indirectly in a\n * {@link Autolinker#replaceFn replaceFn} to create {@link Autolinker.HtmlTag HtmlTag}\n * instances which may be modified before returning from the\n * {@link Autolinker#replaceFn replaceFn}. For example:\n *\n * var html = Autolinker.link( \"Test google.com\", {\n * replaceFn : function( match ) {\n * var tag = match.buildTag(); // returns an {@link Autolinker.HtmlTag} instance\n * tag.setAttr( 'rel', 'nofollow' );\n *\n * return tag;\n * }\n * } );\n *\n * // generated html:\n * // Test google.com\n */\nexport class AnchorTagBuilder {\n\n\t/**\n\t * @cfg {Boolean} newWindow\n\t * @inheritdoc Autolinker#newWindow\n\t */\n\tprivate readonly newWindow: boolean = false; // default value just to get the above doc comment in the ES5 output and documentation generator\n\n\t/**\n\t * @cfg {Object} truncate\n\t * @inheritdoc Autolinker#truncate\n\t */\n\tprivate readonly truncate: TruncateConfigObj = {}; // default value just to get the above doc comment in the ES5 output and documentation generator\n\n\t/**\n\t * @cfg {String} className\n\t * @inheritdoc Autolinker#className\n\t */\n\tprivate readonly className: string = ''; // default value just to get the above doc comment in the ES5 output and documentation generator\n\n\n\t/**\n\t * @method constructor\n\t * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map).\n\t */\n\tconstructor( cfg: AnchorTagBuilderCfg = {} ) {\n\t\tthis.newWindow = cfg.newWindow || false;\n\t\tthis.truncate = cfg.truncate || {};\n\t\tthis.className = cfg.className || '';\n\t}\n\n\n\t/**\n\t * Generates the actual anchor (<a>) tag to use in place of the\n\t * matched text, via its `match` object.\n\t *\n\t * @param {Autolinker.match.Match} match The Match instance to generate an\n\t * anchor tag from.\n\t * @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag.\n\t */\n\tbuild( match: Match ) {\n\t\treturn new HtmlTag( {\n\t\t\ttagName : 'a',\n\t\t\tattrs : this.createAttrs( match ),\n\t\t\tinnerHtml : this.processAnchorText( match.getAnchorText() )\n\t\t} );\n\t}\n\n\n\t/**\n\t * Creates the Object (map) of the HTML attributes for the anchor (<a>)\n\t * tag being generated.\n\t *\n\t * @protected\n\t * @param {Autolinker.match.Match} match The Match instance to generate an\n\t * anchor tag from.\n\t * @return {Object} A key/value Object (map) of the anchor tag's attributes.\n\t */\n\tprotected createAttrs( match: Match ) {\n\t\tlet attrs: {[attrName: string]: string} = {\n\t\t\t'href' : match.getAnchorHref() // we'll always have the `href` attribute\n\t\t};\n\n\t\tlet cssClass = this.createCssClass( match );\n\t\tif( cssClass ) {\n\t\t\tattrs[ 'class' ] = cssClass;\n\t\t}\n\t\tif( this.newWindow ) {\n\t\t\tattrs[ 'target' ] = \"_blank\";\n\t\t\tattrs[ 'rel' ] = \"noopener noreferrer\"; // Issue #149. See https://mathiasbynens.github.io/rel-noopener/\n\t\t}\n\n\t\tif( this.truncate ) {\n\t\t\tif( this.truncate.length && this.truncate.length < match.getAnchorText().length ) {\n\t\t\t\tattrs[ 'title' ] = match.getAnchorHref();\n\t\t\t}\n\t\t}\n\n\t\treturn attrs;\n\t}\n\n\n\t/**\n\t * Creates the CSS class that will be used for a given anchor tag, based on\n\t * the `matchType` and the {@link #className} config.\n\t *\n\t * Example returns:\n\t *\n\t * - \"\" // no {@link #className}\n\t * - \"myLink myLink-url\" // url match\n\t * - \"myLink myLink-email\" // email match\n\t * - \"myLink myLink-phone\" // phone match\n\t * - \"myLink myLink-hashtag\" // hashtag match\n\t * - \"myLink myLink-mention myLink-twitter\" // mention match with Twitter service\n\t *\n\t * @protected\n\t * @param {Autolinker.match.Match} match The Match instance to generate an\n\t * anchor tag from.\n\t * @return {String} The CSS class string for the link. Example return:\n\t * \"myLink myLink-url\". If no {@link #className} was configured, returns\n\t * an empty string.\n\t */\n\tprotected createCssClass( match: Match ) {\n\t\tlet className = this.className;\n\n\t\tif( !className ) {\n\t\t\treturn \"\";\n\n\t\t} else {\n\t\t\tlet returnClasses = [ className ],\n\t\t\t\tcssClassSuffixes = match.getCssClassSuffixes();\n\n\t\t\tfor( let i = 0, len = cssClassSuffixes.length; i < len; i++ ) {\n\t\t\t\treturnClasses.push( className + '-' + cssClassSuffixes[ i ] );\n\t\t\t}\n\t\t\treturn returnClasses.join( ' ' );\n\t\t}\n\t}\n\n\n\t/**\n\t * Processes the `anchorText` by truncating the text according to the\n\t * {@link #truncate} config.\n\t *\n\t * @private\n\t * @param {String} anchorText The anchor tag's text (i.e. what will be\n\t * displayed).\n\t * @return {String} The processed `anchorText`.\n\t */\n\tprivate processAnchorText( anchorText: string ) {\n\t\tanchorText = this.doTruncate( anchorText );\n\n\t\treturn anchorText;\n\t}\n\n\n\t/**\n\t * Performs the truncation of the `anchorText` based on the {@link #truncate}\n\t * option. If the `anchorText` is longer than the length specified by the\n\t * {@link #truncate} option, the truncation is performed based on the\n\t * `location` property. See {@link #truncate} for details.\n\t *\n\t * @private\n\t * @param {String} anchorText The anchor tag's text (i.e. what will be\n\t * displayed).\n\t * @return {String} The truncated anchor text.\n\t */\n\tprivate doTruncate( anchorText: string ) {\n\t\tlet truncate = this.truncate;\n\t\tif( !truncate || !truncate.length ) return anchorText;\n\n\t\tlet truncateLength = truncate.length,\n\t\t\ttruncateLocation = truncate.location;\n\n\t\tif( truncateLocation === 'smart' ) {\n\t\t\treturn truncateSmart( anchorText, truncateLength );\n\n\t\t} else if( truncateLocation === 'middle' ) {\n\t\t\treturn truncateMiddle( anchorText, truncateLength );\n\n\t\t} else {\n\t\t\treturn truncateEnd( anchorText, truncateLength );\n\t\t}\n\t}\n}\n\n\nexport interface AnchorTagBuilderCfg {\n\tnewWindow?: boolean;\n\ttruncate?: TruncateConfigObj;\n\tclassName?: string\n}"]} \ No newline at end of file diff --git a/src/modules/autolinker/autolinker.d.ts b/src/modules/autolinker/autolinker.d.ts new file mode 100644 index 00000000..979a7356 --- /dev/null +++ b/src/modules/autolinker/autolinker.d.ts @@ -0,0 +1,699 @@ +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"; +/** + * @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. + */ +export default class Autolinker { + /** + * @static + * @property {String} version + * + * The Autolinker version number in the form major.minor.patch + * + * Ex: 0.25.1 + */ + static readonly version = "3.14.1"; + /** + * For backwards compatibility with Autolinker 1.x, the AnchorTagBuilder + * class is provided as a static on the Autolinker class. + */ + static readonly AnchorTagBuilder: typeof AnchorTagBuilder; + /** + * For backwards compatibility with Autolinker 1.x, the HtmlTag class is + * provided as a static on the Autolinker class. + */ + static readonly HtmlTag: typeof HtmlTag; + /** + * For backwards compatibility with Autolinker 1.x, the Matcher classes are + * provided as statics on the Autolinker class. + */ + static readonly matcher: { + Email: typeof EmailMatcher; + Hashtag: typeof HashtagMatcher; + Matcher: typeof Matcher; + Mention: typeof MentionMatcher; + Phone: typeof PhoneMatcher; + Url: typeof UrlMatcher; + }; + /** + * For backwards compatibility with Autolinker 1.x, the Match classes are + * provided as statics on the Autolinker class. + */ + static readonly match: { + Email: typeof EmailMatch; + Hashtag: typeof HashtagMatch; + Match: typeof Match; + Mention: typeof MentionMatch; + Phone: typeof PhoneMatch; + Url: typeof UrlMatch; + }; + /** + * 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. + */ + static link(textOrHtml: string, options?: AutolinkerConfig): string; + /** + * 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`. + */ + static parse(textOrHtml: string, options: AutolinkerConfig): Match[]; + /** + * The Autolinker version number exposed on the instance itself. + * + * Ex: 0.25.1 + */ + readonly version = "3.14.1"; + /** + * @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. + */ + private readonly urls; + /** + * @cfg {Boolean} [email=true] + * + * `true` if email addresses should be automatically linked, `false` if they + * should not be. + */ + private readonly email; + /** + * @cfg {Boolean} [phone=true] + * + * `true` if Phone numbers ("(555)555-5555") should be automatically linked, + * `false` if they should not be. + */ + private readonly phone; + /** + * @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. + */ + private readonly hashtag; + /** + * @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. + */ + private readonly mention; + /** + * @cfg {Boolean} [newWindow=true] + * + * `true` if the links should open in a new window, `false` otherwise. + */ + private readonly newWindow; + /** + * @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'`. + */ + private readonly stripPrefix; + /** + * @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`. + */ + private readonly stripTrailingSlash; + /** + * @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é`. + */ + private readonly decodePercentEncoding; + /** + * @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}. + */ + private readonly truncate; + /** + * @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" + */ + private readonly className; + /** + * @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. + */ + private readonly replaceFn; + /** + * @cfg {Object} context + * + * The context object (`this` reference) to call the `replaceFn` with. + * + * Defaults to this Autolinker instance. + */ + private readonly context; + /** + * @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. + */ + private readonly sanitizeHtml; + /** + * @private + * @property {Autolinker.matcher.Matcher[]} matchers + * + * The {@link Autolinker.matcher.Matcher} instances for this Autolinker + * instance. + * + * This is lazily created in {@link #getMatchers}. + */ + private matchers; + /** + * @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. + */ + private tagBuilder; + /** + * @method constructor + * @param {Object} [cfg] The configuration options for the Autolinker instance, + * specified in an Object (map). + */ + constructor(cfg?: AutolinkerConfig); + /** + * 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} + */ + private normalizeUrlsCfg; + /** + * 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} + */ + private normalizeStripPrefixCfg; + /** + * 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} + */ + private normalizeTruncateCfg; + /** + * 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`. + */ + parse(textOrHtml: string): Match[]; + /** + * After we have found all matches, we need to remove matches that overlap + * with a previous match. This can happen for instance with URLs, where the + * url 'google.com/#link' would match '#link' as a hashtag. Because the + * '#link' part is contained in a larger match that comes before the HashTag + * match, we'll remove the HashTag match. + * + * @private + * @param {Autolinker.match.Match[]} matches + * @return {Autolinker.match.Match[]} + */ + private compactMatches; + /** + * Removes matches for matchers that were turned off in the options. For + * example, if {@link #hashtag hashtags} were not to be matched, we'll + * remove them from the `matches` array here. + * + * Note: we *must* use all Matchers on the input string, and then filter + * them out later. For example, if the options were `{ url: false, hashtag: true }`, + * we wouldn't want to match the text '#link' as a HashTag inside of the text + * 'google.com/#link'. The way the algorithm works is that we match the full + * URL first (which prevents the accidental HashTag match), and then we'll + * simply throw away the URL match. + * + * @private + * @param {Autolinker.match.Match[]} matches The array of matches to remove + * the unwanted matches from. Note: this array is mutated for the + * removals. + * @return {Autolinker.match.Match[]} The mutated input `matches` array. + */ + private removeUnwantedMatches; + /** + * Parses the input `text` 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. + * + * This method processes a **non-HTML string**, and is used to parse and + * match within the text nodes of an HTML string. This method is used + * internally by {@link #parse}. + * + * @private + * @param {String} text The text to find matches within (depending on if the + * {@link #urls}, {@link #email}, {@link #phone}, + * {@link #hashtag}, and {@link #mention} options are enabled). This must be a non-HTML string. + * @param {Number} [offset=0] The offset of the text node within the + * original string. This is used when parsing with the {@link #parse} + * method to generate correct offsets within the {@link Autolinker.match.Match} + * instances, but may be omitted if calling this method publicly. + * @return {Autolinker.match.Match[]} The array of Matches found in the + * given input `text`. + */ + private parseText; + /** + * Automatically links URLs, Email addresses, Phone numbers, Hashtags, + * and Mentions (Twitter, Instagram, Soundcloud) found in the given chunk of HTML. Does not link + * URLs found within HTML tags. + * + * For instance, if given the text: `You should go to http://www.yahoo.com`, + * then the result will be `You should go to + * <a href="http://www.yahoo.com">http://www.yahoo.com</a>` + * + * This method finds the text around any HTML elements in the input + * `textOrHtml`, which will be the text that is processed. Any original HTML + * elements will be left as-is, as well as the text that is already wrapped + * in anchor (<a>) tags. + * + * @param {String} textOrHtml The HTML or text to autolink matches within + * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, {@link #hashtag}, and {@link #mention} options are enabled). + * @return {String} The HTML, with matches automatically linked. + */ + link(textOrHtml: string): string; + /** + * Creates the return string value for a given match in the input string. + * + * This method handles the {@link #replaceFn}, if one was provided. + * + * @private + * @param {Autolinker.match.Match} match The Match object that represents + * the match. + * @return {String} The string that the `match` should be replaced with. + * This is usually the anchor tag string, but may be the `matchStr` itself + * if the match is not to be replaced. + */ + private createMatchReturnVal; + /** + * Lazily instantiates and returns the {@link Autolinker.matcher.Matcher} + * instances for this Autolinker instance. + * + * @private + * @return {Autolinker.matcher.Matcher[]} + */ + private getMatchers; + /** + * Returns the {@link #tagBuilder} instance for this Autolinker instance, + * lazily instantiating it if it does not yet exist. + * + * @private + * @return {Autolinker.AnchorTagBuilder} + */ + private getTagBuilder; +} +export interface AutolinkerConfig { + urls?: UrlsConfig; + email?: boolean; + phone?: boolean; + hashtag?: HashtagConfig; + mention?: MentionConfig; + newWindow?: boolean; + stripPrefix?: StripPrefixConfig; + stripTrailingSlash?: boolean; + truncate?: TruncateConfig; + className?: string; + replaceFn?: ReplaceFn | null; + context?: any; + sanitizeHtml?: boolean; + decodePercentEncoding?: boolean; +} +export declare type UrlsConfig = boolean | UrlsConfigObj; +export interface UrlsConfigObj { + schemeMatches?: boolean; + wwwMatches?: boolean; + tldMatches?: boolean; +} +export declare type UrlMatchTypeOptions = 'scheme' | 'www' | 'tld'; +export declare type StripPrefixConfig = boolean | StripPrefixConfigObj; +export interface StripPrefixConfigObj { + scheme?: boolean; + www?: boolean; +} +export declare type TruncateConfig = number | TruncateConfigObj; +export interface TruncateConfigObj { + length?: number; + location?: "end" | "middle" | "smart"; +} +export declare type HashtagConfig = false | HashtagServices; +export declare type HashtagServices = 'twitter' | 'facebook' | 'instagram'; +export declare type MentionConfig = false | MentionServices; +export declare type MentionServices = 'mastodon' | 'twitter' | 'instagram' | 'soundcloud'; +export declare type ReplaceFn = (match: Match) => ReplaceFnReturn; +export declare type ReplaceFnReturn = boolean | string | HtmlTag | null | undefined | void; diff --git a/src/modules/autolinker/autolinker.js b/src/modules/autolinker/autolinker.js new file mode 100644 index 00000000..86ceb9f3 --- /dev/null +++ b/src/modules/autolinker/autolinker.js @@ -0,0 +1,907 @@ +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 ,