diff --git a/packages/linkify-element/src/linkify-element.js b/packages/linkify-element/src/linkify-element.js index c4d13edb..76375361 100644 --- a/packages/linkify-element/src/linkify-element.js +++ b/packages/linkify-element/src/linkify-element.js @@ -7,7 +7,7 @@ const HTML_NODE = 1, TXT_NODE = 3; /** * @param {HTMLElement} parent - * @param {Text | HTMLElement} oldChild + * @param {Text | HTMLElement | ChildNode} oldChild * @param {Array} newChildren */ function replaceChildWithChildren(parent, oldChild, newChildren) { @@ -20,21 +20,21 @@ function replaceChildWithChildren(parent, oldChild, newChildren) { } /** - * @param {MultiToken[]} tokens - * @param {Object} opts - * @param {Document} doc A + * @param {import('linkifyjs').MultiToken[]} tokens + * @param {import('linkifyjs').Options} options + * @param {Document} doc Document implementation * @returns {Array} */ -function tokensToNodes(tokens, opts, doc) { +function tokensToNodes(tokens, options, doc) { const result = []; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; - if (token.t === 'nl' && opts.get('nl2br')) { + if (token.t === 'nl' && options.get('nl2br')) { result.push(doc.createElement('br')); - } else if (!token.isLink || !opts.check(token)) { + } else if (!token.isLink || !options.check(token)) { result.push(doc.createTextNode(token.toString())); } else { - result.push(opts.render(token)); + result.push(options.render(token)); } } @@ -43,12 +43,12 @@ function tokensToNodes(tokens, opts, doc) { /** * Requires document.createElement - * @param {HTMLElement} element - * @param {Options} opts + * @param {HTMLElement | ChildNode} element + * @param {import('linkifyjs').Options} options * @param {Document} doc * @returns {HTMLElement} */ -function linkifyElementHelper(element, opts, doc) { +function linkifyElementHelper(element, options, doc) { // Can the element be linkified? if (!element || element.nodeType !== HTML_NODE) { @@ -56,7 +56,7 @@ function linkifyElementHelper(element, opts, doc) { } // Is this element already a link? - if (element.tagName === 'A' || opts.ignoreTags.indexOf(element.tagName) >= 0) { + if (element.tagName === 'A' || options.ignoreTags.indexOf(element.tagName) >= 0) { // No need to linkify return element; } @@ -68,7 +68,7 @@ function linkifyElementHelper(element, opts, doc) { switch (childElement.nodeType) { case HTML_NODE: - linkifyElementHelper(childElement, opts, doc); + linkifyElementHelper(childElement, options, doc); break; case TXT_NODE: { str = childElement.nodeValue; @@ -79,7 +79,7 @@ function linkifyElementHelper(element, opts, doc) { break; } - nodes = tokensToNodes(tokens, opts, doc); + nodes = tokensToNodes(tokens, options, doc); // Swap out the current child for the set of nodes replaceChildWithChildren(element, childElement, nodes); @@ -123,11 +123,11 @@ function getDefaultRender(doc) { * convert them to anchor tags. * * @param {HTMLElement} element A DOM node to linkify - * @param {Object} opts linkify options + * @param {import('linkifyjs').Opts} [opts] linkify options * @param {Document} [doc] (optional) window.document implementation, if differs from global * @returns {HTMLElement} */ -export default function linkifyElement(element, opts, doc = null) { +export default function linkifyElement(element, opts = null, doc = null) { try { doc = doc || document || window && window.document || global && global.document; } catch (e) { /* do nothing for now */ } @@ -140,12 +140,18 @@ export default function linkifyElement(element, opts, doc = null) { ); } - opts = new Options(opts, getDefaultRender(doc)); - return linkifyElementHelper(element, opts, doc); + const options = new Options(opts, getDefaultRender(doc)); + return linkifyElementHelper(element, options, doc); } -// Maintain reference to the recursive helper to cache option-normalization +// Maintain reference to the recursive helper and option-normalization for use +// in linkify-jquery linkifyElement.helper = linkifyElementHelper; linkifyElement.getDefaultRender = getDefaultRender; + +/** + * @param {import('linkifyjs').Opts | import('linkifyjs').Options} opts + * @param {Document} doc + */ linkifyElement.normalize = (opts, doc) => new Options(opts, getDefaultRender(doc)); diff --git a/packages/linkify-element/tsconfig.json b/packages/linkify-element/tsconfig.json index 06220e5c..d73fe939 100644 --- a/packages/linkify-element/tsconfig.json +++ b/packages/linkify-element/tsconfig.json @@ -5,7 +5,7 @@ "allowJs": true, "declaration": true, "emitDeclarationOnly": true, - "maxNodeModuleJsDepth": 1, + "maxNodeModuleJsDepth": 2, "outDir": "." } } diff --git a/packages/linkify-html/src/linkify-html.js b/packages/linkify-html/src/linkify-html.js index 9c2e1905..9ce7722c 100644 --- a/packages/linkify-html/src/linkify-html.js +++ b/packages/linkify-html/src/linkify-html.js @@ -11,7 +11,7 @@ const Doctype = 'Doctype'; /** * @param {string} str html string to link - * @param {object} [opts] linkify options + * @param {import('linkifyjs').Opts} [opts] linkify options * @returns {string} resulting string */ export default function linkifyHtml(str, opts = {}) { @@ -21,7 +21,7 @@ export default function linkifyHtml(str, opts = {}) { const linkifiedTokens = []; const linkified = []; - opts = new Options(opts, defaultRender); + const options = new Options(opts, defaultRender); // Linkify the tokens given by the parser for (let i = 0; i < tokens.length; i++) { @@ -32,7 +32,7 @@ export default function linkifyHtml(str, opts = {}) { // Ignore all the contents of ignored tags const tagName = token.tagName.toUpperCase(); - const isIgnored = tagName === 'A' || opts.ignoreTags.indexOf(tagName) >= 0; + const isIgnored = tagName === 'A' || options.ignoreTags.indexOf(tagName) >= 0; if (!isIgnored) { continue; } let preskipLen = linkifiedTokens.length; @@ -43,7 +43,7 @@ export default function linkifyHtml(str, opts = {}) { linkifiedTokens.push(token); } else { // Valid text token, linkify it! - const linkifedChars = linkifyChars(token.chars, opts); + const linkifedChars = linkifyChars(token.chars, options); linkifiedTokens.push.apply(linkifiedTokens, linkifedChars); } } @@ -91,27 +91,29 @@ export default function linkifyHtml(str, opts = {}) { /** `tokens` and `token` in this section referes to tokens returned by `linkify.tokenize`. `linkified` will contain HTML Parser-style tokens + @param {string} + @param {import('linkifyjs').Options} */ -function linkifyChars(str, opts) { +function linkifyChars(str, options) { const tokens = linkify.tokenize(str); const result = []; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; - if (token.t === 'nl' && opts.nl2br) { + if (token.t === 'nl' && options.nl2br) { result.push({ type: StartTag, tagName: 'br', attributes: [], selfClosing: true }); - } else if (!token.isLink || !opts.check(token)) { + } else if (!token.isLink || !options.check(token)) { result.push({ type: Chars, chars: token.toString() }); } else { result.push({ type: LinkifyResult, - rendered: opts.render(token) + rendered: options.render(token) }); } } diff --git a/packages/linkify-jquery/src/linkify-jquery.js b/packages/linkify-jquery/src/linkify-jquery.js index 08964e58..b9f2e0ec 100644 --- a/packages/linkify-jquery/src/linkify-jquery.js +++ b/packages/linkify-jquery/src/linkify-jquery.js @@ -29,9 +29,9 @@ export default function apply($, doc = false) { } function jqLinkify(opts) { - opts = linkifyElement.normalize(opts, doc); + const options = linkifyElement.normalize(opts, doc); return this.each(function () { - linkifyElement.helper(this, opts, doc); + linkifyElement.helper(this, options, doc); }); } @@ -44,58 +44,56 @@ export default function apply($, doc = false) { const target = data.linkify; const nl2br = data.linkifyNl2br; - let options = { + const opts = { nl2br: !!nl2br && nl2br !== 0 && nl2br !== 'false' }; if ('linkifyAttributes' in data) { - options.attributes = data.linkifyAttributes; + opts.attributes = data.linkifyAttributes; } if ('linkifyDefaultProtocol' in data) { - options.defaultProtocol = data.linkifyDefaultProtocol; + opts.defaultProtocol = data.linkifyDefaultProtocol; } if ('linkifyEvents' in data) { - options.events = data.linkifyEvents; + opts.events = data.linkifyEvents; } if ('linkifyFormat' in data) { - options.format = data.linkifyFormat; + opts.format = data.linkifyFormat; } if ('linkifyFormatHref' in data) { - options.formatHref = data.linkifyFormatHref; + opts.formatHref = data.linkifyFormatHref; } if ('linkifyTagname' in data) { - options.tagName = data.linkifyTagname; + opts.tagName = data.linkifyTagname; } if ('linkifyTarget' in data) { - options.target = data.linkifyTarget; + opts.target = data.linkifyTarget; } if ('linkifyRel' in data) { - options.rel = data.linkifyRel; + opts.rel = data.linkifyRel; } if ('linkifyValidate' in data) { - options.validate = data.linkifyValidate; + opts.validate = data.linkifyValidate; } if ('linkifyIgnoreTags' in data) { - options.ignoreTags = data.linkifyIgnoreTags; + opts.ignoreTags = data.linkifyIgnoreTags; } if ('linkifyClassName' in data) { - options.className = data.linkifyClassName; + opts.className = data.linkifyClassName; } - options = linkifyElement.normalize(options); - const $target = target === 'this' ? $this : $this.find(target); - $target.linkify(options); + $target.linkify(opts); }); }); } diff --git a/packages/linkify-react/src/linkify-react.js b/packages/linkify-react/src/linkify-react.js index 86af2ca3..f88be30b 100644 --- a/packages/linkify-react/src/linkify-react.js +++ b/packages/linkify-react/src/linkify-react.js @@ -80,7 +80,7 @@ function linkifyReactElement(element, opts, elementId = 0) { /** * @template P * @template {string | React.JSXElementConstructor

} T - * @param {P & { as?: T, tagName?: T, tagName?: T, options?: any, children?: React.ReactNode}} props + * @param {P & { as?: T, tagName?: T, tagName?: T, options?: import('linkifyjs').Opts, children?: React.ReactNode}} props * @returns {React.ReactElement} */ const Linkify = (props) => { diff --git a/packages/linkify-string/src/linkify-string.js b/packages/linkify-string/src/linkify-string.js index e250b946..87c3eb0d 100644 --- a/packages/linkify-string/src/linkify-string.js +++ b/packages/linkify-string/src/linkify-string.js @@ -33,7 +33,7 @@ function defaultRender({ tagName, attributes, content }) { * interface if you need to parse HTML entities. * * @param {string} str string to linkify - * @param {object} [opts] overridable options + * @param {import('linkifyjs').Opts} [opts] overridable options * @returns {string} */ function linkifyStr(str, opts = {}) { diff --git a/packages/linkifyjs/src/assign.js b/packages/linkifyjs/src/assign.js index bd12c900..32813af0 100644 --- a/packages/linkifyjs/src/assign.js +++ b/packages/linkifyjs/src/assign.js @@ -1,7 +1,15 @@ -const assign = Object.assign || ((target, properties) => { +/** + * @template A + * @template B + * @param {A} target + * @param {B} properties + * @return {A & B} + */ +const assign = (target, properties) => { for (const key in properties) { target[key] = properties[key]; } -}); + return target; +}; export default assign; diff --git a/packages/linkifyjs/src/fsm.js b/packages/linkifyjs/src/fsm.js index ef76a193..83070a8f 100644 --- a/packages/linkifyjs/src/fsm.js +++ b/packages/linkifyjs/src/fsm.js @@ -8,32 +8,28 @@ import assign from './assign'; * jr is the list of regex-match transitions, jd is the default state to * transition to t is the accepting token type, if any. If this is the terminal * state, then it does not emit a token. - * @param {string|class} token to emit + * @template T + * @property {{ [string]: State }} j + * @property {[RegExp, State][]} jr + * @property {State} jd + * @property {?T} t */ -export function State(token) { - // this.n = null; // DEBUG: State name - this.j = {}; // IMPLEMENTATION 1 - // this.j = []; // IMPLEMENTATION 2 - this.jr = []; - this.jd = null; - this.t = token; -} - -/** - * Take the transition from this state to the next one on the given input. - * If this state does not exist deterministically, will create it. - * - * @param {string} input character or token to transition on - * @param {string|class} [token] token or multi-token to emit when reaching - * this state - */ -State.prototype = { +export class State { /** - * @param {State} state + * @param {T} [token] to emit */ + constructor(token) { + // this.n = null; // DEBUG: State name + this.j = {}; // IMPLEMENTATION 1 + // this.j = []; // IMPLEMENTATION 2 + this.jr = []; + this.jd = null; + this.t = token; + } + accepts() { return !!this.t; - }, + } /** * Short for "take transition", this is a method for building/working with @@ -51,9 +47,9 @@ State.prototype = { * transitioned to on the given input regardless of what that input * previously did. * - * @param {string} input character or token type to transition on - * @param {Token|State} tokenOrState transition to a matching state - * @returns State taken after the given input + * @param {string | string[]} input character or token type to transition on + * @param {State | T} tokenOrState transition to a matching state + * @returns {State} taken after the given input */ tt(input, tokenOrState) { if (input instanceof Array) { @@ -66,7 +62,7 @@ State.prototype = { return nextState; } - if (tokenOrState && tokenOrState.j) { + if (tokenOrState && tokenOrState instanceof State) { // State, default a basic transition this.j[input] = tokenOrState; return tokenOrState; @@ -98,7 +94,7 @@ State.prototype = { this.j[input] = nextState; return nextState; } -}; +} /** * Utility function to create state without using new keyword (reduced file size @@ -112,7 +108,8 @@ export const makeState = (/*name*/) => { /** * Similar to previous except it is an accepting state that emits a token - * @param {Token} token + * @template T + * @param {T} token */ export const makeAcceptingState = (token/*, name*/) => { const s = new State(token); @@ -122,9 +119,11 @@ export const makeAcceptingState = (token/*, name*/) => { /** * Create a transition from startState to nextState via the given character + * @template T * @param {State} startState transition from thie starting state - * @param {Token} input via this input character or other concrete token type + * @param {string} input via this input character or other concrete token type * @param {State} nextState to this next state + * @return {State} */ export const makeT = (startState, input, nextState) => { // IMPLEMENTATION 1: Add to object (fast) @@ -136,7 +135,6 @@ export const makeT = (startState, input, nextState) => { }; /** - * * @param {State} startState stransition from this starting state * @param {RegExp} regex Regular expression to match on input * @param {State} nextState transition to this next state if there's are regex match @@ -148,7 +146,7 @@ export const makeRegexT = (startState, regex, nextState) => { /** * Follow the transition from the given character to the next state * @param {State} state - * @param {string|Token} input character or other concrete token type to transition + * @param {string} input character or other concrete token type to transition * @returns {?State} the next state, if any */ export const takeT = (state, input) => { @@ -177,7 +175,7 @@ export const takeT = (state, input) => { * Similar to makeT, but takes a list of characters that all transition to the * same nextState startState * @param {State} startState - * @param {Array} chars + * @param {string[]} chars * @param {State} nextState */ export const makeMultiT = (startState, chars, nextState) => { @@ -191,7 +189,7 @@ export const makeMultiT = (startState, chars, nextState) => { * tuples, where the first element is the transitions character and the second * is the state to transition to * @param {State} startState - * @param {Array} transitions + * @param {[string, State][]} transitions */ export const makeBatchT = (startState, transitions) => { for (let i = 0; i < transitions.length; i++) { @@ -211,11 +209,11 @@ export const makeBatchT = (startState, transitions) => { * * This turns the state machine into a Trie-like data structure (rather than a * intelligently-designed DFA). - * @param {State} state + * @param {State} state * @param {string} str - * @param {Token} endStateFactory - * @param {Token} defaultStateFactory - * @return {State} the final state + * @param {State} endState + * @param {() => State} defaultStateFactory + * @return {State} the final state */ export const makeChainT = (state, str, endState, defaultStateFactory) => { let i = 0, len = str.length, nextState; diff --git a/packages/linkifyjs/src/linkify.js b/packages/linkifyjs/src/linkify.js index b1d87177..e26175c4 100644 --- a/packages/linkifyjs/src/linkify.js +++ b/packages/linkifyjs/src/linkify.js @@ -85,31 +85,30 @@ export function init() { } /** - Parse a string into tokens that represent linkable and non-linkable sub-components - @param {string} str - @return {MultiToken[]} tokens -*/ + * Parse a string into tokens that represent linkable and non-linkable sub-components + * @param {string} str + * @return {MultiToken[]} tokens + */ export function tokenize(str) { if (!INIT.initialized) { init(); } return parser.run(INIT.parser.start, str, scanner.run(INIT.scanner.start, str)); } /** - Find a list of linkable items in the given string. - @param {string} str string to find links in - @param {string} [type] (optional) only find links of a specific type, e.g., - 'url' or 'email' - @param {Options|Object} [options] (optional) formatting options for final output + * Find a list of linkable items in the given string. + * @param {string} str string to find links in + * @param {string} [type] only find links of a specific type, e.g., 'url' or 'email' + * @param {Opts} [opts] formatting options for final output */ -export function find(str, type = null, options = {}) { - const opts = new Options(options); +export function find(str, type = null, opts = null) { + const options = new Options(opts); const tokens = tokenize(str); const filtered = []; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token.isLink && (!type || token.t === type)) { - filtered.push(token.toFormattedObject(opts)); + filtered.push(token.toFormattedObject(options)); } } @@ -140,4 +139,5 @@ export function test(str, type = null) { } export * as options from './options'; +export { MultiToken } from './multi'; export { Options }; diff --git a/packages/linkifyjs/src/multi.js b/packages/linkifyjs/src/multi.js index 785f48f5..c0de5b3e 100644 --- a/packages/linkifyjs/src/multi.js +++ b/packages/linkifyjs/src/multi.js @@ -7,49 +7,35 @@ import assign from './assign'; Tokens composed of arrays of TextTokens ******************************************************************************/ -function inherits(parent, child, props={}) { - const extended = Object.create(parent.prototype); - for (const p in props) { - extended[p] = props[p]; - } - extended.constructor = child; - child.prototype = extended; - return child; -} /** - Abstract class used for manufacturing tokens of text tokens. That is rather - than the value for a token being a small string of text, it's value an array - of text tokens. - - Used for grouping together URLs, emails, hashtags, and other potential - creations. - - @class MultiToken - @param {string} value - @param {{t: string, v: string, s: number, e: number}[]} tokens - @abstract -*/ -export function MultiToken() {} + * @param {string} value + * @param {Token[]} tokens + */ +export function MultiToken(value, tokens) { + this.t = 'token'; + this.v = value; + this.tk = tokens; +} +/** + * Abstract class used for manufacturing tokens of text tokens. That is rather + * than the value for a token being a small string of text, it's value an array + * of text tokens. + * + * Used for grouping together URLs, emails, hashtags, and other potential + * creations. + * @class MultiToken + * @property {string} t + * @property {string} v + * @property {Token[]} tk + * @abstract + */ MultiToken.prototype = { - /** - * String representing the type for this token - * @property t - * @default 'token' - */ - t: 'token', - - /** - * Is this multitoken a link? - * @property isLink - * @default false - */ isLink: false, /** * Return the string this token represents. - * @param {Options | string} [_opts] Formatting options * @return {string} */ toString() { @@ -59,21 +45,22 @@ MultiToken.prototype = { /** * What should the value for this token be in the `href` HTML attribute? * Returns the `.toString` value by default. - * @param {Options | string} [_opts] Formatting options + * @param {string} [scheme] * @return {string} */ - toHref() { + toHref(scheme) { + !!scheme; return this.toString(); }, /** - * @param {Options} opts Formatting options + * @param {Options} options Formatting options * @returns {string} */ - toFormattedString(opts) { + toFormattedString(options) { const val = this.toString(); - const truncate = opts.get('truncate', val, this); - const formatted = opts.get('format', val, this); + const truncate = options.get('truncate', val, this); + const formatted = options.get('format', val, this); return (truncate && formatted.length > truncate) ? formatted.substring(0, truncate) + '…' : formatted; @@ -81,11 +68,11 @@ MultiToken.prototype = { /** * - * @param {Options} [opts] + * @param {Options} options * @returns {string} */ - toFormattedHref(opts) { - return opts.get('formatHref', this.toHref(opts.get('defaultProtocol')), this); + toFormattedHref(options) { + return options.get('formatHref', this.toHref(options.get('defaultProtocol')), this); }, /** @@ -128,14 +115,14 @@ MultiToken.prototype = { /** * - * @param {Options} opts Formatting option + * @param {Options} options Formatting option */ - toFormattedObject(opts) { + toFormattedObject(options) { return { type: this.t, - value: this.toFormattedString(opts), + value: this.toFormattedString(options), isLink: this.isLink, - href: this.toFormattedHref(opts), + href: this.toFormattedHref(options), start: this.startIndex(), end: this.endIndex() }; @@ -143,29 +130,29 @@ MultiToken.prototype = { /** * Whether this token should be rendered as a link according to the given options - * @param {Options} opts + * @param {Options} options * @returns {boolean} */ - validate(opts) { - return opts.get('validate', this.toString(), this); + validate(options) { + return options.get('validate', this.toString(), this); }, /** * Return an object that represents how this link should be rendered. - * @param {Options} opts Formattinng options + * @param {Options} options Formattinng options */ - render(opts) { + render(options) { const token = this; - const tagName = opts.get('tagName', href, token); - const href = this.toFormattedHref(opts); - const content = this.toFormattedString(opts); + const href = this.toFormattedHref(options); + const tagName = options.get('tagName', href, token); + const content = this.toFormattedString(options); const attributes = {}; - const className = opts.get('className', href, token); - const target = opts.get('target', href, token); - const rel = opts.get('rel', href, token); - const attrs = opts.getObj('attributes', href, token); - const eventListeners = opts.getObj('events', href, token); + const className = options.get('className', href, token); + const target = options.get('target', href, token); + const rel = options.get('rel', href, token); + const attrs = options.getObj('attributes', href, token); + const eventListeners = options.getObj('events', href, token); attributes.href = href; if (className) { attributes.class = className; } @@ -184,22 +171,23 @@ export { MultiToken as Base }; * Create a new token that can be emitted by the parser state machine * @param {string} type readable type of the token * @param {object} props properties to assign or override, including isLink = true or false - * @returns {(value: string, tokens: {t: string, v: string, s: number, e: number}) => MultiToken} new token class + * @returns {new (value: string, tokens: Token[]) => MultiToken} new token class */ export function createTokenClass(type, props) { - function Token(value, tokens) { - this.t = type; - this.v = value; - this.tk = tokens; + class Token extends MultiToken { + constructor(value, tokens) { + super(value, tokens); + this.t = type; + } + } + for (const p in props) { + Token.prototype[p] = props[p]; } - inherits(MultiToken, Token, props); return Token; } /** Represents a list of tokens making up a valid email address - @class Email - @extends MultiToken */ export const Email = createTokenClass('email', { isLink: true, @@ -210,22 +198,18 @@ export const Email = createTokenClass('email', { /** Represents some plain text - @class Text - @extends MultiToken */ export const Text = createTokenClass('text'); /** Multi-linebreak token - represents a line break @class Nl - @extends MultiToken */ export const Nl = createTokenClass('nl'); /** Represents a list of text tokens making up a valid URL @class Url - @extends MultiToken */ export const Url = createTokenClass('url', { isLink: true, diff --git a/packages/linkifyjs/src/options.js b/packages/linkifyjs/src/options.js index 1350d454..a3da7602 100644 --- a/packages/linkifyjs/src/options.js +++ b/packages/linkifyjs/src/options.js @@ -1,8 +1,85 @@ import assign from './assign'; /** - * @property {string} defaultProtocol - * @property {{[string]: (event) => void}]} [events] + * An object where each key is a valid DOM Event Name such as `click` or `focus` + * and each value is an event handler function. + * + * https://developer.mozilla.org/en-US/docs/Web/API/Element#events + * @typedef {?{ [event: string]: Function }} EventListeners + */ + +/** + * All formatted properties required to render a link, including `tagName`, + * `attributes`, `content` and `events`. + * @typedef {{ tagName: any, attributes: {[attr: string]: any}, content: string, + * events: EventListeners }} IntermediateRepresentation + */ + +/** + * Specify either an object described by the template type `O` or a function. + * + * The function takes a string value (usually the link's href attribute), the + * link type (`'url'`, `'hashtag`', etc.) and an internal token representation + * of the link. It should return an object of the template type `O` + * @template O + * @typedef {O | ((value: string, type: string, token: MultiToken) => O)} OptObj + */ + +/** + * Specify either a function described by template type `F` or an object. + * + * Each key in the object should be a link type (`'url'`, `'hashtag`', etc.). Each + * value should be a function with template type `F` that is called when the + * corresponding link type is encountered. + * @template F + * @typedef {F | { [type: string]: F}} OptFn + */ + +/** + * Specify either a value with template type `V`, a function that returns `V` or + * an object where each value resolves to `V`. + * + * The function takes a string value (usually the link's href attribute), the + * link type (`'url'`, `'hashtag`', etc.) and an internal token representation + * of the link. It should return an object of the template type `V` + * + * For the object, each key should be a link type (`'url'`, `'hashtag`', etc.). + * Each value should either have type `V` or a function that returns V. This + * function similarly takes a string value and a token. + * + * Example valid types for `Opt`: + * + * ```js + * 'hello' + * (value, type, token) => 'world' + * { url: 'hello', email: (value, token) => 'world'} + * ``` + * @template V + * @typedef {OptObj | { [type: string]: V | ((value: string, token: MultiToken) => V) }} Opt + */ + +/** + * See available options: https://linkify.js.org/docs/options.html + * @typedef {{ + * defaultProtocol?: string, + * events?: null | OptObj, + * format?: Opt, + * formatHref?: Opt, + * nl2br?: boolean, + * tagName?: ?Opt, + * target?: ?Opt, + * rel?: ?Opt, + * validate?: Opt, + * truncate?: Opt, + * className?: Opt, + * attributes?: OptObj<{[attr: string]: any}>, + * ignoreTags?: string[], + * render?: OptFn<(ir: IntermediateRepresentation) => any> + * }} Opts + */ + +/** + * @type Required */ export const defaults = { defaultProtocol: 'http', @@ -22,24 +99,19 @@ export const defaults = { }; /** - * @typedef {?{ [event: string]: Function }} LinkifyEventListeners - */ - -/** - * @typedef {{ tagName: any, attributes: any, content: string, events: LinkifyEventListeners }} LinkifyIntermediateRepresentation - */ - -/** - * @class Options - * @param {Object | Options} [opts] Set option properties besides the defaults - * @param {(ir: LinkifyIntermediateRepresentation) => any} [defaultRender] (For internal use) default - * render function that determines how to generate an HTML element based on a - * link token's derived tagName, attributes and HTML. Similar to render option + * Utility class for linkify interfaces to apply specified + * {@link Opts formatting and rendering options}. + * + * @param {Opts | Options} [opts] Option value overrides. + * @param {(ir: IntermediateRepresentation) => any} [defaultRender] (For + * internal use) default render function that determines how to generate an + * HTML element based on a link token's derived tagName, attributes and HTML. + * Similar to render option */ export function Options(opts, defaultRender = null) { - const o = {}; - assign(o, defaults); - if (opts) { assign(o, opts instanceof Options ? opts.o : opts); } + + let o = assign({}, defaults); + if (opts) { o = assign(o, opts instanceof Options ? opts.o : opts); } // Ensure all ignored tags are uppercase const ignoredTags = o.ignoreTags; @@ -47,18 +119,22 @@ export function Options(opts, defaultRender = null) { for (let i = 0; i < ignoredTags.length; i++) { uppercaseIgnoredTags.push(ignoredTags[i].toUpperCase()); } + /** @protected */ this.o = o; - this.defaultRender = defaultRender; + if (defaultRender) { this.defaultRender = defaultRender; } this.ignoreTags = uppercaseIgnoredTags; } Options.prototype = { - o: {}, + o: defaults, /** - * @property {(ir: LinkifyIntermediateRepresentation) => any} [defaultRender] + * @param {IntermediateRepresentation} ir + * @returns {any} */ - defaultRender: null, + defaultRender(ir) { + return ir; + }, /** * Returns true or false based on whether a token should be displayed as a @@ -76,11 +152,12 @@ Options.prototype = { * Resolve an option's value based on the value of the option and the given * params. If operator and token are specified and the target option is * callable, automatically calls the function with the given argument. - * @param {string} key Name of option to use - * @param {any} [operator] will be passed to the target option if it's a + * @template {keyof Opts} K + * @param {K} key Name of option to use + * @param {string} [operator] will be passed to the target option if it's a * function. If not specified, RAW function value gets returned * @param {MultiToken} [token] The token from linkify.tokenize - * @returns {any} Resolved option value + * @returns {Opts[K] | any} */ get(key, operator, token) { const isCallable = operator != null; @@ -98,6 +175,13 @@ Options.prototype = { return option; }, + /** + * @template {keyof Opts} L + * @param {L} key Name of options object to use + * @param {string} [operator] + * @param {MultiToken} [token] + * @returns {Opts[L] | any} + */ getObj(key, operator, token) { let obj = this.o[key]; if (typeof obj === 'function' && operator != null) { @@ -116,7 +200,7 @@ Options.prototype = { render(token) { const ir = token.render(this); // intermediate representation const renderFn = this.get('render', null, token) || this.defaultRender; - return renderFn ? renderFn(ir, token.t, token) : ir; + return renderFn(ir, token.t, token); } }; diff --git a/packages/linkifyjs/src/parser.js b/packages/linkifyjs/src/parser.js index 7bd5efae..ae9eb8cf 100644 --- a/packages/linkifyjs/src/parser.js +++ b/packages/linkifyjs/src/parser.js @@ -315,7 +315,7 @@ export function init() { * * @param {State} start parser start state * @param {string} input the original input used to generate the given tokens - * @param {{t: string, v: string, s: number, e: number}[]} tokens list of scanned tokens + * @param {Token[]} tokens list of scanned tokens * @returns {MultiToken[]} */ export function run(start, input, tokens) { @@ -399,9 +399,9 @@ export { mtk as tokens }; /** * Utility function for instantiating a new multitoken with all the relevant * fields during parsing. - * @param {Class} Multi class to instantiate + * @param {new (value: string, tokens: Token[]) => MultiToken} Multi class to instantiate * @param {string} input original input string - * @param {{t: string, v: string, s: number, e: number}[]} tokens consecutive tokens scanned from input string + * @param {Token[]} tokens consecutive tokens scanned from input string * @returns {MultiToken} */ function parserCreateMultiToken(Multi, input, tokens) { diff --git a/packages/linkifyjs/src/scanner.js b/packages/linkifyjs/src/scanner.js index 8b55c713..74e23aa7 100644 --- a/packages/linkifyjs/src/scanner.js +++ b/packages/linkifyjs/src/scanner.js @@ -206,7 +206,7 @@ export function init(customSchemes = []) { @method run @param {State} start scanner starting state @param {string} str input string to scan - @return {{t: string, v: string, s: number, l: number}[]} list of tokens, each with a type and value + @return {Token[]} list of tokens, each with a type and value */ export function run(start, str) { // State machine is not case sensitive, so input is tokenized in lowercased diff --git a/packages/linkifyjs/src/text.js b/packages/linkifyjs/src/text.js index a6aa958b..5965e495 100644 --- a/packages/linkifyjs/src/text.js +++ b/packages/linkifyjs/src/text.js @@ -100,4 +100,51 @@ export const alphanumeric = words.concat(NUM); export const domain = words.concat(COMPOUND_SCHEME, COMPOUND_SLASH_SCHEME, NUM, EMOJIS); export const scheme = [SCHEME, SLASH_SCHEME, COMPOUND_SCHEME, COMPOUND_SLASH_SCHEME]; -export const collections = { ascii, asciinumeric, words, alphanumeric, domain, scheme }; +// Define each property separately to let typescript know that this object is +// open for adding more collections. +export const collections = {}; +collections.ascii = ascii; +collections.asciinumeric = asciinumeric; +collections.words = words; +collections.alphanumeric = alphanumeric; +collections.domain = domain; +collections.scheme = scheme; + +/** + * @param {string} name Name of text token collections. Will be available in plugins as scanner.tokens.collections. + * @returns {string[]} the collection + */ +export function registerTextTokenCollection(name) { + if (!(name in collections)) { + collections[name] = []; + } + return collections[name]; +} + +export function collectionsWithToken(name) { + let collectionNames = []; + for (let col in collections) { + if (collections[col].indexOf(name) >= 0) { + collectionNames.push(col); + } + } + return collectionNames; +} + +/** + * Register a text token that the parser can recognize + * @param {string} name Token name in all caps (by convention) + * @param {string[]} collectionNames List of collections into which to add this token. Any previously-unknown collection will be created. + * @returns {string} + */ +export function registerTextToken(name, collectionNames = []) { + for (let i = 0; i < collectionNames.length; i++) { + const collection = registerTextTokenCollection(collectionNames[i]); + collection.push(name); + } + return name; +} + +/** + * @typedef {{t: string, v: string, s: number, e: number}} Token + */ diff --git a/packages/linkifyjs/tsconfig.json b/packages/linkifyjs/tsconfig.json index f2143130..362fac7c 100644 --- a/packages/linkifyjs/tsconfig.json +++ b/packages/linkifyjs/tsconfig.json @@ -3,6 +3,7 @@ "exclude": [], "compilerOptions": { "allowJs": true, + // "checkJs": true, "declaration": true, "emitDeclarationOnly": true, "outDir": "."