` anchor tags with [Linkify](https://linkify.js.org/).
7 |
8 | ## Installation
9 |
10 | Install from the command line with NPM
11 |
12 | ```
13 | npm install linkifyjs linkify-plugin-ticket
14 | ```
15 |
16 | Import into your JavaScript with `require`
17 | ```js
18 | const linkify = require('linkifyjs')
19 | require('linkify-plugin-ticket');
20 | ```
21 | or with ES modules
22 |
23 | ```js
24 | import * as linkify from 'linkifyjs';
25 | import 'linkify-plugin-ticket';
26 | ```
27 |
28 | ## Usage
29 |
30 | [Read the full documentation](https://linkify.js.org/docs/plugin-ticket.html).
31 |
32 | ## License
33 |
34 | MIT
35 |
--------------------------------------------------------------------------------
/packages/linkify-plugin-ticket/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "linkify-plugin-ticket",
3 | "type": "module",
4 | "version": "4.3.1",
5 | "description": "Numeric ticket plugin for linkifyjs",
6 | "main": "dist/linkify-plugin-ticket.cjs",
7 | "module": "dist/linkify-plugin-ticket.mjs",
8 | "scripts": {
9 | "build": "rollup -c rollup.config.js",
10 | "clean": "rm -rf lib dist *.tgz *.d.ts",
11 | "prepack": "run-s clean build tsc",
12 | "tsc": "tsc",
13 | "test": "echo \"Error: no test specified\" && exit 1"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/nfrasser/linkifyjs.git",
18 | "directory": "packages/linkify-plugin-ticket"
19 | },
20 | "keywords": [
21 | "link",
22 | "autolink",
23 | "url",
24 | "email"
25 | ],
26 | "author": "Nick Frasser (https://nfrasser.com)",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/nfrasser/linkifyjs/issues"
30 | },
31 | "homepage": "https://linkify.js.org",
32 | "peerDependencies": {
33 | "linkifyjs": "^4.0.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/linkify-plugin-ticket/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { linkifyPlugin } from '../../rollup.config.js';
2 | export default linkifyPlugin('ticket');
3 |
--------------------------------------------------------------------------------
/packages/linkify-plugin-ticket/src/index.mjs:
--------------------------------------------------------------------------------
1 | import { registerPlugin } from 'linkifyjs';
2 | import ticket from './ticket';
3 |
4 | registerPlugin('ticket', ticket);
5 |
--------------------------------------------------------------------------------
/packages/linkify-plugin-ticket/src/ticket.mjs:
--------------------------------------------------------------------------------
1 | import { createTokenClass, State } from 'linkifyjs';
2 |
3 | const TicketToken = createTokenClass('ticket', { isLink: true });
4 |
5 | /**
6 | * @type {import('linkifyjs').Plugin}
7 | */
8 | export default function ticket({ scanner, parser }) {
9 | // TODO: Add cross-repo style tickets? e.g., nfrasser/linkifyjs#42
10 | // Is that even feasible?
11 | const { POUND, groups } = scanner.tokens;
12 |
13 | const Hash = parser.start.tt(POUND);
14 | const Ticket = new State(TicketToken);
15 | Hash.ta(groups.numeric, Ticket);
16 | }
17 |
--------------------------------------------------------------------------------
/packages/linkify-plugin-ticket/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["dist/linkify-plugin-ticket.cjs", "dist/linkify-plugin-ticket.mjs"],
3 | "exclude": [],
4 | "compilerOptions": {
5 | "allowJs": true,
6 | "declaration": true,
7 | "emitDeclarationOnly": true,
8 | "maxNodeModuleJsDepth": 1
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/linkify-react/.npmignore:
--------------------------------------------------------------------------------
1 | ../../.npmignore
--------------------------------------------------------------------------------
/packages/linkify-react/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 Nick Frasser
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/linkify-react/README.md:
--------------------------------------------------------------------------------
1 | linkify-react
2 | ===
3 |
4 | [](https://www.npmjs.com/package/linkify-react)
5 |
6 | [Linkify](https://linkify.js.org/) React component. Use it to find URLs, email addresses and more in child strings and replace them with strings and <a> elements.
7 |
8 | ## Installation
9 |
10 | Install from the command line with NPM
11 |
12 | ```
13 | npm install linkifyjs linkify-react
14 | ```
15 |
16 | Import into your JavaScript with `require`
17 | ```js
18 | const Linkify = require('linkify-react');
19 | ```
20 | or with ES modules
21 |
22 | ```js
23 | import Linkify from 'linkify-react';
24 | ```
25 |
26 | ## Usage
27 |
28 | ```jsx
29 | const contents = 'helloworld.com';
30 |
31 |
32 | {contents}
33 |
34 | ```
35 |
36 | [Read the full documentation](https://linkify.js.org/docs/linkify-react.html).
37 |
38 | ## License
39 |
40 | MIT
41 |
--------------------------------------------------------------------------------
/packages/linkify-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "linkify-react",
3 | "type": "module",
4 | "version": "4.3.1",
5 | "description": "React element interface for linkifyjs",
6 | "main": "dist/linkify-react.cjs",
7 | "module": "dist/linkify-react.mjs",
8 | "scripts": {
9 | "build": "rollup -c rollup.config.js",
10 | "clean": "rm -rf lib dist *.tgz *.d.ts",
11 | "prepack": "run-s clean build tsc",
12 | "tsc": "tsc",
13 | "test": "echo \"Error: no test specified\" && exit 1"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/nfrasser/linkifyjs.git",
18 | "directory": "packages/linkify-react"
19 | },
20 | "keywords": [
21 | "link",
22 | "autolink",
23 | "url",
24 | "email",
25 | "react"
26 | ],
27 | "author": "Nick Frasser (https://nfrasser.com)",
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/nfrasser/linkifyjs/issues"
31 | },
32 | "homepage": "https://linkify.js.org",
33 | "peerDependencies": {
34 | "linkifyjs": "^4.0.0",
35 | "react": ">= 15.0.0"
36 | },
37 | "devDependencies": {
38 | "@types/react": "^19.1.3"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/linkify-react/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { linkifyInterface } from '../../rollup.config.js';
2 |
3 | export default linkifyInterface('react', {
4 | globalName: 'Linkify',
5 | globals: { react: 'React' },
6 | external: ['react'],
7 | });
8 |
--------------------------------------------------------------------------------
/packages/linkify-react/src/linkify-react.mjs:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { tokenize, Options, options } from 'linkifyjs';
3 |
4 | /**
5 | * Given a string, converts to an array of valid React components
6 | * (which may include strings)
7 | * @param {string} str
8 | * @param {Options} opts
9 | * @param {{ [elementId: string]: number }} meta
10 | * @returns {React.ReactNodeArray}
11 | */
12 | function stringToElements(str, opts, meta) {
13 |
14 | const tokens = tokenize(str);
15 | const elements = [];
16 |
17 | for (let i = 0; i < tokens.length; i++) {
18 | const token = tokens[i];
19 |
20 | if (token.t === 'nl' && opts.get('nl2br')) {
21 | const key = `__linkify-el-${meta.elementId++}`;
22 | elements.push(React.createElement('br', { key }));
23 | } else if (!token.isLink || !opts.check(token)) {
24 | // Regular text
25 | elements.push(token.toString());
26 | } else {
27 | let rendered = opts.render(token);
28 | if (!('key' in rendered.props)) {
29 | // Ensure generated element has unique key
30 | const key = `__linkify-el-${meta.elementId++}`;
31 | const props = options.assign({ key }, rendered.props);
32 | rendered = React.cloneElement(rendered, props);
33 | }
34 | elements.push(rendered);
35 | }
36 | }
37 |
38 | return elements;
39 | }
40 |
41 | // Recursively linkify the contents of the given React Element instance
42 | /**
43 | * @template P
44 | * @template {string | React.JSXElementConstructor} T
45 | * @param {React.ReactElement
} element
46 | * @param {Options} opts
47 | * @param {{ [elementId: string]: number }} meta
48 | * @returns {React.ReactElement
}
49 | */
50 | function linkifyReactElement(element, opts, meta) {
51 | if (React.Children.count(element.props.children) === 0) {
52 | // No need to clone if the element had no children
53 | return element;
54 | }
55 |
56 | const children = [];
57 |
58 | React.Children.forEach(element.props.children, (child) => {
59 | if (typeof child === 'string') {
60 | // ensure that we always generate unique element IDs for keys
61 | children.push.apply(children, stringToElements(child, opts, meta));
62 | } else if (React.isValidElement(child)) {
63 | if (typeof child.type === 'string'
64 | && opts.ignoreTags.indexOf(child.type.toUpperCase()) >= 0
65 | ) {
66 | // Don't linkify this element
67 | children.push(child);
68 | } else {
69 | children.push(linkifyReactElement(child, opts, meta));
70 | }
71 | } else {
72 | // Unknown element type, just push
73 | children.push(child);
74 | }
75 | });
76 |
77 | // Set a default unique key, copy over remaining props
78 | const key = `__linkify-el-${meta.elementId++}`;
79 | const newProps = options.assign({ key }, element.props);
80 | return React.cloneElement(element, newProps, children);
81 | }
82 |
83 | /**
84 | * @template P
85 | * @template {string | React.JSXElementConstructor
} T
86 | * @param {P & { as?: T, tagName?: T, options?: import('linkifyjs').Opts, children?: React.ReactNode}} props
87 | * @returns {React.ReactElement
}
88 | */
89 | const Linkify = (props) => {
90 | // Copy over all non-linkify-specific props
91 | let linkId = 0;
92 |
93 | const defaultLinkRender = ({ tagName, attributes, content }) => {
94 | attributes.key = `__linkify-lnk-${linkId++}`;
95 | if (attributes.class) {
96 | attributes.className = attributes.class;
97 | delete attributes.class;
98 | }
99 | return React.createElement(tagName, attributes, content);
100 | };
101 |
102 | const newProps = { key: '__linkify-wrapper' };
103 | for (const prop in props) {
104 | if (prop !== 'options' && prop !== 'as' && prop !== 'tagName' && prop !== 'children') {
105 | newProps[prop] = props[prop];
106 | }
107 | }
108 |
109 | const opts = new Options(props.options, defaultLinkRender);
110 | const as = props.as || props.tagName || React.Fragment || 'span';
111 | const children = props.children;
112 | const element = React.createElement(as, newProps, children);
113 |
114 | return linkifyReactElement(element, opts, { elementId: 0 });
115 | };
116 |
117 | export default Linkify;
118 |
--------------------------------------------------------------------------------
/packages/linkify-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["dist/linkify-react.cjs", "dist/linkify-react.mjs"],
3 | "exclude": [],
4 | "compilerOptions": {
5 | "allowJs": true,
6 | "declaration": true,
7 | "emitDeclarationOnly": true,
8 | "maxNodeModuleJsDepth": 1
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/linkify-string/.npmignore:
--------------------------------------------------------------------------------
1 | ../../.npmignore
--------------------------------------------------------------------------------
/packages/linkify-string/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 Nick Frasser
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/linkify-string/README.md:
--------------------------------------------------------------------------------
1 | linkify-string
2 | ===
3 |
4 | [](https://www.npmjs.com/package/linkify-string)
5 |
6 | [Linkify](https://linkify.js.org/) String Interface. Use `linkify-string` to detect URLs, email addresses and more in plain-text strings and wrap them with anchor `` tags.
7 |
8 | This function will ***not*** parse strings with HTML. Use one of the following instead, depending on your application:
9 |
10 | * [`linkify-html`](../linkify-html/)
11 | * [`linkify-element`](../linkify-element/)
12 | * [`linkify-jquery`](../linkify-jquery/)
13 |
14 |
15 | ## Installation
16 |
17 | Install from the command line with NPM
18 |
19 | ```
20 | npm install linkifyjs linkify-string
21 | ```
22 |
23 | Import into your JavaScript with `require`
24 | ```js
25 | const linkifyStr = require('linkify-string');
26 | ```
27 | or with ES modules
28 |
29 | ```js
30 | import linkifyStr from 'linkify-string';
31 | ```
32 |
33 | ## Usage
34 |
35 | [Read the full documentation](https://linkify.js.org/docs/linkify-string.html).
36 |
37 | ## License
38 |
39 | MIT
40 |
--------------------------------------------------------------------------------
/packages/linkify-string/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "linkify-string",
3 | "type": "module",
4 | "version": "4.3.1",
5 | "description": "String interface for linkifyjs",
6 | "main": "dist/linkify-string.cjs",
7 | "module": "dist/linkify-string.mjs",
8 | "scripts": {
9 | "build": "rollup -c rollup.config.js",
10 | "clean": "rm -rf lib dist *.tgz *.d.ts",
11 | "prepack": "run-s clean build tsc",
12 | "tsc": "tsc",
13 | "test": "echo \"Error: no test specified\" && exit 1"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/nfrasser/linkifyjs.git",
18 | "directory": "packages/linkify-string"
19 | },
20 | "keywords": [
21 | "link",
22 | "autolink",
23 | "url",
24 | "email"
25 | ],
26 | "author": "Nick Frasser (https://nfrasser.com)",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/nfrasser/linkifyjs/issues"
30 | },
31 | "homepage": "https://linkify.js.org",
32 | "peerDependencies": {
33 | "linkifyjs": "^4.0.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/linkify-string/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { linkifyInterface } from '../../rollup.config.js';
2 | export default linkifyInterface('string', { globalName: 'linkifyStr' });
3 |
--------------------------------------------------------------------------------
/packages/linkify-string/src/linkify-string.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | Convert strings of text into linkable HTML text
3 | */
4 | import { tokenize, Options } from 'linkifyjs';
5 |
6 | function escapeText(text) {
7 | return text
8 | .replace(/&/g, '&')
9 | .replace(//g, '>');
11 | }
12 |
13 | function escapeAttr(href) {
14 | return href.replace(/"/g, '"');
15 | }
16 |
17 | function attributesToString(attributes) {
18 | const result = [];
19 | for (const attr in attributes) {
20 | let val = attributes[attr] + '';
21 | result.push(`${attr}="${escapeAttr(val)}"`);
22 | }
23 | return result.join(' ');
24 | }
25 |
26 | function defaultRender({ tagName, attributes, content }) {
27 | return `<${tagName} ${attributesToString(attributes)}>${escapeText(content)}${tagName}>`;
28 | }
29 |
30 | /**
31 | * Convert a plan text string to an HTML string with links. Expects that the
32 | * given strings does not contain any HTML entities. Use the linkify-html
33 | * interface if you need to parse HTML entities.
34 | *
35 | * @param {string} str string to linkify
36 | * @param {import('linkifyjs').Opts} [opts] overridable options
37 | * @returns {string}
38 | */
39 | function linkifyStr(str, opts = {}) {
40 | opts = new Options(opts, defaultRender);
41 |
42 | const tokens = tokenize(str);
43 | const result = [];
44 |
45 | for (let i = 0; i < tokens.length; i++) {
46 | const token = tokens[i];
47 |
48 | if (token.t === 'nl' && opts.get('nl2br')) {
49 | result.push('
\n');
50 | } else if (!token.isLink || !opts.check(token)) {
51 | result.push(escapeText(token.toString()));
52 | } else {
53 | result.push(opts.render(token));
54 | }
55 | }
56 |
57 | return result.join('');
58 | }
59 |
60 | if (!String.prototype.linkify) {
61 | Object.defineProperty(String.prototype, 'linkify', {
62 | writable: false,
63 | value: function linkify(options) {
64 | return linkifyStr(this, options);
65 | }
66 | });
67 | }
68 |
69 | export default linkifyStr;
70 |
--------------------------------------------------------------------------------
/packages/linkify-string/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["dist/linkify-string.cjs", "dist/linkify-string.mjs"],
3 | "exclude": [],
4 | "compilerOptions": {
5 | "allowJs": true,
6 | "declaration": true,
7 | "emitDeclarationOnly": true,
8 | "maxNodeModuleJsDepth": 1
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/linkifyjs/.npmignore:
--------------------------------------------------------------------------------
1 | ../../.npmignore
--------------------------------------------------------------------------------
/packages/linkifyjs/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 Nick Frasser
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/packages/linkifyjs/README.md:
--------------------------------------------------------------------------------
1 | linkifyjs
2 | ===
3 |
4 | [](https://www.npmjs.com/package/linkifyjs)
5 |
6 | Core [Linkify](https://linkify.js.org/) JavaScript library. Use Linkify and its
7 | related packages to detect URLs, email addresses and more in plain-text strings and convert them to HTML `` anchor tags.
8 |
9 | ## Installation
10 |
11 | Install from the command line with NPM
12 |
13 | ```
14 | npm install linkifyjs
15 | ```
16 |
17 | Import into your JavaScript with `require`
18 | ```js
19 | const linkify = require('linkifyjs');
20 | ```
21 | or with ES modules
22 |
23 | ```js
24 | import * as linkify from 'linkifyjs';
25 | ```
26 |
27 | Separate packages are available for each of the following features:
28 | - [HTML strings](../linkify-html)
29 | - [React component](../linkify-react)
30 | - [jQuery plugin](../linkify-jquery)
31 | - [DOM Elements](../linkify-element)
32 | - [Plain-text](../linkify-string)
33 | - [#hashtag plugin](../linkify-plugin-hashtag)
34 | - [@mention plugin](../linkify-plugin-mention)
35 | - [#ticket plugin](../linkify-plugin-ticket)
36 | - [IP address plugin](../linkify-plugin-ip)
37 | - [Keyword plugin](../linkify-plugin-keyword)
38 |
39 | ## Usage
40 |
41 | [Read the full documentation](https://linkify.js.org/docs/linkifyjs.html).
42 |
43 | ## License
44 |
45 | MIT
46 |
--------------------------------------------------------------------------------
/packages/linkifyjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "linkifyjs",
3 | "type": "module",
4 | "version": "4.3.1",
5 | "description": "Find URLs, email addresses, #hashtags and @mentions in plain-text strings, then convert them into HTML links.",
6 | "main": "dist/linkify.cjs",
7 | "module": "dist/linkify.mjs",
8 | "scripts": {
9 | "build": "rollup -c rollup.config.js",
10 | "clean": "rm -rf lib dist *.tgz *.d.ts",
11 | "prepack": "run-s clean build tsc",
12 | "tsc": "tsc",
13 | "test": "echo \"Error: no test specified\" && exit 1"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/nfrasser/linkifyjs.git",
18 | "directory": "packages/linkifyjs"
19 | },
20 | "keywords": [
21 | "autolink",
22 | "email",
23 | "hashtag",
24 | "html",
25 | "jquery",
26 | "link",
27 | "mention",
28 | "react",
29 | "twitter",
30 | "url"
31 | ],
32 | "author": "Nick Frasser (https://nfrasser.com)",
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/nfrasser/linkifyjs/issues"
36 | },
37 | "homepage": "https://linkify.js.org"
38 | }
39 |
--------------------------------------------------------------------------------
/packages/linkifyjs/rollup.config.js:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 | import { plugins } from '../../rollup.config.js';
3 |
4 | export default [
5 | {
6 | input: 'src/linkify.mjs',
7 | output: [
8 | { file: 'dist/linkify.js', name: 'linkify', format: 'iife' },
9 | { file: 'dist/linkify.min.js', name: 'linkify', format: 'iife', plugins: [terser()] },
10 | { file: 'dist/linkify.cjs', format: 'cjs', exports: 'auto' },
11 | { file: 'dist/linkify.mjs', format: 'es' },
12 | ],
13 | plugins,
14 | },
15 | ];
16 |
--------------------------------------------------------------------------------
/packages/linkifyjs/src/assign.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @template A
3 | * @template B
4 | * @param {A} target
5 | * @param {B} properties
6 | * @return {A & B}
7 | */
8 | const assign = (target, properties) => {
9 | for (const key in properties) {
10 | target[key] = properties[key];
11 | }
12 | return target;
13 | };
14 |
15 | export default assign;
16 |
--------------------------------------------------------------------------------
/packages/linkifyjs/src/linkify.mjs:
--------------------------------------------------------------------------------
1 | import { init as initScanner, run as runScanner } from './scanner.mjs';
2 | import { init as initParser, run as runParser } from './parser.mjs';
3 | import { Options } from './options.mjs';
4 | import { State } from './fsm.mjs';
5 |
6 | const warn = (typeof console !== 'undefined' && console && console.warn) || (() => {});
7 | const warnAdvice =
8 | 'until manual call of linkify.init(). Register all schemes and plugins before invoking linkify the first time.';
9 |
10 | // Side-effect initialization state
11 | const INIT = {
12 | scanner: null,
13 | parser: null,
14 | tokenQueue: [],
15 | pluginQueue: [],
16 | customSchemes: [],
17 | initialized: false,
18 | };
19 |
20 | /**
21 | * @typedef {{
22 | * start: State,
23 | * tokens: { groups: Collections } & typeof tk
24 | * }} ScannerInit
25 | */
26 |
27 | /**
28 | * @typedef {{
29 | * start: State,
30 | * tokens: typeof multi
31 | * }} ParserInit
32 | */
33 |
34 | /**
35 | * @typedef {(arg: { scanner: ScannerInit }) => void} TokenPlugin
36 | */
37 |
38 | /**
39 | * @typedef {(arg: { scanner: ScannerInit, parser: ParserInit }) => void} Plugin
40 | */
41 |
42 | /**
43 | * De-register all plugins and reset the internal state-machine. Used for
44 | * testing; not required in practice.
45 | * @private
46 | */
47 | export function reset() {
48 | State.groups = {};
49 | INIT.scanner = null;
50 | INIT.parser = null;
51 | INIT.tokenQueue = [];
52 | INIT.pluginQueue = [];
53 | INIT.customSchemes = [];
54 | INIT.initialized = false;
55 | return INIT;
56 | }
57 |
58 | /**
59 | * Register a token plugin to allow the scanner to recognize additional token
60 | * types before the parser state machine is constructed from the results.
61 | * @param {string} name of plugin to register
62 | * @param {TokenPlugin} plugin function that accepts the scanner state machine
63 | * and available scanner tokens and collections and extends the state machine to
64 | * recognize additional tokens or groups.
65 | */
66 | export function registerTokenPlugin(name, plugin) {
67 | if (typeof plugin !== 'function') {
68 | throw new Error(`linkifyjs: Invalid token plugin ${plugin} (expects function)`);
69 | }
70 | for (let i = 0; i < INIT.tokenQueue.length; i++) {
71 | if (name === INIT.tokenQueue[i][0]) {
72 | warn(`linkifyjs: token plugin "${name}" already registered - will be overwritten`);
73 | INIT.tokenQueue[i] = [name, plugin];
74 | return;
75 | }
76 | }
77 | INIT.tokenQueue.push([name, plugin]);
78 | if (INIT.initialized) {
79 | warn(`linkifyjs: already initialized - will not register token plugin "${name}" ${warnAdvice}`);
80 | }
81 | }
82 |
83 | /**
84 | * Register a linkify plugin
85 | * @param {string} name of plugin to register
86 | * @param {Plugin} plugin function that accepts the parser state machine and
87 | * extends the parser to recognize additional link types
88 | */
89 | export function registerPlugin(name, plugin) {
90 | if (typeof plugin !== 'function') {
91 | throw new Error(`linkifyjs: Invalid plugin ${plugin} (expects function)`);
92 | }
93 | for (let i = 0; i < INIT.pluginQueue.length; i++) {
94 | if (name === INIT.pluginQueue[i][0]) {
95 | warn(`linkifyjs: plugin "${name}" already registered - will be overwritten`);
96 | INIT.pluginQueue[i] = [name, plugin];
97 | return;
98 | }
99 | }
100 | INIT.pluginQueue.push([name, plugin]);
101 | if (INIT.initialized) {
102 | warn(`linkifyjs: already initialized - will not register plugin "${name}" ${warnAdvice}`);
103 | }
104 | }
105 |
106 | /**
107 | * Detect URLs with the following additional protocol. Anything with format
108 | * "protocol://..." will be considered a link. If `optionalSlashSlash` is set to
109 | * `true`, anything with format "protocol:..." will be considered a link.
110 | * @param {string} scheme
111 | * @param {boolean} [optionalSlashSlash]
112 | */
113 | export function registerCustomProtocol(scheme, optionalSlashSlash = false) {
114 | if (INIT.initialized) {
115 | warn(`linkifyjs: already initialized - will not register custom scheme "${scheme}" ${warnAdvice}`);
116 | }
117 | if (!/^[0-9a-z]+(-[0-9a-z]+)*$/.test(scheme)) {
118 | throw new Error(`linkifyjs: incorrect scheme format.
119 | 1. Must only contain digits, lowercase ASCII letters or "-"
120 | 2. Cannot start or end with "-"
121 | 3. "-" cannot repeat`);
122 | }
123 | INIT.customSchemes.push([scheme, optionalSlashSlash]);
124 | }
125 |
126 | /**
127 | * Initialize the linkify state machine. Called automatically the first time
128 | * linkify is called on a string, but may be called manually as well.
129 | */
130 | export function init() {
131 | // Initialize scanner state machine and plugins
132 | INIT.scanner = initScanner(INIT.customSchemes);
133 | for (let i = 0; i < INIT.tokenQueue.length; i++) {
134 | INIT.tokenQueue[i][1]({
135 | scanner: INIT.scanner,
136 | });
137 | }
138 |
139 | // Initialize parser state machine and plugins
140 | INIT.parser = initParser(INIT.scanner.tokens);
141 | for (let i = 0; i < INIT.pluginQueue.length; i++) {
142 | INIT.pluginQueue[i][1]({
143 | scanner: INIT.scanner,
144 | parser: INIT.parser,
145 | });
146 | }
147 | INIT.initialized = true;
148 | return INIT;
149 | }
150 |
151 | /**
152 | * Parse a string into tokens that represent linkable and non-linkable sub-components
153 | * @param {string} str
154 | * @return {MultiToken[]} tokens
155 | */
156 | export function tokenize(str) {
157 | if (!INIT.initialized) {
158 | init();
159 | }
160 | return runParser(INIT.parser.start, str, runScanner(INIT.scanner.start, str));
161 | }
162 | tokenize.scan = runScanner; // for testing
163 |
164 | /**
165 | * Find a list of linkable items in the given string.
166 | * @param {string} str string to find links in
167 | * @param {string | Opts} [type] either formatting options or specific type of
168 | * links to find, e.g., 'url' or 'email'
169 | * @param {Opts} [opts] formatting options for final output. Cannot be specified
170 | * if opts already provided in `type` argument
171 | */
172 | export function find(str, type = null, opts = null) {
173 | if (type && typeof type === 'object') {
174 | if (opts) {
175 | throw Error(`linkifyjs: Invalid link type ${type}; must be a string`);
176 | }
177 | opts = type;
178 | type = null;
179 | }
180 | const options = new Options(opts);
181 | const tokens = tokenize(str);
182 | const filtered = [];
183 |
184 | for (let i = 0; i < tokens.length; i++) {
185 | const token = tokens[i];
186 | if (token.isLink && (!type || token.t === type) && options.check(token)) {
187 | filtered.push(token.toFormattedObject(options));
188 | }
189 | }
190 |
191 | return filtered;
192 | }
193 |
194 | /**
195 | * Is the given string valid linkable text of some sort. Note that this does not
196 | * trim the text for you.
197 | *
198 | * Optionally pass in a second `type` param, which is the type of link to test
199 | * for.
200 | *
201 | * For example,
202 | *
203 | * linkify.test(str, 'email');
204 | *
205 | * Returns `true` if str is a valid email.
206 | * @param {string} str string to test for links
207 | * @param {string} [type] optional specific link type to look for
208 | * @returns boolean true/false
209 | */
210 | export function test(str, type = null) {
211 | const tokens = tokenize(str);
212 | return tokens.length === 1 && tokens[0].isLink && (!type || tokens[0].t === type);
213 | }
214 |
215 | export * as options from './options.mjs';
216 | export * as regexp from './regexp.mjs';
217 | export * as multi from './multi.mjs';
218 | export * as text from './multi.mjs';
219 | export { MultiToken, createTokenClass } from './multi.mjs';
220 | export { stringToArray } from './scanner.mjs';
221 | export { State } from './fsm.mjs';
222 | export { Options };
223 |
--------------------------------------------------------------------------------
/packages/linkifyjs/src/multi.mjs:
--------------------------------------------------------------------------------
1 | import { COLON, LOCALHOST } from './text.mjs';
2 | import { defaults } from './options.mjs';
3 | import assign from './assign.mjs';
4 |
5 | /******************************************************************************
6 | Multi-Tokens
7 | Tokens composed of arrays of TextTokens
8 | ******************************************************************************/
9 |
10 | /**
11 | * @param {string} value
12 | * @param {Token[]} tokens
13 | */
14 | export function MultiToken(value, tokens) {
15 | this.t = 'token';
16 | this.v = value;
17 | this.tk = tokens;
18 | }
19 |
20 | /**
21 | * Abstract class used for manufacturing tokens of text tokens. That is rather
22 | * than the value for a token being a small string of text, it's value an array
23 | * of text tokens.
24 | *
25 | * Used for grouping together URLs, emails, hashtags, and other potential
26 | * creations.
27 | * @class MultiToken
28 | * @property {string} t
29 | * @property {string} v
30 | * @property {Token[]} tk
31 | * @abstract
32 | */
33 | MultiToken.prototype = {
34 | isLink: false,
35 |
36 | /**
37 | * Return the string this token represents.
38 | * @return {string}
39 | */
40 | toString() {
41 | return this.v;
42 | },
43 |
44 | /**
45 | * What should the value for this token be in the `href` HTML attribute?
46 | * Returns the `.toString` value by default.
47 | * @param {string} [scheme]
48 | * @return {string}
49 | */
50 | toHref(scheme) {
51 | !!scheme;
52 | return this.toString();
53 | },
54 |
55 | /**
56 | * @param {Options} options Formatting options
57 | * @returns {string}
58 | */
59 | toFormattedString(options) {
60 | const val = this.toString();
61 | const truncate = options.get('truncate', val, this);
62 | const formatted = options.get('format', val, this);
63 | return truncate && formatted.length > truncate ? formatted.substring(0, truncate) + '…' : formatted;
64 | },
65 |
66 | /**
67 | *
68 | * @param {Options} options
69 | * @returns {string}
70 | */
71 | toFormattedHref(options) {
72 | return options.get('formatHref', this.toHref(options.get('defaultProtocol')), this);
73 | },
74 |
75 | /**
76 | * The start index of this token in the original input string
77 | * @returns {number}
78 | */
79 | startIndex() {
80 | return this.tk[0].s;
81 | },
82 |
83 | /**
84 | * The end index of this token in the original input string (up to this
85 | * index but not including it)
86 | * @returns {number}
87 | */
88 | endIndex() {
89 | return this.tk[this.tk.length - 1].e;
90 | },
91 |
92 | /**
93 | Returns an object of relevant values for this token, which includes keys
94 | * type - Kind of token ('url', 'email', etc.)
95 | * value - Original text
96 | * href - The value that should be added to the anchor tag's href
97 | attribute
98 |
99 | @method toObject
100 | @param {string} [protocol] `'http'` by default
101 | */
102 | toObject(protocol = defaults.defaultProtocol) {
103 | return {
104 | type: this.t,
105 | value: this.toString(),
106 | isLink: this.isLink,
107 | href: this.toHref(protocol),
108 | start: this.startIndex(),
109 | end: this.endIndex(),
110 | };
111 | },
112 |
113 | /**
114 | *
115 | * @param {Options} options Formatting option
116 | */
117 | toFormattedObject(options) {
118 | return {
119 | type: this.t,
120 | value: this.toFormattedString(options),
121 | isLink: this.isLink,
122 | href: this.toFormattedHref(options),
123 | start: this.startIndex(),
124 | end: this.endIndex(),
125 | };
126 | },
127 |
128 | /**
129 | * Whether this token should be rendered as a link according to the given options
130 | * @param {Options} options
131 | * @returns {boolean}
132 | */
133 | validate(options) {
134 | return options.get('validate', this.toString(), this);
135 | },
136 |
137 | /**
138 | * Return an object that represents how this link should be rendered.
139 | * @param {Options} options Formattinng options
140 | */
141 | render(options) {
142 | const token = this;
143 | const href = this.toHref(options.get('defaultProtocol'));
144 | const formattedHref = options.get('formatHref', href, this);
145 | const tagName = options.get('tagName', href, token);
146 | const content = this.toFormattedString(options);
147 |
148 | const attributes = {};
149 | const className = options.get('className', href, token);
150 | const target = options.get('target', href, token);
151 | const rel = options.get('rel', href, token);
152 | const attrs = options.getObj('attributes', href, token);
153 | const eventListeners = options.getObj('events', href, token);
154 |
155 | attributes.href = formattedHref;
156 | if (className) {
157 | attributes.class = className;
158 | }
159 | if (target) {
160 | attributes.target = target;
161 | }
162 | if (rel) {
163 | attributes.rel = rel;
164 | }
165 | if (attrs) {
166 | assign(attributes, attrs);
167 | }
168 |
169 | return { tagName, attributes, content, eventListeners };
170 | },
171 | };
172 |
173 | // Base token
174 | export { MultiToken as Base };
175 |
176 | /**
177 | * Create a new token that can be emitted by the parser state machine
178 | * @param {string} type readable type of the token
179 | * @param {object} props properties to assign or override, including isLink = true or false
180 | * @returns {new (value: string, tokens: Token[]) => MultiToken} new token class
181 | */
182 | export function createTokenClass(type, props) {
183 | class Token extends MultiToken {
184 | constructor(value, tokens) {
185 | super(value, tokens);
186 | this.t = type;
187 | }
188 | }
189 | for (const p in props) {
190 | Token.prototype[p] = props[p];
191 | }
192 | Token.t = type;
193 | return Token;
194 | }
195 |
196 | /**
197 | Represents a list of tokens making up a valid email address
198 | */
199 | export const Email = createTokenClass('email', {
200 | isLink: true,
201 | toHref() {
202 | return 'mailto:' + this.toString();
203 | },
204 | });
205 |
206 | /**
207 | Represents some plain text
208 | */
209 | export const Text = createTokenClass('text');
210 |
211 | /**
212 | Multi-linebreak token - represents a line break
213 | @class Nl
214 | */
215 | export const Nl = createTokenClass('nl');
216 |
217 | /**
218 | Represents a list of text tokens making up a valid URL
219 | @class Url
220 | */
221 | export const Url = createTokenClass('url', {
222 | isLink: true,
223 |
224 | /**
225 | Lowercases relevant parts of the domain and adds the protocol if
226 | required. Note that this will not escape unsafe HTML characters in the
227 | URL.
228 |
229 | @param {string} [scheme] default scheme (e.g., 'https')
230 | @return {string} the full href
231 | */
232 | toHref(scheme = defaults.defaultProtocol) {
233 | // Check if already has a prefix scheme
234 | return this.hasProtocol() ? this.v : `${scheme}://${this.v}`;
235 | },
236 |
237 | /**
238 | * Check whether this URL token has a protocol
239 | * @return {boolean}
240 | */
241 | hasProtocol() {
242 | const tokens = this.tk;
243 | return tokens.length >= 2 && tokens[0].t !== LOCALHOST && tokens[1].t === COLON;
244 | },
245 | });
246 |
--------------------------------------------------------------------------------
/packages/linkifyjs/src/options.mjs:
--------------------------------------------------------------------------------
1 | import assign from './assign.mjs';
2 |
3 | /**
4 | * An object where each key is a valid DOM Event Name such as `click` or `focus`
5 | * and each value is an event handler function.
6 | *
7 | * https://developer.mozilla.org/en-US/docs/Web/API/Element#events
8 | * @typedef {?{ [event: string]: Function }} EventListeners
9 | */
10 |
11 | /**
12 | * All formatted properties required to render a link, including `tagName`,
13 | * `attributes`, `content` and `eventListeners`.
14 | * @typedef {{ tagName: any, attributes: {[attr: string]: any}, content: string,
15 | * eventListeners: EventListeners }} IntermediateRepresentation
16 | */
17 |
18 | /**
19 | * Specify either an object described by the template type `O` or a function.
20 | *
21 | * The function takes a string value (usually the link's href attribute), the
22 | * link type (`'url'`, `'hashtag`', etc.) and an internal token representation
23 | * of the link. It should return an object of the template type `O`
24 | * @template O
25 | * @typedef {O | ((value: string, type: string, token: MultiToken) => O)} OptObj
26 | */
27 |
28 | /**
29 | * Specify either a function described by template type `F` or an object.
30 | *
31 | * Each key in the object should be a link type (`'url'`, `'hashtag`', etc.). Each
32 | * value should be a function with template type `F` that is called when the
33 | * corresponding link type is encountered.
34 | * @template F
35 | * @typedef {F | { [type: string]: F}} OptFn
36 | */
37 |
38 | /**
39 | * Specify either a value with template type `V`, a function that returns `V` or
40 | * an object where each value resolves to `V`.
41 | *
42 | * The function takes a string value (usually the link's href attribute), the
43 | * link type (`'url'`, `'hashtag`', etc.) and an internal token representation
44 | * of the link. It should return an object of the template type `V`
45 | *
46 | * For the object, each key should be a link type (`'url'`, `'hashtag`', etc.).
47 | * Each value should either have type `V` or a function that returns V. This
48 | * function similarly takes a string value and a token.
49 | *
50 | * Example valid types for `Opt`:
51 | *
52 | * ```js
53 | * 'hello'
54 | * (value, type, token) => 'world'
55 | * { url: 'hello', email: (value, token) => 'world'}
56 | * ```
57 | * @template V
58 | * @typedef {V | ((value: string, type: string, token: MultiToken) => V) | { [type: string]: V | ((value: string, token: MultiToken) => V) }} Opt
59 | */
60 |
61 | /**
62 | * See available options: https://linkify.js.org/docs/options.html
63 | * @typedef {{
64 | * defaultProtocol?: string,
65 | * events?: OptObj,
66 | * format?: Opt,
67 | * formatHref?: Opt,
68 | * nl2br?: boolean,
69 | * tagName?: Opt,
70 | * target?: Opt,
71 | * rel?: Opt,
72 | * validate?: Opt,
73 | * truncate?: Opt,
74 | * className?: Opt,
75 | * attributes?: OptObj<({ [attr: string]: any })>,
76 | * ignoreTags?: string[],
77 | * render?: OptFn<((ir: IntermediateRepresentation) => any)>
78 | * }} Opts
79 | */
80 |
81 | /**
82 | * @type Required
83 | */
84 | export const defaults = {
85 | defaultProtocol: 'http',
86 | events: null,
87 | format: noop,
88 | formatHref: noop,
89 | nl2br: false,
90 | tagName: 'a',
91 | target: null,
92 | rel: null,
93 | validate: true,
94 | truncate: Infinity,
95 | className: null,
96 | attributes: null,
97 | ignoreTags: [],
98 | render: null,
99 | };
100 |
101 | /**
102 | * Utility class for linkify interfaces to apply specified
103 | * {@link Opts formatting and rendering options}.
104 | *
105 | * @param {Opts | Options} [opts] Option value overrides.
106 | * @param {(ir: IntermediateRepresentation) => any} [defaultRender] (For
107 | * internal use) default render function that determines how to generate an
108 | * HTML element based on a link token's derived tagName, attributes and HTML.
109 | * Similar to render option
110 | */
111 | export function Options(opts, defaultRender = null) {
112 | let o = assign({}, defaults);
113 | if (opts) {
114 | o = assign(o, opts instanceof Options ? opts.o : opts);
115 | }
116 |
117 | // Ensure all ignored tags are uppercase
118 | const ignoredTags = o.ignoreTags;
119 | const uppercaseIgnoredTags = [];
120 | for (let i = 0; i < ignoredTags.length; i++) {
121 | uppercaseIgnoredTags.push(ignoredTags[i].toUpperCase());
122 | }
123 | /** @protected */
124 | this.o = o;
125 | if (defaultRender) {
126 | this.defaultRender = defaultRender;
127 | }
128 | this.ignoreTags = uppercaseIgnoredTags;
129 | }
130 |
131 | Options.prototype = {
132 | o: defaults,
133 |
134 | /**
135 | * @type string[]
136 | */
137 | ignoreTags: [],
138 |
139 | /**
140 | * @param {IntermediateRepresentation} ir
141 | * @returns {any}
142 | */
143 | defaultRender(ir) {
144 | return ir;
145 | },
146 |
147 | /**
148 | * Returns true or false based on whether a token should be displayed as a
149 | * link based on the user options.
150 | * @param {MultiToken} token
151 | * @returns {boolean}
152 | */
153 | check(token) {
154 | return this.get('validate', token.toString(), token);
155 | },
156 |
157 | // Private methods
158 |
159 | /**
160 | * Resolve an option's value based on the value of the option and the given
161 | * params. If operator and token are specified and the target option is
162 | * callable, automatically calls the function with the given argument.
163 | * @template {keyof Opts} K
164 | * @param {K} key Name of option to use
165 | * @param {string} [operator] will be passed to the target option if it's a
166 | * function. If not specified, RAW function value gets returned
167 | * @param {MultiToken} [token] The token from linkify.tokenize
168 | * @returns {Opts[K] | any}
169 | */
170 | get(key, operator, token) {
171 | const isCallable = operator != null;
172 | let option = this.o[key];
173 | if (!option) {
174 | return option;
175 | }
176 | if (typeof option === 'object') {
177 | option = token.t in option ? option[token.t] : defaults[key];
178 | if (typeof option === 'function' && isCallable) {
179 | option = option(operator, token);
180 | }
181 | } else if (typeof option === 'function' && isCallable) {
182 | option = option(operator, token.t, token);
183 | }
184 |
185 | return option;
186 | },
187 |
188 | /**
189 | * @template {keyof Opts} L
190 | * @param {L} key Name of options object to use
191 | * @param {string} [operator]
192 | * @param {MultiToken} [token]
193 | * @returns {Opts[L] | any}
194 | */
195 | getObj(key, operator, token) {
196 | let obj = this.o[key];
197 | if (typeof obj === 'function' && operator != null) {
198 | obj = obj(operator, token.t, token);
199 | }
200 | return obj;
201 | },
202 |
203 | /**
204 | * Convert the given token to a rendered element that may be added to the
205 | * calling-interface's DOM
206 | * @param {MultiToken} token Token to render to an HTML element
207 | * @returns {any} Render result; e.g., HTML string, DOM element, React
208 | * Component, etc.
209 | */
210 | render(token) {
211 | const ir = token.render(this); // intermediate representation
212 | const renderFn = this.get('render', null, token) || this.defaultRender;
213 | return renderFn(ir, token.t, token);
214 | },
215 | };
216 |
217 | export { assign };
218 |
219 | function noop(val) {
220 | return val;
221 | }
222 |
--------------------------------------------------------------------------------
/packages/linkifyjs/src/regexp.mjs:
--------------------------------------------------------------------------------
1 | // Note that these two Unicode ones expand into a really big one with Babel
2 | export const ASCII_LETTER = /[a-z]/;
3 | export const LETTER = /\p{L}/u; // Any Unicode character with letter data type
4 | export const EMOJI = /\p{Emoji}/u; // Any Unicode emoji character
5 | export const EMOJI_VARIATION = /\ufe0f/;
6 | export const DIGIT = /\d/;
7 | export const SPACE = /\s/;
8 |
--------------------------------------------------------------------------------
/packages/linkifyjs/src/text.mjs:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | Text Tokens
3 | Identifiers for token outputs from the regexp scanner
4 | ******************************************************************************/
5 |
6 | // A valid web domain token
7 | export const WORD = 'WORD'; // only contains a-z
8 | export const UWORD = 'UWORD'; // contains letters other than a-z, used for IDN
9 | export const ASCIINUMERICAL = 'ASCIINUMERICAL'; // contains a-z, 0-9
10 | export const ALPHANUMERICAL = 'ALPHANUMERICAL'; // contains numbers and letters other than a-z, used for IDN
11 |
12 | // Special case of word
13 | export const LOCALHOST = 'LOCALHOST';
14 |
15 | // Valid top-level domain, special case of WORD (see tlds.js)
16 | export const TLD = 'TLD';
17 |
18 | // Valid IDN TLD, special case of UWORD (see tlds.js)
19 | export const UTLD = 'UTLD';
20 |
21 | // The scheme portion of a web URI protocol. Supported types include: `mailto`,
22 | // `file`, and user-defined custom protocols. Limited to schemes that contain
23 | // only letters
24 | export const SCHEME = 'SCHEME';
25 |
26 | // Similar to SCHEME, except makes distinction for schemes that must always be
27 | // followed by `://`, not just `:`. Supported types include `http`, `https`,
28 | // `ftp`, `ftps`
29 | export const SLASH_SCHEME = 'SLASH_SCHEME';
30 |
31 | // Any sequence of digits 0-9
32 | export const NUM = 'NUM';
33 |
34 | // Any number of consecutive whitespace characters that are not newline
35 | export const WS = 'WS';
36 |
37 | // New line (unix style)
38 | export const NL = 'NL'; // \n
39 |
40 | // Opening/closing bracket classes
41 | // TODO: Rename OPEN -> LEFT and CLOSE -> RIGHT in v5 to fit with Unicode names
42 | // Also rename angle brackes to LESSTHAN and GREATER THAN
43 | export const OPENBRACE = 'OPENBRACE'; // {
44 | export const CLOSEBRACE = 'CLOSEBRACE'; // }
45 | export const OPENBRACKET = 'OPENBRACKET'; // [
46 | export const CLOSEBRACKET = 'CLOSEBRACKET'; // ]
47 | export const OPENPAREN = 'OPENPAREN'; // (
48 | export const CLOSEPAREN = 'CLOSEPAREN'; // )
49 | export const OPENANGLEBRACKET = 'OPENANGLEBRACKET'; // <
50 | export const CLOSEANGLEBRACKET = 'CLOSEANGLEBRACKET'; // >
51 | export const FULLWIDTHLEFTPAREN = 'FULLWIDTHLEFTPAREN'; // (
52 | export const FULLWIDTHRIGHTPAREN = 'FULLWIDTHRIGHTPAREN'; // )
53 | export const LEFTCORNERBRACKET = 'LEFTCORNERBRACKET'; // 「
54 | export const RIGHTCORNERBRACKET = 'RIGHTCORNERBRACKET'; // 」
55 | export const LEFTWHITECORNERBRACKET = 'LEFTWHITECORNERBRACKET'; // 『
56 | export const RIGHTWHITECORNERBRACKET = 'RIGHTWHITECORNERBRACKET'; // 』
57 | export const FULLWIDTHLESSTHAN = 'FULLWIDTHLESSTHAN'; // <
58 | export const FULLWIDTHGREATERTHAN = 'FULLWIDTHGREATERTHAN'; // >
59 |
60 | // Various symbols
61 | export const AMPERSAND = 'AMPERSAND'; // &
62 | export const APOSTROPHE = 'APOSTROPHE'; // '
63 | export const ASTERISK = 'ASTERISK'; // *
64 | export const AT = 'AT'; // @
65 | export const BACKSLASH = 'BACKSLASH'; // \
66 | export const BACKTICK = 'BACKTICK'; // `
67 | export const CARET = 'CARET'; // ^
68 | export const COLON = 'COLON'; // :
69 | export const COMMA = 'COMMA'; // ,
70 | export const DOLLAR = 'DOLLAR'; // $
71 | export const DOT = 'DOT'; // .
72 | export const EQUALS = 'EQUALS'; // =
73 | export const EXCLAMATION = 'EXCLAMATION'; // !
74 | export const HYPHEN = 'HYPHEN'; // -
75 | export const PERCENT = 'PERCENT'; // %
76 | export const PIPE = 'PIPE'; // |
77 | export const PLUS = 'PLUS'; // +
78 | export const POUND = 'POUND'; // #
79 | export const QUERY = 'QUERY'; // ?
80 | export const QUOTE = 'QUOTE'; // "
81 | export const FULLWIDTHMIDDLEDOT = 'FULLWIDTHMIDDLEDOT'; // ・
82 |
83 | export const SEMI = 'SEMI'; // ;
84 | export const SLASH = 'SLASH'; // /
85 | export const TILDE = 'TILDE'; // ~
86 | export const UNDERSCORE = 'UNDERSCORE'; // _
87 |
88 | // Emoji symbol
89 | export const EMOJI = 'EMOJI';
90 |
91 | // Default token - anything that is not one of the above
92 | export const SYM = 'SYM';
93 |
--------------------------------------------------------------------------------
/packages/linkifyjs/src/tlds.mjs:
--------------------------------------------------------------------------------
1 | // THIS FILE IS AUTOMATICALLY GENERATED DO NOT EDIT DIRECTLY
2 | // See update-tlds.js for encoding/decoding format
3 | // https://data.iana.org/TLD/tlds-alpha-by-domain.txt
4 | export const encodedTlds = 'aaa1rp3bb0ott3vie4c1le2ogado5udhabi7c0ademy5centure6ountant0s9o1tor4d0s1ult4e0g1ro2tna4f0l1rica5g0akhan5ency5i0g1rbus3force5tel5kdn3l0ibaba4pay4lfinanz6state5y2sace3tom5m0azon4ericanexpress7family11x2fam3ica3sterdam8nalytics7droid5quan4z2o0l2partments8p0le4q0uarelle8r0ab1mco4chi3my2pa2t0e3s0da2ia2sociates9t0hleta5torney7u0ction5di0ble3o3spost5thor3o0s4w0s2x0a2z0ure5ba0by2idu3namex4d1k2r0celona5laycard4s5efoot5gains6seball5ketball8uhaus5yern5b0c1t1va3cg1n2d1e0ats2uty4er2rlin4st0buy5t2f1g1h0arti5i0ble3d1ke2ng0o3o1z2j1lack0friday9ockbuster8g1omberg7ue3m0s1w2n0pparibas9o0ats3ehringer8fa2m1nd2o0k0ing5sch2tik2on4t1utique6x2r0adesco6idgestone9oadway5ker3ther5ussels7s1t1uild0ers6siness6y1zz3v1w1y1z0h3ca0b1fe2l0l1vinklein9m0era3p2non3petown5ital0one8r0avan4ds2e0er0s4s2sa1e1h1ino4t0ering5holic7ba1n1re3c1d1enter4o1rn3f0a1d2g1h0anel2nel4rity4se2t2eap3intai5ristmas6ome4urch5i0priani6rcle4sco3tadel4i0c2y3k1l0aims4eaning6ick2nic1que6othing5ud3ub0med6m1n1o0ach3des3ffee4llege4ogne5m0mbank4unity6pany2re3uter5sec4ndos3struction8ulting7tact3ractors9oking4l1p2rsica5untry4pon0s4rses6pa2r0edit0card4union9icket5own3s1uise0s6u0isinella9v1w1x1y0mru3ou3z2dad1nce3ta1e1ing3sun4y2clk3ds2e0al0er2s3gree4livery5l1oitte5ta3mocrat6ntal2ist5si0gn4v2hl2iamonds6et2gital5rect0ory7scount3ver5h2y2j1k1m1np2o0cs1tor4g1mains5t1wnload7rive4tv2ubai3nlop4pont4rban5vag2r2z2earth3t2c0o2deka3u0cation8e1g1mail3erck5nergy4gineer0ing9terprises10pson4quipment8r0icsson6ni3s0q1tate5t1u0rovision8s2vents5xchange6pert3osed4ress5traspace10fage2il1rwinds6th3mily4n0s2rm0ers5shion4t3edex3edback6rrari3ero6i0delity5o2lm2nal1nce1ial7re0stone6mdale6sh0ing5t0ness6j1k1lickr3ghts4r2orist4wers5y2m1o0o0d1tball6rd1ex2sale4um3undation8x2r0ee1senius7l1ogans4ntier7tr2ujitsu5n0d2rniture7tbol5yi3ga0l0lery3o1up4me0s3p1rden4y2b0iz3d0n2e0a1nt0ing5orge5f1g0ee3h1i0ft0s3ves2ing5l0ass3e1obal2o4m0ail3bh2o1x2n1odaddy5ld0point6f2o0dyear5g0le4p1t1v2p1q1r0ainger5phics5tis4een3ipe3ocery4up4s1t1u0cci3ge2ide2tars5ru3w1y2hair2mburg5ngout5us3bo2dfc0bank7ealth0care8lp1sinki6re1mes5iphop4samitsu7tachi5v2k0t2m1n1ockey4ldings5iday5medepot5goods5s0ense7nda3rse3spital5t0ing5t0els3mail5use3w2r1sbc3t1u0ghes5yatt3undai7ibm2cbc2e1u2d1e0ee3fm2kano4l1m0amat4db2mo0bilien9n0c1dustries8finiti5o2g1k1stitute6urance4e4t0ernational10uit4vestments10o1piranga7q1r0ish4s0maili5t0anbul7t0au2v3jaguar4va3cb2e0ep2tzt3welry6io2ll2m0p2nj2o0bs1urg4t1y2p0morgan6rs3uegos4niper7kaufen5ddi3e0rryhotels6properties14fh2g1h1i0a1ds2m1ndle4tchen5wi3m1n1oeln3matsu5sher5p0mg2n2r0d1ed3uokgroup8w1y0oto4z2la0caixa5mborghini8er3nd0rover6xess5salle5t0ino3robe5w0yer5b1c1ds2ease3clerc5frak4gal2o2xus4gbt3i0dl2fe0insurance9style7ghting6ke2lly3mited4o2ncoln4k2ve1ing5k1lc1p2oan0s3cker3us3l1ndon4tte1o3ve3pl0financial11r1s1t0d0a3u0ndbeck6xe1ury5v1y2ma0drid4if1son4keup4n0agement7go3p1rket0ing3s4riott5shalls7ttel5ba2c0kinsey7d1e0d0ia3et2lbourne7me1orial6n0u2rckmsd7g1h1iami3crosoft7l1ni1t2t0subishi9k1l0b1s2m0a2n1o0bi0le4da2e1i1m1nash3ey2ster5rmon3tgage6scow4to0rcycles9v0ie4p1q1r1s0d2t0n1r2u0seum3ic4v1w1x1y1z2na0b1goya4me2vy3ba2c1e0c1t0bank4flix4work5ustar5w0s2xt0direct7us4f0l2g0o2hk2i0co2ke1on3nja3ssan1y5l1o0kia3rton4w0ruz3tv4p1r0a1w2tt2u1yc2z2obi1server7ffice5kinawa6layan0group9lo3m0ega4ne1g1l0ine5oo2pen3racle3nge4g0anic5igins6saka4tsuka4t2vh3pa0ge2nasonic7ris2s1tners4s1y3y2ccw3e0t2f0izer5g1h0armacy6d1ilips5one2to0graphy6s4ysio5ics1tet2ures6d1n0g1k2oneer5zza4k1l0ace2y0station9umbing5s3m1n0c2ohl2ker3litie5rn2st3r0america6xi3ess3ime3o0d0uctions8f1gressive8mo2perties3y5tection8u0dential9s1t1ub2w0c2y2qa1pon3uebec3st5racing4dio4e0ad1lestate6tor2y4cipes5d0stone5umbrella9hab3ise0n3t2liance6n0t0als5pair3ort3ublican8st0aurant8view0s5xroth6ich0ardli6oh3l1o1p2o0cks3deo3gers4om3s0vp3u0gby3hr2n2w0e2yukyu6sa0arland6fe0ty4kura4le1on3msclub4ung5ndvik0coromant12ofi4p1rl2s1ve2xo3b0i1s2c0b1haeffler7midt4olarships8ol3ule3warz5ience5ot3d1e0arch3t2cure1ity6ek2lect4ner3rvices6ven3w1x0y3fr2g1h0angrila6rp3ell3ia1ksha5oes2p0ping5uji3w3i0lk2na1gles5te3j1k0i0n2y0pe4l0ing4m0art3ile4n0cf3o0ccer3ial4ftbank4ware6hu2lar2utions7ng1y2y2pa0ce3ort2t3r0l2s1t0ada2ples4r1tebank4farm7c0group6ockholm6rage3e3ream4udio2y3yle4u0cks3pplies3y2ort5rf1gery5zuki5v1watch4iss4x1y0dney4stems6z2tab1ipei4lk2obao4rget4tamotors6r2too4x0i3c0i2d0k2eam2ch0nology8l1masek5nnis4va3f1g1h0d1eater2re6iaa2ckets5enda4ps2res2ol4j0maxx4x2k0maxx5l1m0all4n1o0day3kyo3ols3p1ray3shiba5tal3urs3wn2yota3s3r0ade1ing4ining5vel0ers0insurance16ust3v2t1ube2i1nes3shu4v0s2w1z2ua1bank3s2g1k1nicom3versity8o2ol2ps2s1y1z2va0cations7na1guard7c1e0gas3ntures6risign5mögensberater2ung14sicherung10t2g1i0ajes4deo3g1king4llas4n1p1rgin4sa1ion4va1o3laanderen9n1odka3lvo3te1ing3o2yage5u2wales2mart4ter4ng0gou5tch0es6eather0channel12bcam3er2site5d0ding5ibo2r3f1hoswho6ien2ki2lliamhill9n0dows4e1ners6me2olterskluwer11odside6rk0s2ld3w2s1tc1f3xbox3erox4ihuan4n2xx2yz3yachts4hoo3maxun5ndex5e1odobashi7ga2kohama6u0tube6t1un3za0ppos4ra3ero3ip2m1one3uerich6w2';
5 | // Internationalized domain names containing non-ASCII
6 | export const encodedUtlds = 'ελ1υ2бг1ел3дети4ею2католик6ом3мкд2он1сква6онлайн5рг3рус2ф2сайт3рб3укр3қаз3հայ3ישראל5קום3ابوظبي5رامكو5لاردن4بحرين5جزائر5سعودية6عليان5مغرب5مارات5یران5بارت2زار4يتك3ھارت5تونس4سودان3رية5شبكة4عراق2ب2مان4فلسطين6قطر3كاثوليك6وم3مصر2ليسيا5وريتانيا7قع4همراه5پاکستان7ڀارت4कॉम3नेट3भारत0म्3ोत5संगठन5বাংলা5ভারত2ৰত4ਭਾਰਤ4ભારત4ଭାରତ4இந்தியா6லங்கை6சிங்கப்பூர்11భారత్5ಭಾರತ4ഭാരതം5ලංකා4คอม3ไทย3ລາວ3გე2みんな3アマゾン4クラウド4グーグル4コム2ストア3セール3ファッション6ポイント4世界2中信1国1國1文网3亚马逊3企业2佛山2信息2健康2八卦2公司1益2台湾1灣2商城1店1标2嘉里0大酒店5在线2大拿2天主教3娱乐2家電2广东2微博2慈善2我爱你3手机2招聘2政务1府2新加坡2闻2时尚2書籍2机构2淡马锡3游戏2澳門2点看2移动2组织机构4网址1店1站1络2联通2谷歌2购物2通販2集团2電訊盈科4飞利浦3食品2餐厅2香格里拉3港2닷넷1컴2삼성2한국2';
7 |
--------------------------------------------------------------------------------
/packages/linkifyjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["dist/linkify.cjs", "dist/linkify.mjs"],
3 | "exclude": [],
4 | "compilerOptions": {
5 | "allowJs": true,
6 | // "checkJs": true,
7 | "declaration": true,
8 | "emitDeclarationOnly": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 | import resolve from '@rollup/plugin-node-resolve';
3 | import commonjs from '@rollup/plugin-commonjs';
4 | import babel from '@rollup/plugin-babel';
5 | import replace from '@rollup/plugin-replace';
6 |
7 | export const plugins = [resolve({ browser: true }), babel({ babelHelpers: 'bundled' })];
8 |
9 | // For interfaces in their dedicated packages
10 | export function linkifyInterface(name, opts = {}) {
11 | const iifeOpts = { name };
12 | const globals = { linkifyjs: 'linkify' };
13 | const external = ['linkifyjs'];
14 | if ('globalName' in opts) {
15 | iifeOpts.name = opts.globalName;
16 | }
17 | if ('globals' in opts) {
18 | Object.assign(globals, opts.globals);
19 | }
20 | if ('external' in opts) {
21 | external.push(...opts.external);
22 | }
23 |
24 | return {
25 | input: `src/linkify-${name}.mjs`,
26 | external,
27 | output: [
28 | { file: `dist/linkify-${name}.js`, format: 'iife', globals, ...iifeOpts },
29 | { file: `dist/linkify-${name}.min.js`, format: 'iife', globals, ...iifeOpts, plugins: [terser()] },
30 | { file: `dist/linkify-${name}.cjs`, format: 'cjs', exports: 'auto' },
31 | { file: `dist/linkify-${name}.mjs`, format: 'es' },
32 | ],
33 | plugins,
34 | };
35 | }
36 |
37 | // Includes plugins from main linkifyjs package because those have not yet been
38 | // fully migrated to their own packages to maintain backward compatibility with
39 | // v2. Will change in v4
40 | export function linkifyPlugin(plugin, opts = {}) {
41 | const name = opts.globalName || false; // Most plugins don't export anything
42 | const globals = { linkifyjs: 'linkify' };
43 | return {
44 | input: 'src/index.mjs',
45 | external: ['linkifyjs'],
46 | output: [
47 | { file: `dist/linkify-plugin-${plugin}.js`, format: 'iife', globals, name },
48 | { file: `dist/linkify-plugin-${plugin}.min.js`, format: 'iife', globals, name, plugins: [terser()] },
49 | { file: `dist/linkify-plugin-${plugin}.cjs`, format: 'cjs', exports: 'auto' },
50 | { file: `dist/linkify-plugin-${plugin}.mjs`, format: 'es' },
51 | ],
52 | plugins,
53 | };
54 | }
55 |
56 | // Build react globals for qunit tests
57 | export default [
58 | {
59 | input: 'test/react.mjs',
60 | output: [
61 | {
62 | file: 'test/qunit/vendor/react.min.js',
63 | name: 'React',
64 | format: 'iife',
65 | plugins: [terser()],
66 | },
67 | ],
68 | plugins: plugins.concat([
69 | replace({ 'process.env.NODE_ENV': '"production"', preventAssignment: true }),
70 | commonjs(),
71 | ]),
72 | },
73 | {
74 | input: 'test/react-dom.mjs',
75 | output: [
76 | {
77 | file: 'test/qunit/vendor/react-dom.min.js',
78 | name: 'ReactDOM',
79 | globals: { react: 'React' },
80 | format: 'iife',
81 | plugins: [terser()],
82 | },
83 | ],
84 | plugins: plugins.concat([
85 | replace({ 'process.env.NODE_ENV': '"production"', preventAssignment: true }),
86 | commonjs(),
87 | ]),
88 | },
89 | ];
90 |
--------------------------------------------------------------------------------
/tasks/update-tlds.cjs:
--------------------------------------------------------------------------------
1 | const http = require('https'); // or 'https' for https:// URLs
2 | const fs = require('fs');
3 | const punycode = require('punycode/');
4 |
5 | const tldsListUrl = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt';
6 | const tldsjs = 'packages/linkifyjs/src/tlds.mjs';
7 | let tldsListContents = '';
8 |
9 | /**
10 | * Given a list of TLDs, encodes into a compact string that may be decoded
11 | * with decodeTlds. Works best when input is sorted alphabetically
12 | *
13 | * Example input: ['cat', 'cats', 'car', 'cars']
14 | * Example output: 'cat0s2r0s4'
15 | */
16 | function encodeTlds(tlds) {
17 | return encodeTrie(createTrie(tlds));
18 | }
19 |
20 | /**
21 | * Given a list of words, encodes into an object-based trie.
22 | *
23 | * Example input: ['cat', 'cats', 'car', 'cars']
24 | * Example output: {
25 | * c: {
26 | * a: {
27 | * t: {
28 | * isWord: true,
29 | * s: { isWord: true }
30 | * },
31 | * r: {
32 | * isWord: true,
33 | * s: { isWord: true }
34 | * }
35 | * }
36 | * }
37 | * }
38 | */
39 | function createTrie(words) {
40 | const root = {};
41 | for (const word of words) {
42 | let current = root;
43 | for (const letter of word) {
44 | if (!(letter in current)) {
45 | current[letter] = {};
46 | }
47 | current = current[letter];
48 | }
49 | current.isWord = true;
50 | }
51 | return root;
52 | }
53 |
54 | /**
55 | * Given an object trie created by `createTrie`, encodes into a compact string
56 | * that can later be decoded back into a list of strings.
57 | *
58 | * Using the example trie above, output would be: 'cat0s2r0s4'
59 | *
60 | * NOTE: Does not work if trie contains worlds with digits 0-9
61 | */
62 | function encodeTrie(trie) {
63 | return encodeTrieHelper(trie).join('');
64 | }
65 |
66 | function encodeTrieHelper(trie) {
67 | const output = [];
68 | for (const k in trie) {
69 | if (k === 'isWord') {
70 | output.push(0); // Zero means previous steps into trie make a word
71 | continue;
72 | }
73 | output.push(k); // Push child node means drop down a level into the trie
74 | output.push(...encodeTrieHelper(trie[k]));
75 | // increment the number of times we have to go back up to get to this
76 | // level of the trie.
77 | if (typeof output[output.length - 1] === 'number') {
78 | output[output.length - 1] += 1;
79 | } else {
80 | output.push(1);
81 | }
82 | }
83 | return output;
84 | }
85 |
86 | /**
87 | * Converts a string of Top-Level Domain names back into a list of strings.
88 | *
89 | * Example input: 'cat0s2r0s4'
90 | * Example output: ['cat', 'cats', 'car', 'cars']
91 | */
92 | function decodeTlds(encoded) {
93 | const words = [];
94 | const stack = [];
95 | let i = 0;
96 | let digits = '0123456789';
97 | while (i < encoded.length) {
98 | let popDigitCount = 0;
99 | while (digits.indexOf(encoded[i + popDigitCount]) >= 0) {
100 | popDigitCount++; // encountered some digits, have to pop to go one level up trie
101 | }
102 | if (popDigitCount > 0) {
103 | words.push(stack.join('')); // whatever preceded the pop digits must be a word
104 | for (let popCount = parseInt(encoded.substring(i, i + popDigitCount), 10); popCount > 0; popCount--) {
105 | stack.pop();
106 | }
107 | i += popDigitCount;
108 | } else {
109 | stack.push(encoded[i]); // drop down a level into the trie
110 | i++;
111 | }
112 | }
113 | return words;
114 | }
115 |
116 | http.get(tldsListUrl, (response) => {
117 | console.log(`Downloading ${tldsListUrl}...`);
118 | response.on('data', (chunk) => {
119 | tldsListContents += chunk;
120 | });
121 | response.on('end', () => {
122 | console.log(`Downloaded. Re-generating ${tldsjs}...`);
123 |
124 | // NOTE: punycode versions of IDNs (e.g., `XN--...`) do not get included
125 | // in the TLDs list because these will not be as commonly used without
126 | // the http prefix anyway and linkify will already force-encode those.
127 | let tlds = [];
128 | let utlds = [];
129 |
130 | // NOTE: vermögensberater vermögensberatung are special cases because
131 | // they're the only ones that contain a mix of ASCII and non-ASCII
132 | // characters.
133 | const specialTlds = ['XN--VERMGENSBERATER-CTB', 'XN--VERMGENSBERATUNG-PWB'];
134 | const specialUtlds = specialTlds.map((tld) => punycode.toUnicode(tld.toLowerCase()));
135 |
136 | for (const line of tldsListContents.split('\n').map((line) => line.trim())) {
137 | if (!line || line[0] === '#' || specialTlds.includes(line)) {
138 | continue;
139 | }
140 | if (/^XN--/.test(line)) {
141 | utlds.push(punycode.toUnicode(line.toLowerCase()));
142 | } else {
143 | tlds.push(line.toLowerCase());
144 | }
145 | }
146 | tlds = tlds.concat(specialUtlds).sort();
147 | utlds = utlds.sort();
148 |
149 | console.log('Encoding...');
150 | const encodedTlds = encodeTlds(tlds);
151 | const encodedUtlds = encodeTlds(utlds);
152 |
153 | console.log('Testing decode...');
154 | const decodedTlds = decodeTlds(encodedTlds);
155 | console.assert(JSON.stringify(decodedTlds) === JSON.stringify(tlds), 'Invalid encode/decode routine');
156 |
157 | const jsFile = fs.openSync(tldsjs, 'w');
158 | fs.writeSync(jsFile, '// THIS FILE IS AUTOMATICALLY GENERATED DO NOT EDIT DIRECTLY\n');
159 | fs.writeSync(jsFile, '// See update-tlds.js for encoding/decoding format\n');
160 | fs.writeSync(jsFile, `// ${tldsListUrl}\n`);
161 |
162 | // Write TLDs
163 | fs.writeSync(jsFile, "export const encodedTlds = '");
164 | fs.writeSync(jsFile, encodedTlds);
165 | fs.writeSync(jsFile, "';\n");
166 | fs.writeSync(jsFile, '// Internationalized domain names containing non-ASCII\n');
167 | fs.writeSync(jsFile, "export const encodedUtlds = '");
168 | fs.writeSync(jsFile, encodedUtlds);
169 | fs.writeSync(jsFile, "';\n");
170 | fs.closeSync(jsFile);
171 |
172 | console.log('Done');
173 | });
174 | });
175 |
--------------------------------------------------------------------------------
/test/chrome.conf.cjs:
--------------------------------------------------------------------------------
1 | // Karma Chrome configuration
2 | // Just opens Google Chrome for testing
3 |
4 | const base = require('./conf.cjs');
5 |
6 | module.exports = function (config) {
7 | config.set({
8 | ...base,
9 |
10 | // level of logging
11 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
12 | logLevel: config.LOG_INFO,
13 | browsers: ['Chrome'],
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/test/ci1.conf.cjs:
--------------------------------------------------------------------------------
1 | // Karma CI configuration (1/2)
2 | // The CIs are split up to prevent too many parellel launchers
3 | const base = require('./conf.cjs');
4 |
5 | module.exports = function (config) {
6 | // https://www.browserstack.com/docs/automate/api-reference/selenium/introduction#rest-api-browsers
7 | const customLaunchers = {
8 | bs_chrome_mac: {
9 | base: 'BrowserStack',
10 | browser: 'chrome',
11 | os: 'OS X',
12 | os_version: 'Ventura',
13 | },
14 | bs_chrome_windows: {
15 | base: 'BrowserStack',
16 | browser: 'chrome',
17 | os: 'Windows',
18 | os_version: '10',
19 | },
20 | bs_firefox_windows: {
21 | base: 'BrowserStack',
22 | browser: 'firefox',
23 | os: 'Windows',
24 | os_version: '10',
25 | },
26 | bs_android_8: {
27 | base: 'BrowserStack',
28 | os: 'android',
29 | os_version: '9.0',
30 | browser: 'android',
31 | device: 'Google Pixel 3',
32 | },
33 | bs_android_11: {
34 | base: 'BrowserStack',
35 | os: 'android',
36 | os_version: '11.0',
37 | browser: 'android',
38 | device: 'Google Pixel 5',
39 | },
40 | };
41 |
42 | config.set({
43 | ...base,
44 |
45 | // level of logging
46 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
47 | logLevel: config.LOG_WARN,
48 |
49 | browserStack: {
50 | project: 'linkifyjs',
51 | username: process.env.BROWSERSTACK_USERNAME,
52 | accessKey: process.env.BROWSERSTACK_ACCESS_KEY,
53 | name: process.env.GITHUB_WORKFLOW,
54 | build: process.env.GITHUB_RUN_NUMBER,
55 | },
56 |
57 | customLaunchers,
58 | browsers: Object.keys(customLaunchers),
59 | singleRun: true,
60 | reporters: ['dots', 'BrowserStack'],
61 | });
62 | };
63 |
--------------------------------------------------------------------------------
/test/ci2.conf.cjs:
--------------------------------------------------------------------------------
1 | // Karma CI configuration (2/2)
2 | // The CIs are split up to prevent too many parellel launchers
3 | const base = require('./conf.cjs');
4 |
5 | module.exports = function (config) {
6 | // https://www.browserstack.com/docs/automate/api-reference/selenium/introduction#rest-api-browsers
7 | const customLaunchers = {
8 | bs_safari_sierra: {
9 | base: 'BrowserStack',
10 | browser: 'safari',
11 | os: 'OS X',
12 | os_version: 'Monterey',
13 | },
14 | bs_safari_bigsur: {
15 | base: 'BrowserStack',
16 | browser: 'safari',
17 | os: 'OS X',
18 | os_version: 'Sonoma',
19 | },
20 | bs_ios_safari: {
21 | base: 'BrowserStack',
22 | browser: 'iphone',
23 | os: 'ios',
24 | os_version: '16',
25 | device: 'iPhone 14',
26 | },
27 | bs_edge: {
28 | base: 'BrowserStack',
29 | browser: 'edge',
30 | os: 'Windows',
31 | os_version: '11',
32 | },
33 | };
34 |
35 | config.set({
36 | ...base,
37 |
38 | // level of logging
39 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
40 | logLevel: config.LOG_WARN,
41 |
42 | browserStack: {
43 | project: 'linkifyjs',
44 | username: process.env.BROWSERSTACK_USERNAME,
45 | accessKey: process.env.BROWSERSTACK_ACCESS_KEY,
46 | name: process.env.GITHUB_WORKFLOW,
47 | build: process.env.GITHUB_RUN_NUMBER,
48 | },
49 |
50 | customLaunchers,
51 | browsers: Object.keys(customLaunchers),
52 | singleRun: true,
53 | reporters: ['dots', 'BrowserStack'],
54 | });
55 | };
56 |
--------------------------------------------------------------------------------
/test/conf.cjs:
--------------------------------------------------------------------------------
1 | // const fs = require('fs');
2 |
3 | // React path may vary depending on version
4 | // const reactPath = fs.existsSync('node_modules/react/dist/react.min.js') ? 'dist/react' : 'umd/react.production';
5 | // const reactDomPath = fs.existsSync('node_modules/react-dom/dist/react-dom.min.js')
6 | // ? 'dist/react-dom'
7 | // : 'umd/react-dom.production';
8 |
9 | module.exports = {
10 | // base path that will be used to resolve all patterns (eg. files, exclude)
11 | basePath: __dirname.replace(/\/?test\/?$/, '/'),
12 |
13 | // frameworks to use
14 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
15 | frameworks: ['qunit'],
16 |
17 | // list of files / patterns to load in the browser
18 | files: [
19 | { pattern: 'node_modules/jquery/dist/jquery.js', watched: false },
20 | 'test/qunit/vendor/react.min.js',
21 | 'test/qunit/vendor/react-dom.min.js',
22 | 'dist/linkify.min.js',
23 | // 'dist/linkify.js', // Uncompressed
24 | 'dist/*.min.js',
25 | 'test/qunit/globals.js',
26 | 'test/qunit/main.js',
27 | ],
28 |
29 | // QUnit configuration
30 | client: {
31 | clearContext: false,
32 | qunit: {
33 | showUI: true,
34 | },
35 | },
36 |
37 | // preprocess matching files before serving them to the browser
38 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
39 | preprocessors: {},
40 |
41 | // test results reporter to use
42 | // possible values: 'dots', 'progress'
43 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
44 | reporters: ['dots'],
45 |
46 | // web server port
47 | port: 9876,
48 |
49 | // enable / disable colors in the output (reporters and logs)
50 | colors: true,
51 |
52 | // enable / disable watching file and executing tests whenever any file changes
53 | autoWatch: true,
54 |
55 | // Continuous Integration mode
56 | // if true, Karma captures browsers, runs the tests and exits
57 | singleRun: false,
58 | };
59 |
--------------------------------------------------------------------------------
/test/firefox.conf.cjs:
--------------------------------------------------------------------------------
1 | // Karma Chrome configuration
2 | // Just opens Google Chrome for testing
3 |
4 | const base = require('./conf.cjs');
5 |
6 | module.exports = function (config) {
7 | config.set({
8 | ...base,
9 |
10 | // level of logging
11 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
12 | logLevel: config.LOG_INFO,
13 | browsers: ['Firefox'],
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/test/qunit/globals.js:
--------------------------------------------------------------------------------
1 | this.w = window;
2 |
--------------------------------------------------------------------------------
/test/react-dom.mjs:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 | export default ReactDOM;
3 |
--------------------------------------------------------------------------------
/test/react.mjs:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export default React;
3 |
--------------------------------------------------------------------------------
/test/setup.mjs:
--------------------------------------------------------------------------------
1 | import * as linkify from 'linkifyjs/src/linkify.mjs';
2 |
3 | /**
4 | Gracefully truncate a string to a given limit. Will replace extraneous
5 | text with a single ellipsis character (`…`).
6 | */
7 | String.prototype.truncate = function (limit) {
8 | limit = limit || Infinity;
9 | return this.length > limit ? this.substring(0, limit) + '…' : this;
10 | };
11 |
12 | // eslint-disable-next-line mocha/no-top-level-hooks
13 | beforeEach(() => {
14 | linkify.reset();
15 | });
16 |
--------------------------------------------------------------------------------
/test/spec/html/extra.html:
--------------------------------------------------------------------------------
1 | Have a link to:
2 | github.com!
Another test@gmail.com email as well as a http://t.co link.
3 |
--------------------------------------------------------------------------------
/test/spec/html/linkified-alt.html:
--------------------------------------------------------------------------------
1 | Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right? Here is a nested github.com/Hypercontext/linkifyjs paragraph
and another link to www.google.com and a
2 | Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right? Here is a nested github.com/Hypercontext/linkifyjs paragraph
and another link to www.google.com and a
3 | Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right? Here is a nested github.com/Hypercontext/linkifyjs paragraph
and another link to www.google.com and a
4 | Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right? Here is a nested github.com/Hypercontext/linkifyjs paragraph
and another link to www.google.com and a
5 |
--------------------------------------------------------------------------------
/test/spec/html/linkified-validate.html:
--------------------------------------------------------------------------------
1 | Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right? Here is a nested github.com/Hypercontext/linkifyjs paragraph
and another link to www.google.com and a
2 |
--------------------------------------------------------------------------------
/test/spec/html/linkified.html:
--------------------------------------------------------------------------------
1 | Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right? Here is a nested github.com/Hypercontext/linkifyjs paragraph
and another link to www.google.com and a
2 |
--------------------------------------------------------------------------------
/test/spec/html/options.mjs:
--------------------------------------------------------------------------------
1 | // HTML to use with linkify-element and linkify-jquery
2 | import fs from 'fs';
3 | export default {
4 | original: fs.readFileSync('test/spec/html/original.html', 'utf8').trim(),
5 |
6 | // These are split into arrays by line, where each line represents a
7 | // different attribute ordering (based on the rendering engine)
8 | // Each line is semantically identical.
9 | linkified: fs.readFileSync('test/spec/html/linkified.html', 'utf8').trim().split('\n'),
10 | linkifiedAlt: fs.readFileSync('test/spec/html/linkified-alt.html', 'utf8').trim().split('\n'),
11 | linkifiedValidate: fs.readFileSync('test/spec/html/linkified-validate.html', 'utf8').trim().split('\n'),
12 |
13 | extra: fs.readFileSync('test/spec/html/extra.html', 'utf8').trim(), // for jQuery plugin tests
14 | email: fs.readFileSync('test/spec/html/email.html', 'utf8').trim(), // for linkify-html performance tests
15 | altOptions: {
16 | className: 'linkified',
17 | rel: 'nofollow',
18 | target: '_blank',
19 | attributes: {
20 | type: 'text/html',
21 | },
22 | events: {
23 | click: function () {
24 | throw 'Clicked!';
25 | },
26 | mouseover: function () {
27 | throw 'Hovered!';
28 | },
29 | },
30 | ignoreTags: ['script', 'style'],
31 | },
32 |
33 | validateOptions: {
34 | validate: {
35 | url: function (text) {
36 | return /^(http|ftp)s?:\/\//.test(text) || text.slice(0, 3) === 'www';
37 | },
38 | },
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/test/spec/html/original.html:
--------------------------------------------------------------------------------
1 | Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right? Here is a nested github.com/Hypercontext/linkifyjs paragraph
and another link to www.google.com and a
2 |
--------------------------------------------------------------------------------
/test/spec/linkify-element.test.mjs:
--------------------------------------------------------------------------------
1 | import linkifyElement from 'linkify-element/src/linkify-element.mjs';
2 | import htmlOptions from './html/options.mjs';
3 | import { expect } from 'chai';
4 |
5 | let doc, testContainer, JSDOM;
6 | try {
7 | doc = document;
8 | } catch (e) {
9 | doc = null;
10 | }
11 |
12 | if (!doc) {
13 | const jsdom = await import('jsdom');
14 | JSDOM = jsdom.JSDOM;
15 | }
16 |
17 | describe('linkify-element', () => {
18 | /**
19 | Set up the JavaScript document and the element for it
20 | This code allows testing on Node.js and on Browser environments
21 | */
22 | before(function (done) {
23 | function onDoc(doc) {
24 | testContainer = doc.createElement('div');
25 | testContainer.id = 'linkify-element-test-container';
26 | doc.body.appendChild(testContainer);
27 | done();
28 | }
29 |
30 | if (doc) {
31 | return onDoc(doc);
32 | }
33 |
34 | const dom = new JSDOM('Linkify Test');
35 | doc = dom.window.document;
36 | onDoc(dom.window.document);
37 | });
38 |
39 | beforeEach(() => {
40 | // Make sure we start out with a fresh DOM every time
41 | testContainer.innerHTML = htmlOptions.original;
42 | });
43 |
44 | it('Has a helper function', () => {
45 | expect(linkifyElement.helper).to.be.a('function');
46 | });
47 |
48 | it('Works with default options', () => {
49 | var result = linkifyElement(testContainer, null, doc);
50 | expect(result).to.equal(testContainer); // should return the same element
51 | expect(testContainer.innerHTML).to.be.oneOf(htmlOptions.linkified);
52 | });
53 |
54 | it('Works with overriden options (general)', () => {
55 | var result = linkifyElement(testContainer, htmlOptions.altOptions, doc);
56 | expect(result).to.equal(testContainer); // should return the same element
57 | expect(testContainer.innerHTML).to.be.oneOf(htmlOptions.linkifiedAlt);
58 | });
59 |
60 | it('Works with overriden options (validate)', () => {
61 | var result = linkifyElement(testContainer, htmlOptions.validateOptions, doc);
62 | expect(result).to.equal(testContainer); // should return the same element
63 | expect(testContainer.innerHTML).to.be.oneOf(htmlOptions.linkifiedValidate);
64 | });
65 |
66 | it('Works when there is an empty text nodes', () => {
67 | testContainer.appendChild(doc.createTextNode(''));
68 | var result = linkifyElement(testContainer, null, doc);
69 | expect(result).to.equal(testContainer); // should return the same element
70 | expect(testContainer.innerHTML).to.be.oneOf(htmlOptions.linkified);
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/test/spec/linkify-jquery.test.mjs:
--------------------------------------------------------------------------------
1 | import applyLinkify from 'linkify-jquery/src/linkify-jquery.mjs';
2 | import htmlOptions from './html/options.mjs';
3 | import { expect } from 'chai';
4 | let $, doc, testContainer, JSDOM;
5 |
6 | try {
7 | doc = document;
8 | $ = require('jquery'); // should be available through Browserify
9 | } catch (e) {
10 | doc = null;
11 | $ = null;
12 | }
13 |
14 | if (!doc) {
15 | const jsdom = await import('jsdom');
16 | JSDOM = jsdom.JSDOM;
17 | }
18 |
19 | describe('linkify-jquery', function () {
20 | // Sometimes jQuery is slow to load
21 | this.timeout(10000);
22 |
23 | /**
24 | Set up the JavaScript document and the element for it
25 | This code allows testing on Node.js and on Browser environments
26 | */
27 | before(function (done) {
28 | function onDoc($, doc) {
29 | doc.body.innerHTML = htmlOptions.extra;
30 |
31 | // Add the linkify plugin to jQuery
32 | applyLinkify($, doc);
33 | $(doc).trigger('ready');
34 |
35 | testContainer = doc.createElement('div');
36 | testContainer.id = 'linkify-jquery-test-container';
37 |
38 | doc.body.appendChild(testContainer);
39 | done();
40 | }
41 |
42 | if (doc) {
43 | return onDoc($, doc);
44 | }
45 | // no document element, use a virtual dom to test
46 |
47 | let dom = new JSDOM(
48 | 'Linkify Test',
49 | {
50 | runScripts: 'dangerously',
51 | resources: 'usable',
52 | },
53 | );
54 | doc = dom.window.document;
55 | dom.window.onload = () => {
56 | $ = dom.window.jQuery;
57 | onDoc($, doc);
58 | };
59 | });
60 |
61 | // Make sure we start out with a fresh DOM every time
62 | beforeEach(() => (testContainer.innerHTML = htmlOptions.original));
63 |
64 | it('Works with the DOM Data API', () => {
65 | expect($('header').first().html()).to.be.eql('Have a link to:
github.com!');
66 | expect($('#linkify-test-div').html()).to.be.eql(
67 | 'Another test@gmail.com email as well as a ' +
70 | 'http://t.co link.',
71 | );
72 | });
73 |
74 | it('Works with default options', () => {
75 | var $container = $('#linkify-jquery-test-container');
76 | expect($container.length).to.be.eql(1);
77 | var result = $container.linkify();
78 | // `should` is not defined on jQuery objects
79 | expect(result === $container).to.be.ok; // should return the same element
80 | expect($container.html()).to.be.oneOf(htmlOptions.linkified);
81 | });
82 |
83 | it('Works with overriden options (general)', () => {
84 | var $container = $('#linkify-jquery-test-container');
85 | expect($container.length).to.be.eql(1);
86 | var result = $container.linkify(htmlOptions.altOptions);
87 | // `should` is not defined on jQuery objects
88 | expect(result === $container).to.be.ok; // should return the same element
89 | expect($container.html()).to.be.oneOf(htmlOptions.linkifiedAlt);
90 | });
91 |
92 | it('Works with overriden options (validate)', () => {
93 | var $container = $('#linkify-jquery-test-container');
94 | expect($container.length).to.be.eql(1);
95 | var result = $container.linkify(htmlOptions.validateOptions);
96 | // `should` is not defined on jQuery objects
97 | expect(result === $container).to.be.ok; // should return the same element
98 | expect($container.html()).to.be.oneOf(htmlOptions.linkifiedValidate);
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/test/spec/linkify-plugin-hashtag.test.mjs:
--------------------------------------------------------------------------------
1 | import * as linkify from 'linkifyjs/src/linkify.mjs';
2 | import hashtag from 'linkify-plugin-hashtag/src/hashtag.mjs';
3 | import { expect } from 'chai';
4 |
5 | describe('linkify-plugin-hashtag', () => {
6 | beforeEach(() => {
7 | linkify.reset();
8 | });
9 |
10 | it('cannot parse hashtags before applying the plugin', () => {
11 | expect(linkify.find('There is a #hashtag #YOLO-2015 and #1234 and #%^&*( should not work')).to.be.eql([]);
12 |
13 | expect(linkify.test('#wat', 'hashtag')).to.not.be.ok;
14 | expect(linkify.test('#987', 'hashtag')).to.not.be.ok;
15 | });
16 |
17 | describe('after plugin is applied', () => {
18 | beforeEach(() => {
19 | linkify.registerPlugin('hashtag', hashtag);
20 | });
21 |
22 | it('can parse hashtags after applying the plugin', () => {
23 | expect(
24 | linkify.find('There is a #hashtag 💃#YOLO_2015 #__swag__ and #1234 and #%^&*( #_ #__ should not work'),
25 | ).to.be.eql([
26 | {
27 | type: 'hashtag',
28 | value: '#hashtag',
29 | href: '#hashtag',
30 | isLink: true,
31 | start: 11,
32 | end: 19,
33 | },
34 | {
35 | type: 'hashtag',
36 | value: '#YOLO_2015',
37 | href: '#YOLO_2015',
38 | isLink: true,
39 | start: 22,
40 | end: 32,
41 | },
42 | {
43 | type: 'hashtag',
44 | value: '#__swag__',
45 | href: '#__swag__',
46 | isLink: true,
47 | start: 33,
48 | end: 42,
49 | },
50 | ]);
51 | });
52 |
53 | it('Works with basic hashtags', () => {
54 | expect(linkify.test('#wat', 'hashtag')).to.be.ok;
55 | });
56 |
57 | it('Works with trailing underscores', () => {
58 | expect(linkify.test('#bug_', 'hashtag')).to.be.ok;
59 | });
60 |
61 | it('Works with underscores', () => {
62 | expect(linkify.test('#bug_test', 'hashtag')).to.be.ok;
63 | });
64 |
65 | it('Works with double underscores', () => {
66 | expect(linkify.test('#bug__test', 'hashtag')).to.be.ok;
67 | });
68 |
69 | it('Works with number prefix', () => {
70 | expect(linkify.test('#123abc', 'hashtag')).to.be.ok;
71 | });
72 |
73 | it('Works with number/underscore prefix', () => {
74 | expect(linkify.test('#123_abc', 'hashtag')).to.be.ok;
75 | });
76 |
77 | it('Works with Hangul characters', () => {
78 | expect(linkify.test('#일상', 'hashtag')).to.be.ok;
79 | });
80 |
81 | it('Works with Cyrillic characters', () => {
82 | expect(linkify.test('#АБВ_бв', 'hashtag')).to.be.ok;
83 | });
84 |
85 | it('Works with Arabic characters', () => {
86 | expect(linkify.test('#سلام', 'hashtag')).to.be.ok;
87 | });
88 |
89 | it('Works with Japanese characters', () => {
90 | expect(linkify.test('#おはよう', 'hashtag')).to.be.ok;
91 | });
92 |
93 | it('Works with Japanese characters and full width middle dot', () => {
94 | expect(linkify.test('#おは・よう', 'hashtag')).to.be.ok;
95 | });
96 |
97 | it('Works with emojis', () => {
98 | expect(linkify.test('#🍭', 'hashtag')).to.be.ok;
99 | });
100 |
101 | it('Works with emojis and letters', () => {
102 | expect(linkify.test('#candy🍭', 'hashtag')).to.be.ok;
103 | });
104 |
105 | it('Works with emojis and letters and underscores', () => {
106 | expect(linkify.test('#__candy_🍭sdsd🖤_wat', 'hashtag')).to.be.ok;
107 | });
108 |
109 | it('Does not work with just numbers', () => {
110 | expect(linkify.test('#987', 'hashtag')).to.not.be.ok;
111 | });
112 |
113 | it('Does not work with just numbers and underscore', () => {
114 | expect(linkify.test('#987_654', 'hashtag')).to.not.be.ok;
115 | });
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/test/spec/linkify-plugin-ip.test.mjs:
--------------------------------------------------------------------------------
1 | import * as linkify from 'linkifyjs/src/linkify.mjs';
2 | import { init as initScanner, run as runScanner } from 'linkifyjs/src/scanner.mjs';
3 | import { ipv4Tokens, ipv6Tokens, ip } from 'linkify-plugin-ip/src/ip.mjs';
4 | import { expect } from 'chai';
5 |
6 | describe('linkify-plugin-ip', () => {
7 | beforeEach(() => {
8 | linkify.reset();
9 | });
10 |
11 | it('cannot parse IP addresse before applying the plugin', () => {
12 | expect(linkify.find('No place like 127.0.0.1')).to.be.eql([]);
13 | expect(linkify.test('255.255.255.255', 'ipv4')).to.not.be.ok;
14 | expect(linkify.test('http://[2001:db8::ff00:42:8329]', 'url')).to.not.be.ok;
15 | });
16 |
17 | describe('scanner', () => {
18 | let scanner;
19 |
20 | beforeEach(() => {
21 | scanner = initScanner();
22 | ipv4Tokens({ scanner });
23 | ipv6Tokens({ scanner });
24 | });
25 |
26 | it('Scans IPV6 tokens', () => {
27 | const tokens = runScanner(scanner.start, '[2606:4700:4700:0:0:0:0:1111]');
28 | expect(tokens).to.eql([
29 | {
30 | t: 'B_IPV6_B',
31 | v: '[2606:4700:4700:0:0:0:0:1111]',
32 | s: 0,
33 | e: 29,
34 | },
35 | ]);
36 | });
37 | });
38 |
39 | describe('after plugin is applied', () => {
40 | beforeEach(() => {
41 | linkify.registerTokenPlugin('ipv4', ipv4Tokens);
42 | linkify.registerTokenPlugin('ipv6', ipv6Tokens);
43 | linkify.registerPlugin('ip', ip);
44 | });
45 |
46 | it('can parse ips after applying the plugin', () => {
47 | expect(linkify.find('No place like 127.0.0.1')).to.be.eql([
48 | {
49 | type: 'ipv4',
50 | value: '127.0.0.1',
51 | href: 'http://127.0.0.1',
52 | isLink: true,
53 | start: 14,
54 | end: 23,
55 | },
56 | ]);
57 |
58 | expect(linkify.test('255.255.255.255', 'ipv4')).to.be.ok;
59 | });
60 |
61 | const validTests = [
62 | ['0.0.0.0', 'ipv4'],
63 | ['192.168.0.1', 'ipv4'],
64 | ['255.255.255.255', 'ipv4'],
65 | ['232.121.20.3/', 'url'],
66 | ['232.121.20.3:255', 'url'],
67 | ['232.121.20.3:3000', 'url'],
68 | ['http://[::]', 'url'],
69 | ['http://[1::]', 'url'],
70 | ['http://[123::]', 'url'],
71 | ['http://[::1]', 'url'],
72 | ['http://[::123]', 'url'],
73 | ['http://[1::1]', 'url'],
74 | ['http://[123::123]', 'url'],
75 | ['http://[12ef::12ef]', 'url'],
76 | ['http://[::1:2:3]', 'url'],
77 | ['http://[f:f::f:f]', 'url'],
78 | ['http://[f:f:f:f:f:f:f:f]', 'url'],
79 | ['http://[:f:f:f:f:f:f:f]', 'url'],
80 | ['http://[::1:2:3:a:b:c]', 'url'],
81 | ['http://[11:22:33:aa:bb:cc::]', 'url'],
82 | ['http://[2606:4700:4700:0:0:0:0:1111]/', 'url'],
83 | ['https://[2606:4700:4700:0:0:0:0:1111]:443/', 'url'],
84 | ['http://[2001:db8::ff00:42:8329]', 'url'],
85 | ];
86 |
87 | const invalidTests = [
88 | '0.0.0.0.0',
89 | '255.255.256.255',
90 | '232.121.20/',
91 | '232.121.3:255',
92 | '121.20.3.242.232:3000',
93 | 'http://[f:f:f:f:f:f:f]', // too few components
94 | 'http://[:f:f:f:f:f:f]', // too few components
95 | 'http://[f:f:f:f:f:f:]', // too few components
96 | 'http://[f:f:f:f:f:f:f:f:f]', // too many components
97 | 'http://[f:f:f:f:::f]', // too many colons
98 | 'http://[::123ef]', // component too long
99 | 'http://[123ef::]', // component too long
100 | 'http://[123ef::fed21]', // component too long
101 | 'http://[::g]', // invalid hex digit
102 | // 'http://[::f:f:f:f:f:f:f:f]', // too many components (hard to implement)
103 | // 'http://[f:f:f:f::f:f:f:f]', // too many components (hard to implement)
104 | // 'http://[f::ff:ff::f]', // too many colons, ambiguous (hard to implement)
105 | ];
106 |
107 | for (const [value, type] of validTests) {
108 | it(`Detects ${value} as ${type}`, () => {
109 | expect(linkify.test(value, type)).to.be.ok;
110 | });
111 | }
112 |
113 | for (const test of invalidTests) {
114 | it(`Does not detect ${test}`, () => {
115 | expect(linkify.test(test)).to.not.be.ok;
116 | });
117 | }
118 | });
119 | });
120 |
--------------------------------------------------------------------------------
/test/spec/linkify-plugin-keyword.test.mjs:
--------------------------------------------------------------------------------
1 | import * as linkify from 'linkifyjs/src/linkify.mjs';
2 | import { keyword, tokens, registerKeywords } from 'linkify-plugin-keyword/src/keyword.mjs';
3 | import { expect } from 'chai';
4 |
5 | describe('linkify-plugin-keyword', () => {
6 | beforeEach(() => {
7 | linkify.reset();
8 | });
9 |
10 | it('cannot parse keywords before applying the plugin', () => {
11 | expect(linkify.find('Hello, World!')).to.be.eql([]);
12 | });
13 |
14 | describe('#registerKeywords()', () => {
15 | it('Throws on empty keywords', () => {
16 | expect(() => registerKeywords([''])).to.throw();
17 | });
18 |
19 | it('Throws on non-string keywords', () => {
20 | expect(() => registerKeywords([42])).to.throw();
21 | });
22 | });
23 |
24 | describe('after plugin is applied with no keywords', () => {
25 | beforeEach(() => {
26 | registerKeywords([]); // just to test the branch
27 | linkify.registerTokenPlugin('keyword', tokens);
28 | linkify.registerPlugin('keyword', keyword);
29 | });
30 |
31 | it('Does not interfere with initialization', () => {
32 | expect(linkify.find('http.org')).to.be.ok;
33 | });
34 | });
35 |
36 | describe('after plugin is applied', () => {
37 | const keywords = [
38 | '42',
39 | 'hello',
40 | 'world',
41 | '500px',
42 | 'テスト',
43 | 'öko123',
44 | 'http',
45 | 'view-source',
46 | 'view--source',
47 | '-view-source-',
48 | '🍕💩',
49 | 'Hello, World!',
50 | 'world', // repeat
51 | '~ ^_^ ~',
52 | ];
53 |
54 | const potentiallyConflictingStrings = [
55 | ['http://192.168.0.42:4242', 'url'],
56 | ['http.org', 'url'],
57 | ['hello.world', 'url'],
58 | ['world.world', 'url'],
59 | ['hello42öko123.world', 'url'],
60 | ['https://hello.world', 'url'],
61 | ['500px.com', 'url'],
62 | ['テスト@example.com', 'email'],
63 | ['example@テスト.to', 'email'],
64 | ['www.view-source.com', 'url'],
65 | ['🍕💩.kz', 'url'],
66 | ];
67 |
68 | beforeEach(() => {
69 | registerKeywords(keywords);
70 | linkify.registerTokenPlugin('keyword', tokens);
71 | linkify.registerPlugin('keyword', keyword);
72 | });
73 |
74 | it('finds numeric keywords', () => {
75 | expect(linkify.find('The magic number is 42!')).to.be.eql([
76 | {
77 | type: 'keyword',
78 | value: '42',
79 | href: '42',
80 | isLink: true,
81 | start: 20,
82 | end: 22,
83 | },
84 | ]);
85 | });
86 |
87 | for (const keyword of keywords) {
88 | it(`Detects keyword ${keyword}`, () => {
89 | expect(linkify.test(keyword, 'keyword')).to.be.ok;
90 | });
91 | }
92 |
93 | for (const [str, type] of potentiallyConflictingStrings) {
94 | it(`Does not conflict with existing token ${type} ${str}`, () => {
95 | expect(linkify.test(str, type)).to.be.ok;
96 | });
97 | }
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/test/spec/linkify-plugin-mention.test.mjs:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as linkify from 'linkifyjs/src/linkify.mjs';
3 | import mention from 'linkify-plugin-mention/src/mention.mjs';
4 |
5 | describe('linkify-plugin-mention', () => {
6 | beforeEach(() => {
7 | linkify.reset();
8 | });
9 |
10 | it('Cannot parse mentions before applying the plugin', () => {
11 | expect(linkify.find('There is a @mention @YOLO2016 and @1234 and @%^&*( should not work')).to.be.eql([]);
12 |
13 | expect(linkify.test('@wat', 'mention')).to.not.be.ok;
14 | expect(linkify.test('@007', 'mention')).to.not.be.ok;
15 | });
16 |
17 | describe('after plugin is applied', () => {
18 | beforeEach(() => {
19 | linkify.registerPlugin('mention', mention);
20 | });
21 |
22 | it('Can parse mentions after applying the plugin', () => {
23 | expect(linkify.find('There is a @mention @YOLO2016 and @1234 and @%^&*( should not work')).to.deep.equal([
24 | {
25 | type: 'mention',
26 | value: '@mention',
27 | href: '/mention',
28 | isLink: true,
29 | start: 11,
30 | end: 19,
31 | },
32 | {
33 | type: 'mention',
34 | value: '@YOLO2016',
35 | href: '/YOLO2016',
36 | isLink: true,
37 | start: 20,
38 | end: 29,
39 | },
40 | {
41 | type: 'mention',
42 | value: '@1234',
43 | href: '/1234',
44 | isLink: true,
45 | start: 34,
46 | end: 39,
47 | },
48 | ]);
49 |
50 | expect(linkify.test('@wat', 'mention')).to.be.ok;
51 | expect(linkify.test('@987', 'mention')).to.be.ok;
52 | });
53 |
54 | it('detects mentions with just text', () => {
55 | expect(linkify.find('Hey @nfrasser')).to.deep.equal([
56 | {
57 | type: 'mention',
58 | value: '@nfrasser',
59 | href: '/nfrasser',
60 | isLink: true,
61 | start: 4,
62 | end: 13,
63 | },
64 | ]);
65 | });
66 |
67 | it('parses mentions that begin and end with underscores', () => {
68 | expect(linkify.find('Mention for @__lI3t__')).to.deep.equal([
69 | {
70 | type: 'mention',
71 | value: '@__lI3t__',
72 | href: '/__lI3t__',
73 | isLink: true,
74 | start: 12,
75 | end: 21,
76 | },
77 | ]);
78 | });
79 |
80 | it('parses mentions with hyphens and underscores', () => {
81 | expect(linkify.find('Paging @sir_mc-lovin')).to.deep.equal([
82 | {
83 | type: 'mention',
84 | value: '@sir_mc-lovin',
85 | href: '/sir_mc-lovin',
86 | isLink: true,
87 | start: 7,
88 | end: 20,
89 | },
90 | ]);
91 | });
92 |
93 | it('parses github team-style mentions with slashes', () => {
94 | expect(linkify.find('Hey @500px/web please review this')).to.deep.equal([
95 | {
96 | type: 'mention',
97 | value: '@500px/web',
98 | href: '/500px/web',
99 | isLink: true,
100 | start: 4,
101 | end: 14,
102 | },
103 | ]);
104 | });
105 |
106 | it('ignores extra slashes at the end of mentions', () => {
107 | expect(linkify.find('We should get ///@soapbox/_developers/@soapbox/cs//// to review these')).to.deep.equal(
108 | [
109 | {
110 | type: 'mention',
111 | value: '@soapbox/_developers',
112 | href: '/soapbox/_developers',
113 | isLink: true,
114 | start: 17,
115 | end: 37,
116 | },
117 | {
118 | type: 'mention',
119 | value: '@soapbox/cs',
120 | href: '/soapbox/cs',
121 | isLink: true,
122 | start: 38,
123 | end: 49,
124 | },
125 | ],
126 | );
127 | });
128 |
129 | it('parses mentions with dots (ignores past the dots)', () => {
130 | expect(linkify.find('Hey @john.doe please review this')).to.deep.equal([
131 | {
132 | type: 'mention',
133 | value: '@john',
134 | href: '/john',
135 | isLink: true,
136 | start: 4,
137 | end: 9,
138 | },
139 | ]);
140 | });
141 |
142 | it('ignores extra dots at the end of mentions', () => {
143 | expect(linkify.find('We should get ...@soapbox-_developers.@soapbox_cs.... to be awesome')).to.deep.equal([
144 | {
145 | type: 'mention',
146 | value: '@soapbox-_developers',
147 | href: '/soapbox-_developers',
148 | isLink: true,
149 | start: 17,
150 | end: 37,
151 | },
152 | {
153 | type: 'mention',
154 | value: '@soapbox_cs',
155 | href: '/soapbox_cs',
156 | isLink: true,
157 | start: 38,
158 | end: 49,
159 | },
160 | ]);
161 | });
162 |
163 | it('does not treat @/.* as a mention', () => {
164 | expect(linkify.find('What about @/ and @/nfrasser?')).to.deep.equal([]);
165 | });
166 |
167 | it('ignores text only made up of symbols', () => {
168 | expect(linkify.find('Is @- or @~! a person? What about @%_% no, probably not')).to.deep.equal([]);
169 | });
170 |
171 | it('ignores punctuation at the end of mentions', () => {
172 | expect(linkify.find('These people are awesome: @graham, @brennan, and @chris! Also @nick.')).to.deep.equal([
173 | {
174 | type: 'mention',
175 | value: '@graham',
176 | href: '/graham',
177 | isLink: true,
178 | start: 26,
179 | end: 33,
180 | },
181 | {
182 | type: 'mention',
183 | value: '@brennan',
184 | href: '/brennan',
185 | isLink: true,
186 | start: 35,
187 | end: 43,
188 | },
189 | {
190 | type: 'mention',
191 | value: '@chris',
192 | href: '/chris',
193 | isLink: true,
194 | start: 49,
195 | end: 55,
196 | },
197 | {
198 | type: 'mention',
199 | value: '@nick',
200 | href: '/nick',
201 | isLink: true,
202 | start: 62,
203 | end: 67,
204 | },
205 | ]);
206 | });
207 |
208 | it('detects numerical mentions', () => {
209 | expect(linkify.find('Hey @123 and @456_78910__')).to.deep.equal([
210 | {
211 | type: 'mention',
212 | value: '@123',
213 | href: '/123',
214 | isLink: true,
215 | start: 4,
216 | end: 8,
217 | },
218 | {
219 | type: 'mention',
220 | value: '@456_78910__',
221 | href: '/456_78910__',
222 | isLink: true,
223 | start: 13,
224 | end: 25,
225 | },
226 | ]);
227 | });
228 |
229 | it('detects trailing hyphen', () => {
230 | expect(linkify.test('@123-', 'mention')).to.be.ok;
231 | });
232 |
233 | it('detects interjecting hyphen', () => {
234 | expect(linkify.test('@123-abc', 'mention')).to.be.ok;
235 | });
236 |
237 | it('detects single underscore', () => {
238 | expect(linkify.test('@_', 'mention')).to.be.ok;
239 | });
240 |
241 | it('detects multiple underscore', () => {
242 | expect(linkify.test('@__', 'mention')).to.be.ok;
243 | });
244 |
245 | it('ignores interjecting dot', () => {
246 | expect(linkify.test('@hello.world', 'mention')).to.not.be.ok;
247 | });
248 |
249 | it('begin with hyphen', () => {
250 | expect(linkify.test('@-advanced', 'mention')).to.be.ok;
251 | });
252 | });
253 |
254 | afterEach(() => {
255 | linkify.reset();
256 | });
257 | });
258 |
--------------------------------------------------------------------------------
/test/spec/linkify-plugin-ticket.test.mjs:
--------------------------------------------------------------------------------
1 | import * as linkify from 'linkifyjs/src/linkify.mjs';
2 | import ticket from 'linkify-plugin-ticket/src/ticket.mjs';
3 | import { expect } from 'chai';
4 |
5 | describe('linkify-plugin-ticket', () => {
6 | beforeEach(() => {
7 | linkify.reset();
8 | });
9 |
10 | it('cannot parse tickets before applying the plugin', () => {
11 | expect(linkify.find('This is ticket #2015 and #1234 and #%^&*( should not work')).to.be.eql([]);
12 | expect(linkify.test('#1422', 'ticket')).to.not.be.ok;
13 | expect(linkify.test('#987', 'ticket')).to.not.be.ok;
14 | });
15 |
16 | describe('after plugin is applied', () => {
17 | it('can parse tickets after applying the plugin', () => {
18 | linkify.registerPlugin('ticket', ticket);
19 |
20 | expect(linkify.find('Check out issue #42')).to.be.eql([
21 | {
22 | type: 'ticket',
23 | value: '#42',
24 | href: '#42',
25 | isLink: true,
26 | start: 16,
27 | end: 19,
28 | },
29 | ]);
30 |
31 | expect(linkify.find('Check out issue #9999999 and also #0')).to.be.eql([
32 | {
33 | type: 'ticket',
34 | value: '#9999999',
35 | href: '#9999999',
36 | isLink: true,
37 | start: 16,
38 | end: 24,
39 | },
40 | {
41 | type: 'ticket',
42 | value: '#0',
43 | href: '#0',
44 | isLink: true,
45 | start: 34,
46 | end: 36,
47 | },
48 | ]);
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/test/spec/linkify-react.test.mjs:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { renderToStaticMarkup } from 'react-dom/server';
3 | import * as linkify from 'linkifyjs';
4 | import Linkify from 'linkify-react/src/linkify-react.mjs';
5 | import mention from 'linkify-plugin-mention/src/mention.mjs';
6 | import { expect } from 'chai';
7 |
8 | const options = {
9 | // test options
10 | tagName: 'em',
11 | target: '_parent',
12 | nl2br: true,
13 | className: 'my-linkify-class',
14 | defaultProtocol: 'https',
15 | rel: 'nofollow',
16 | attributes: {
17 | onClick() {
18 | alert('Hello World!');
19 | },
20 | },
21 | format: function (val) {
22 | return val.truncate(40);
23 | },
24 | formatHref: {
25 | email: (href) => href + '?subject=Hello%20from%20Linkify',
26 | },
27 | };
28 |
29 | describe('linkify-react', () => {
30 | // For each element in this array
31 | // [0] - Original text
32 | // [1] - Linkified with default options
33 | // [2] - Linkified with new options
34 | let tests = [
35 | ['Test with no links', 'Test with no links', 'Test with no links
'],
36 | [
37 | 'The URL is google.com and the email is test@example.com',
38 | 'The URL is google.com and the email is test@example.com',
39 | 'The URL is google.com and the email is test@example.com
',
40 | ],
41 | [
42 | 'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: test.wut.yo@gmail.co.uk!\n',
43 | 'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: test.wut.yo@gmail.co.uk!\n',
44 | 'Super long maps URL https://www.google.ca/maps/@43.472082,-8…, a #hash-tag, and an email: test.wut.yo@gmail.co.uk!
',
45 | ],
46 | [
47 | 'Link with @some.username\nshould not work as a link',
48 | 'Link with @some.username\nshould not work as a link',
49 | 'Link with @some.username
should not work as a link
',
50 | ],
51 | ];
52 |
53 | it('Works with default options', function () {
54 | tests.map((test) => {
55 | var linkified = React.createElement(Linkify, null, test[0]);
56 | var result = renderToStaticMarkup(linkified);
57 | expect(result).to.be.oneOf([test[1], `${test[1]}`]);
58 | });
59 | });
60 |
61 | it('Works with overriden options', function () {
62 | tests.map((test) => {
63 | var props = { options, as: 'div', className: 'lambda' };
64 | var linkified = React.createElement(Linkify, props, test[0]);
65 | var result = renderToStaticMarkup(linkified);
66 | expect(result).to.be.eql(test[2]);
67 | });
68 | });
69 |
70 | it('Finds links recursively', function () {
71 | var strong = React.createElement('strong', null, 'https://facebook.github.io/react/');
72 | var linkified = React.createElement(Linkify, null, 'A great site is google.com AND ', strong);
73 | var result = renderToStaticMarkup(linkified);
74 | var expected =
75 | 'A great site is google.com AND https://facebook.github.io/react/';
76 | expect(result).to.be.oneOf([expected, `${expected}`]);
77 | });
78 |
79 | it('Excludes self-closing elements', () => {
80 | class Delta extends React.Component {
81 | render() {
82 | return React.createElement('strong', this.props, 'https://facebook.github.io/react/');
83 | }
84 | }
85 |
86 | var delta = React.createElement(Delta);
87 | var linkified = React.createElement(Linkify, null, 'A great site is google.com AND ', delta);
88 | var result = renderToStaticMarkup(linkified);
89 | var expected =
90 | 'A great site is google.com AND https://facebook.github.io/react/';
91 | expect(result).to.be.oneOf([expected, `${expected}`]);
92 | });
93 |
94 | it('Obeys ignoreTags option', () => {
95 | var options = {
96 | ignoreTags: ['em'],
97 | };
98 | var em = React.createElement('em', null, 'https://facebook.github.io/react/');
99 | var linkified = React.createElement(Linkify, { options }, 'A great site is google.com AND ', em);
100 | var result = renderToStaticMarkup(linkified);
101 | var expected =
102 | 'A great site is google.com AND https://facebook.github.io/react/';
103 | expect(result).to.be.oneOf([expected, `${expected}`]);
104 | });
105 |
106 | it('Correctly renders multiple text and element children', () => {
107 | const options = { nl2br: true };
108 | const foo = `hello
109 |
110 | `;
111 | const bar = `hello
112 |
113 | `;
114 | const linkified = React.createElement(
115 | Linkify,
116 | { options },
117 | foo,
118 | ' ',
119 | bar,
120 | React.createElement('em', { key: 0 }, ['or contact nfrasser@example.com']),
121 | 'For the latest javascript.net\n',
122 | React.createElement('strong', { key: 1 }, ['and also\n', '🥺👄.ws']),
123 | );
124 | const result = renderToStaticMarkup(linkified);
125 | const expected = [
126 | 'hello
\t\t ',
127 | 'hello
\t\t',
128 | 'or contact nfrasser@example.com',
129 | 'For the latest javascript.net
',
130 | 'and also
🥺👄.ws',
131 | ].join('');
132 | expect(result).to.be.oneOf([expected, `${expected}`]);
133 | });
134 |
135 | describe('Custom render', () => {
136 | beforeEach(() => {
137 | linkify.reset();
138 | linkify.registerPlugin('mention', mention);
139 | });
140 |
141 | it('Renders dedicated mentions component', () => {
142 | const options = {
143 | formatHref: {
144 | mention: (href) => `/users${href}`,
145 | },
146 | render: {
147 | mention: ({ attributes, content }) => {
148 | const { href, ...props } = attributes;
149 | return React.createElement('span', { 'data-to': href, ...props }, content);
150 | },
151 | },
152 | };
153 | const linkified = React.createElement(
154 | Linkify,
155 | { options },
156 | 'Check out linkify.js.org or contact @nfrasser',
157 | );
158 | const result = renderToStaticMarkup(linkified);
159 | const expected =
160 | 'Check out linkify.js.org or contact @nfrasser';
161 | expect(result).to.be.oneOf([expected, `${expected}`]);
162 | });
163 | });
164 | });
165 |
--------------------------------------------------------------------------------
/test/spec/linkify-string.test.mjs:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import linkifyStr from 'linkify-string/src/linkify-string.mjs';
3 |
4 | describe('linkify-string', () => {
5 | // For each element in this array
6 | // [0] - Original text
7 | // [1] - Linkified with default options
8 | // [2] - Linkified with new options
9 | const tests = [
10 | ['Test with no links', 'Test with no links', 'Test with no links'],
11 | [
12 | 'The URL is google.com and the email is test@example.com',
13 | 'The URL is google.com and the email is test@example.com',
14 | 'The URL is google.com and the email is test@example.com',
15 | ],
16 | [
17 | 'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: test.wut.yo@gmail.co.uk!\n',
18 | 'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: test.wut.yo@gmail.co.uk!\n',
19 | 'Super long maps URL https://www.google.ca/maps/@43.472082,-8…, a #hash-tag, and an email: test.wut.yo@gmail.co.uk!
\n',
20 | ],
21 | ];
22 |
23 | let options;
24 |
25 | before(() => {
26 | options = {
27 | // test options
28 | tagName: 'span',
29 | target: '_parent',
30 | nl2br: true,
31 | className: 'my-linkify-class',
32 | defaultProtocol: 'https',
33 | rel: 'nofollow',
34 | attributes: {
35 | onclick: 'javascript:alert("Hello");',
36 | },
37 | format: function (val) {
38 | return val.truncate(40);
39 | },
40 | formatHref: function (href, type) {
41 | if (type === 'email') {
42 | href += '?subject=Hello%20from%20Linkify';
43 | }
44 | return href;
45 | },
46 | };
47 | });
48 |
49 | it('Works with default options', () => {
50 | tests.map(function (test) {
51 | expect(linkifyStr(test[0])).to.be.eql(test[1]);
52 | });
53 | });
54 |
55 | it('Works with overriden options (general)', () => {
56 | tests.map(function (test) {
57 | expect(linkifyStr(test[0], options)).to.be.eql(test[2]);
58 | });
59 | });
60 |
61 | describe('Prototype method', () => {
62 | it('Works with default options', () => {
63 | tests.map(function (test) {
64 | expect(test[0].linkify()).to.be.eql(test[1]);
65 | });
66 | });
67 |
68 | it('Works with overriden options (general)', () => {
69 | tests.map(function (test) {
70 | expect(test[0].linkify(options)).to.be.eql(test[2]);
71 | });
72 | });
73 | });
74 |
75 | describe('Validation', () => {
76 | // Test specific options
77 | const options = {
78 | validate: {
79 | url: (text) => /^(http|ftp)s?:\/\//.test(text) || text.slice(0, 3) === 'www',
80 | },
81 | };
82 |
83 | const tests = [
84 | ['1.Test with no links', '1.Test with no links'],
85 | [
86 | '2.The URL is google.com and the email is test@example.com',
87 | '2.The URL is google.com and the email is test@example.com',
88 | ],
89 | ['3.The URL is www.google.com', '3.The URL is www.google.com'],
90 | ['4.The URL is http://google.com', '4.The URL is http://google.com'],
91 | ['5.The URL is ftp://google.com', '5.The URL is ftp://google.com'],
92 | [
93 | '6.Test with no links.It is sloppy avoiding spaces after the dot',
94 | '6.Test with no links.It is sloppy avoiding spaces after the dot',
95 | ],
96 | ];
97 |
98 | it('Works with overriden options (validate)', function () {
99 | tests.map(function (test) {
100 | expect(linkifyStr(test[0], options)).to.be.eql(test[1]);
101 | });
102 | });
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/test/spec/linkifyjs.test.mjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable mocha/no-setup-in-describe */
2 | import { expect } from 'chai';
3 | import * as linkify from 'linkifyjs/src/linkify.mjs';
4 |
5 | const TicketToken = linkify.createTokenClass('ticket', { isLink: true });
6 |
7 | /**
8 | * @type import('linkifyjs').Plugin
9 | */
10 | const ticketPlugin = ({ scanner, parser }) => {
11 | const { POUND, groups } = scanner.tokens;
12 | const Hash = parser.start.tt(POUND);
13 | const Ticket = new linkify.State(TicketToken);
14 | Hash.ta(groups.numeric, Ticket);
15 | };
16 |
17 | describe('linkifyjs', () => {
18 | describe('registerPlugin', () => {
19 | beforeEach(() => {
20 | linkify.registerPlugin('ticket', ticketPlugin);
21 | });
22 |
23 | it('Detects tickets after applying', () => {
24 | expect(linkify.test('#123', 'ticket')).to.be.ok;
25 | });
26 |
27 | it('Logs a warning if registering same plugin twice', () => {
28 | linkify.registerPlugin('ticket', ticketPlugin);
29 | expect(linkify.test('#123', 'ticket')).to.be.ok;
30 | });
31 |
32 | it('Logs a warning if already initialized', () => {
33 | linkify.init();
34 | linkify.registerPlugin('ticket2', ticketPlugin);
35 | });
36 | });
37 |
38 | describe('registerCustomProtocol', () => {
39 | beforeEach(() => {
40 | linkify.registerCustomProtocol('view-source');
41 | linkify.registerCustomProtocol('instagram', true);
42 | linkify.registerCustomProtocol('magnet', true);
43 | });
44 |
45 | it('Detects basic protocol', () => {
46 | expect(linkify.test('instagram:user/nfrasser', 'url')).to.be.ok;
47 | });
48 |
49 | it('Detects basic protocol with slash slash', () => {
50 | expect(linkify.test('instagram://user/nfrasser', 'url')).to.be.ok;
51 | });
52 |
53 | it('Detects magnet protocol', () => {
54 | const magnetLink =
55 | 'magnet:?xt=urn:btih:5a7f5e0f3ce439e2f1a83e718a8405ec8809110b&dn=ernfkjenrkfk%5FSQ80%5FV%5Fv1.0.0%5Ferfnkerkf%5Ferfnkerkfefrfvegrteggt.net.rar';
56 | expect(linkify.test(magnetLink, 'url')).to.be.ok;
57 | });
58 |
59 | it('Detects compound protocol', () => {
60 | expect(linkify.test('view-source://http://github.com/', 'url')).to.be.ok;
61 | });
62 |
63 | it('Does not detect protocol with non-optional //', () => {
64 | expect(linkify.test('view-source:http://github.com/', 'url')).to.not.be.ok;
65 | });
66 |
67 | it('Does not detect custom protocol if already initialized', () => {
68 | linkify.init();
69 | linkify.registerCustomProtocol('fb');
70 | expect(linkify.test('fb://feed')).to.not.be.ok;
71 | });
72 |
73 | it('Throws error when protocol has invalid format', () => {
74 | expect(() => linkify.registerCustomProtocol('-')).to.throw();
75 | expect(() => linkify.registerCustomProtocol('-fb')).to.throw();
76 | expect(() => linkify.registerCustomProtocol('fb-')).to.throw();
77 | expect(() => linkify.registerCustomProtocol('git+https')).to.throw(); // this may work in the future
78 | });
79 | });
80 |
81 | describe('tokenize', () => {
82 | it('is a function', () => {
83 | expect(linkify.tokenize).to.be.a('function');
84 | });
85 |
86 | it('takes a single argument', () => {
87 | expect(linkify.tokenize.length).to.be.eql(1);
88 | });
89 | });
90 |
91 | describe('find', () => {
92 | it('is a function', () => {
93 | expect(linkify.find).to.be.a('function');
94 | });
95 |
96 | it('Find nothing in an empty string', () => {
97 | expect(linkify.find('')).to.deep.eql([]);
98 | });
99 |
100 | it('Find nothing in a string with no links', () => {
101 | expect(linkify.find('Hello World!')).to.deep.eql([]);
102 | });
103 |
104 | it('Find the link', () => {
105 | expect(linkify.find('hello.world!')).to.deep.eql([
106 | {
107 | type: 'url',
108 | value: 'hello.world',
109 | href: 'http://hello.world',
110 | isLink: true,
111 | start: 0,
112 | end: 11,
113 | },
114 | ]);
115 | });
116 |
117 | it('Find the link of the specific type', () => {
118 | expect(linkify.find('For help with github.com, please contact support@example.com', 'email')).to.deep.eql([
119 | {
120 | type: 'email',
121 | value: 'support@example.com',
122 | href: 'mailto:support@example.com',
123 | isLink: true,
124 | start: 41,
125 | end: 60,
126 | },
127 | ]);
128 | });
129 |
130 | it('Finds with opts', () => {
131 | expect(linkify.find('Does www.truncate.com work with truncate?', { truncate: 10 })).to.deep.eql([
132 | {
133 | type: 'url',
134 | value: 'www.trunca…',
135 | isLink: true,
136 | href: 'http://www.truncate.com',
137 | start: 5,
138 | end: 21,
139 | },
140 | ]);
141 | });
142 |
143 | it('Finds type and opts', () => {
144 | expect(
145 | linkify.find('Does www.truncate.com work with example@truncate.com?', 'email', { truncate: 10 }),
146 | ).to.deep.eql([
147 | {
148 | type: 'email',
149 | value: 'example@tr…',
150 | isLink: true,
151 | href: 'mailto:example@truncate.com',
152 | start: 32,
153 | end: 52,
154 | },
155 | ]);
156 | });
157 |
158 | it('Throws on ambiguous invocation', () => {
159 | expect(() => linkify.find('Hello.com', { type: 'email' }, { truncate: 10 })).to.throw();
160 | });
161 |
162 | it('Uses validation to ignore links', () => {
163 | expect(
164 | linkify.find('foo.com and bar.com and baz.com', { validate: (url) => url !== 'bar.com' }),
165 | ).to.deep.eql([
166 | {
167 | type: 'url',
168 | value: 'foo.com',
169 | isLink: true,
170 | href: 'http://foo.com',
171 | start: 0,
172 | end: 7,
173 | },
174 | {
175 | end: 31,
176 | href: 'http://baz.com',
177 | isLink: true,
178 | start: 24,
179 | type: 'url',
180 | value: 'baz.com',
181 | },
182 | ]);
183 | });
184 | });
185 |
186 | describe('test', () => {
187 | /*
188 | For each element,
189 |
190 | * [0] is the input string
191 | * [1] is the expected return value
192 | * [2] (optional) the type of link to look for
193 | */
194 | const tests = [
195 | ['Herp derp', false],
196 | ['Herp derp', false, 'email'],
197 | ['Herp derp', false, 'asdf'],
198 | ['https://google.com/?q=yey', true],
199 | ['https://google.com/?q=yey', true, 'url'],
200 | ['https://google.com/?q=yey', false, 'email'],
201 | ['test+4@uwaterloo.ca', true],
202 | ['test+4@uwaterloo.ca', false, 'url'],
203 | ['test+4@uwaterloo.ca', true, 'email'],
204 | ['mailto:test+5@uwaterloo.ca', true, 'url'],
205 | ['t.co', true],
206 | ['t.co g.co', false], // can only be one
207 | ['test@g.co t.co', false], // can only be one
208 | ];
209 |
210 | it('is a function', () => {
211 | expect(linkify.test).to.be.a('function');
212 | });
213 |
214 | let testName;
215 | for (const test of tests) {
216 | testName = 'Correctly tests the string "' + test[0] + '"';
217 | testName += ' as `' + (test[1] ? 'true' : 'false') + '`';
218 | if (test[2]) {
219 | testName += ' (' + test[2] + ')';
220 | }
221 | testName += '.';
222 |
223 | it(testName, () => {
224 | expect(linkify.test(test[0], test[2])).to.be.eql(test[1]);
225 | });
226 | }
227 | });
228 |
229 | describe('options', () => {
230 | it('is an object', () => {
231 | expect(linkify.options).to.exist;
232 | });
233 | });
234 | });
235 |
--------------------------------------------------------------------------------
/test/spec/linkifyjs/fsm.test.mjs:
--------------------------------------------------------------------------------
1 | import * as tk from 'linkifyjs/src/text.mjs';
2 | import * as fsm from 'linkifyjs/src/fsm.mjs';
3 | import { State } from 'linkifyjs/src/linkify.mjs';
4 | import { expect } from 'chai';
5 |
6 | describe('linkifyjs/fsm/State', () => {
7 | let Start, Num, Word;
8 |
9 | beforeEach(() => {
10 | State.groups = {};
11 | Start = new fsm.State();
12 | Start.tt('.', tk.DOT);
13 | Num = Start.tr(/[0-9]/, tk.NUM, { numeric: true });
14 | Num.tr(/[0-9]/, Num);
15 | Word = Start.tr(/[a-z]/i, tk.WORD, { ascii: true });
16 | Word.tr(/[a-z]/i, Word);
17 | });
18 |
19 | after(() => {
20 | State.groups = {};
21 | });
22 |
23 | it('Creates DOT transition on Start state', () => {
24 | expect(Object.keys(Start.j)).to.eql(['.']);
25 | expect(Start.j['.'].t).to.eql(tk.DOT);
26 | });
27 |
28 | it('Creates regexp number transitions on Start state', () => {
29 | expect(Start.jr.length).to.eql(2);
30 | expect(Start.jr[0][0].test('42')).to.be.ok;
31 | expect(Start.jr[0][1].t).to.eql(tk.NUM);
32 | });
33 |
34 | it('Creates regexp word transitions on start state', () => {
35 | expect(Start.jr.length).to.eql(2);
36 | expect(Start.jr[1][0].test('hello')).to.be.ok;
37 | expect(Start.jr[1][1].t).to.eql(tk.WORD);
38 | });
39 |
40 | it('Populates groups', () => {
41 | expect(State.groups).to.eql({
42 | numeric: [tk.NUM],
43 | ascii: [tk.WORD],
44 | asciinumeric: [tk.NUM, tk.WORD],
45 | alpha: [tk.WORD],
46 | alphanumeric: [tk.NUM, tk.WORD],
47 | domain: [tk.NUM, tk.WORD],
48 | });
49 | });
50 |
51 | describe('#has()', () => {
52 | it('Does not have # transition', () => {
53 | expect(Start.has('#')).to.not.be.ok;
54 | });
55 |
56 | it('Has . transition', () => {
57 | expect(Start.has('.')).to.be.ok;
58 | });
59 |
60 | it('Has exact . transition', () => {
61 | expect(Start.has('.', true)).to.be.ok;
62 | });
63 |
64 | it('Has x transition', () => {
65 | expect(Start.has('x')).to.be.ok;
66 | });
67 |
68 | it('Does not have exact # transition', () => {
69 | expect(Start.has('#', true)).to.not.be.ok;
70 | });
71 | });
72 |
73 | describe('Add schemes', () => {
74 | beforeEach(() => {
75 | Start.ts('http', 'http', { ascii: true, scheme: true });
76 | Start.ts('https', 'https', { ascii: true, scheme: true });
77 | Start.ts('view-source', 'view-source', { domain: true, scheme: true });
78 | });
79 |
80 | it('Adds tokens to ascii group', () => {
81 | expect(State.groups.ascii).not.contains('htt');
82 | expect(State.groups.ascii).contains('http');
83 | expect(State.groups.ascii).contains('https');
84 | expect(State.groups.ascii).not.contains('view-source');
85 | });
86 |
87 | it('Adds tokens to domain group', () => {
88 | expect(State.groups.domain).not.contains('htt');
89 | expect(State.groups.domain).contains('http');
90 | expect(State.groups.domain).contains('https');
91 | expect(State.groups.domain).contains('view-source');
92 | });
93 |
94 | it('Adds tokens to scheme group', () => {
95 | expect(State.groups.scheme).contains('http');
96 | expect(State.groups.scheme).contains('https');
97 | expect(State.groups.scheme).contains('view-source');
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/test/spec/linkifyjs/options.test.mjs:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { fake } from 'sinon';
3 | import * as options from 'linkifyjs/src/options.mjs';
4 | import * as scanner from 'linkifyjs/src/scanner.mjs';
5 | import * as mtk from 'linkifyjs/src/multi.mjs';
6 |
7 | const Options = options.Options;
8 |
9 | describe('linkifyjs/options', () => {
10 | describe('defaults', () => {
11 | after(() => {
12 | options.defaults.defaultProtocol = 'http';
13 | });
14 |
15 | it('is an object', () => {
16 | expect(options.defaults).to.be.an('object');
17 | });
18 |
19 | it('contains some keys', () => {
20 | expect(Object.keys(options.defaults).length).to.be.above(0);
21 | });
22 |
23 | it('defines the value for unspecified Options', () => {
24 | var opts = new Options();
25 | options.defaults.defaultProtocol = 'https';
26 | var newOpts = new Options();
27 | expect(opts.get('defaultProtocol')).to.equal('http');
28 | expect(newOpts.get('defaultProtocol')).to.equal('https');
29 | });
30 | });
31 |
32 | describe('Options', () => {
33 | const events = { click: () => alert('clicked!') };
34 | let urlToken, emailToken, scannerStart;
35 | let attributes, opts, renderOpts;
36 |
37 | before(() => {
38 | scannerStart = scanner.init().start;
39 | const inputUrl = 'github.com';
40 | const inputEmail = 'test@example.com';
41 |
42 | const urlTextTokens = scanner.run(scannerStart, inputUrl);
43 | const emailTextTokens = scanner.run(scannerStart, inputEmail);
44 |
45 | urlToken = new mtk.Url(inputUrl, urlTextTokens);
46 | emailToken = new mtk.Email(inputEmail, emailTextTokens);
47 | });
48 |
49 | beforeEach(() => {
50 | attributes = fake.returns({ type: 'text/html' });
51 | opts = new Options({
52 | defaultProtocol: 'https',
53 | events,
54 | format: (text) => `<${text}>`,
55 | formatHref: {
56 | url: (url) => `${url}/?from=linkify`,
57 | email: (mailto) => `${mailto}?subject=Hello+from+Linkify`,
58 | },
59 | nl2br: true,
60 | validate: {
61 | url: (url) => /^http(s)?:\/\//.test(url), // only urls with protocols
62 | },
63 | ignoreTags: ['script', 'style'],
64 | rel: 'nofollow',
65 | attributes,
66 | className: 'custom-class-name',
67 | truncate: 40,
68 | });
69 |
70 | renderOpts = new Options(
71 | {
72 | tagName: 'b',
73 | className: 'linkified',
74 | render: {
75 | email: ({ attributes, content }) =>
76 | // Ignore tagname and most attributes
77 | `${content}`,
78 | },
79 | },
80 | ({ tagName, attributes, content }) => {
81 | const attrStrs = Object.keys(attributes).reduce(
82 | (a, attr) => a.concat(`${attr}="${attributes[attr]}"`),
83 | [],
84 | );
85 | return `<${tagName} ${attrStrs.join(' ')}>${content}${tagName}>`;
86 | },
87 | );
88 | });
89 |
90 | describe('#check()', () => {
91 | it('returns false for url token', () => {
92 | expect(opts.check(urlToken)).not.to.be.ok;
93 | });
94 |
95 | it('returns true for email token', () => {
96 | expect(opts.check(emailToken)).to.be.ok;
97 | });
98 | });
99 |
100 | describe('#render()', () => {
101 | it('Returns intermediate representation when render option not specified', () => {
102 | expect(opts.render(urlToken)).to.eql({
103 | tagName: 'a',
104 | attributes: {
105 | href: 'https://github.com/?from=linkify',
106 | class: 'custom-class-name',
107 | rel: 'nofollow',
108 | type: 'text/html',
109 | },
110 | content: '',
111 | eventListeners: events,
112 | });
113 | });
114 |
115 | it('Calls attributes option with unformatted href', () => {
116 | opts.render(urlToken);
117 | expect(attributes.calledWith('https://github.com', 'url', urlToken)).to.be.true;
118 | });
119 |
120 | it('renders a URL', () => {
121 | expect(renderOpts.render(urlToken)).to.eql(
122 | 'github.com',
123 | );
124 | });
125 |
126 | it('renders an email address', () => {
127 | expect(renderOpts.render(emailToken)).to.eql(
128 | 'test@example.com',
129 | );
130 | });
131 | });
132 | });
133 |
134 | describe('Nullifying Options', () => {
135 | var opts;
136 |
137 | beforeEach(() => {
138 | opts = new Options({ target: null, className: null });
139 | });
140 |
141 | describe('target', () => {
142 | it('should be nulled', () => {
143 | expect(opts.get('target')).to.be.null;
144 | });
145 | });
146 |
147 | describe('className', () => {
148 | it('should be nulled', () => {
149 | expect(opts.get('className')).to.be.null;
150 | });
151 | });
152 | });
153 | });
154 |
--------------------------------------------------------------------------------
/test/spec/linkifyjs/scanner.test.mjs:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as scanner from 'linkifyjs/src/scanner.mjs';
3 | import * as t from 'linkifyjs/src/text.mjs';
4 |
5 | // The elements are
6 | // 1. input string
7 | // 2. Types for the resulting instances
8 | // 3. String values for the resulting instances
9 | const tests = [
10 | ['', [], []],
11 | ['@', [t.AT], ['@']],
12 | [':', [t.COLON], [':']],
13 | ['.', [t.DOT], ['.']],
14 | ['-', [t.HYPHEN], ['-']],
15 | ['\n', [t.NL], ['\n']],
16 | ['\r\n', [t.NL], ['\r\n']],
17 | [' \r\n', [t.WS, t.NL], [' ', '\r\n']],
18 | ['\r\n ', [t.NL, t.WS], ['\r\n', ' ']],
19 | ['\r \n', [t.WS, t.NL], ['\r ', '\n']],
20 | ['+', [t.PLUS], ['+']],
21 | ['#', [t.POUND], ['#']],
22 | ['/', [t.SLASH], ['/']],
23 | ['&', [t.AMPERSAND], ['&']],
24 | ['*', [t.ASTERISK], ['*']],
25 | ['\\', [t.BACKSLASH], ['\\']],
26 | ['%', [t.PERCENT], ['%']],
27 | ['`', [t.BACKTICK], ['`']],
28 | ['^', [t.CARET], ['^']],
29 | ['|', [t.PIPE], ['|']],
30 | ['~', [t.TILDE], ['~']],
31 | ['$', [t.DOLLAR], ['$']],
32 | ['=', [t.EQUALS], ['=']],
33 | ['-', [t.HYPHEN], ['-']],
34 | ['・', [t.FULLWIDTHMIDDLEDOT], ['・']],
35 | ['&?<>(', [t.AMPERSAND, t.QUERY, t.OPENANGLEBRACKET, t.CLOSEANGLEBRACKET, t.OPENPAREN], ['&', '?', '<', '>', '(']],
36 | [
37 | '([{}])',
38 | [t.OPENPAREN, t.OPENBRACKET, t.OPENBRACE, t.CLOSEBRACE, t.CLOSEBRACKET, t.CLOSEPAREN],
39 | ['(', '[', '{', '}', ']', ')'],
40 | ],
41 | ["!,;'", [t.EXCLAMATION, t.COMMA, t.SEMI, t.APOSTROPHE], ['!', ',', ';', "'"]],
42 | ['hello', [t.WORD], ['hello']],
43 | ['Hello123', [t.ASCIINUMERICAL], ['Hello123']],
44 | ['hello123world', [t.ASCIINUMERICAL], ['hello123world']],
45 | ['0123', [t.NUM], ['0123']],
46 | ['123abc', [t.ASCIINUMERICAL], ['123abc']],
47 | ['http', [t.SLASH_SCHEME], ['http']],
48 | ['http:', [t.SLASH_SCHEME, t.COLON], ['http', ':']],
49 | ['https:', [t.SLASH_SCHEME, t.COLON], ['https', ':']],
50 | ['files:', [t.WORD, t.COLON], ['files', ':']],
51 | ['file//', [t.SCHEME, t.SLASH, t.SLASH], ['file', '/', '/']],
52 | ['ftp://', [t.SLASH_SCHEME, t.COLON, t.SLASH, t.SLASH], ['ftp', ':', '/', '/']],
53 | ['mailto', [t.SCHEME], ['mailto']],
54 | ['mailto:', [t.SCHEME, t.COLON], ['mailto', ':']],
55 | ['c', [t.WORD], ['c']],
56 | ['co', [t.TLD], ['co']],
57 | ['com', [t.TLD], ['com']],
58 | ['comm', [t.WORD], ['comm']],
59 | [
60 | 'vermögensberater السعودية москва',
61 | [t.TLD, t.WS, t.UTLD, t.WS, t.UTLD],
62 | ['vermögensberater', ' ', 'السعودية', ' ', 'москва'],
63 | ],
64 | ['abc 123 DoReMi', [t.TLD, t.WS, t.NUM, t.WS, t.WORD], ['abc', ' ', '123', ' ', 'DoReMi']],
65 | [
66 | 'abc 123 \n DoReMi',
67 | [t.TLD, t.WS, t.NUM, t.WS, t.NL, t.WS, t.WORD],
68 | ['abc', ' ', '123', ' ', '\n', ' ', 'DoReMi'],
69 | ],
70 | ['local', [t.WORD], ['local']],
71 | ['localhost', [t.LOCALHOST], ['localhost']],
72 | ['localhosts', [t.WORD], ['localhosts']],
73 | ['500px', [t.ASCIINUMERICAL], ['500px']],
74 | ['500-px', [t.NUM, t.HYPHEN, t.WORD], ['500', '-', 'px']],
75 | ['-500px', [t.HYPHEN, t.ASCIINUMERICAL], ['-', '500px']],
76 | ['500px-', [t.ASCIINUMERICAL, t.HYPHEN], ['500px', '-']],
77 | ['123-456', [t.NUM, t.HYPHEN, t.NUM], ['123', '-', '456']],
78 | ['foo\u00a0bar', [t.TLD, t.WS, t.TLD], ['foo', '\u00a0', 'bar']], // nbsp
79 | ['çïrâ.ca', [t.UWORD, t.WORD, t.UWORD, t.DOT, t.TLD], ['çï', 'r', 'â', '.', 'ca']],
80 | ['❤️💚', [t.EMOJI], ['❤️💚']],
81 | ['👊🏿🧑🏼🔬🌚', [t.EMOJI], ['👊🏿🧑🏼🔬🌚']], // contains zero-width joiner \u200d
82 | ['www.🍕💩.ws', [t.WORD, t.DOT, t.EMOJI, t.DOT, t.TLD], ['www', '.', '🍕💩', '.', 'ws']],
83 | [
84 | 'za̡͊͠͝lgό.gay', // May support diacritics in the future if someone complains
85 | [t.TLD, t.SYM, t.SYM, t.SYM, t.SYM, t.WORD, t.UWORD, t.DOT, t.TLD],
86 | ['za', '͠', '̡', '͊', '͝', 'lg', 'ό', '.', 'gay'],
87 | ],
88 | [
89 | "Direniş İzleme Grubu'nun",
90 | [t.WORD, t.UWORD, t.WS, t.UWORD, t.WORD, t.WS, t.WORD, t.APOSTROPHE, t.WORD],
91 | ['Direni', 'ş', ' ', 'İ', 'zleme', ' ', 'Grubu', "'", 'nun'],
92 | ],
93 | [
94 | 'example.com テスト', // spaces are ideographic space
95 | [t.WORD, t.DOT, t.TLD, t.WS, t.UWORD],
96 | ['example', '.', 'com', ' ', 'テスト'],
97 | ],
98 | [
99 | '#АБВ_бв #한글 #سلام',
100 | [t.POUND, t.UWORD, t.UNDERSCORE, t.UWORD, t.WS, t.POUND, t.UWORD, t.WS, t.POUND, t.UWORD],
101 | ['#', 'АБВ', '_', 'бв', ' ', '#', '한글', ' ', '#', 'سلام'],
102 | ],
103 | ['#おは・よう', [t.POUND, t.UWORD, t.FULLWIDTHMIDDLEDOT, t.UWORD], ['#', 'おは', '・', 'よう']],
104 | ['テストexample.comテスト', [t.UWORD, t.WORD, t.DOT, t.TLD, t.UWORD], ['テスト', 'example', '.', 'com', 'テスト']],
105 | [
106 | 'テストhttp://example.comテスト',
107 | [t.UWORD, t.SLASH_SCHEME, t.COLON, t.SLASH, t.SLASH, t.WORD, t.DOT, t.TLD, t.UWORD],
108 | ['テスト', 'http', ':', '/', '/', 'example', '.', 'com', 'テスト'],
109 | ],
110 | ['👻#PhotoOfTheDay', [t.EMOJI, t.POUND, t.WORD], ['👻', '#', 'PhotoOfTheDay']],
111 | ];
112 |
113 | const customSchemeTests = [
114 | ['stea', [t.WORD], ['stea']],
115 | ['steam', ['steam'], ['steam']],
116 | ['steams', [t.WORD], ['steams']],
117 | ['view', [t.WORD], ['view']],
118 | ['view-', [t.WORD, t.HYPHEN], ['view', '-']],
119 | ['view-s', [t.WORD, t.HYPHEN, t.WORD], ['view', '-', 's']],
120 | ['view-sour', [t.WORD, t.HYPHEN, t.WORD], ['view', '-', 'sour']],
121 | ['view-source', ['view-source'], ['view-source']],
122 | ['view-sources', ['view-source', t.WORD], ['view-source', 's']], // This is an unfortunate consequence :(
123 | ['twitter dot com', ['twitter', t.WS, t.TLD, t.WS, t.TLD], ['twitter', ' ', 'dot', ' ', 'com']],
124 | ['ms-settings', ['ms-settings'], ['ms-settings']],
125 | ['geo', ['geo'], ['geo']],
126 | ['42', ['42'], ['42']],
127 | ];
128 |
129 | describe('linkifyjs/scanner', () => {
130 | let start, tokens;
131 |
132 | before(() => {
133 | const result = scanner.init();
134 | start = result.start;
135 | tokens = result.tokens;
136 | });
137 |
138 | function makeTest(test) {
139 | return it('Tokenizes the string "' + test[0] + '"', () => {
140 | var str = test[0];
141 | var types = test[1];
142 | var values = test[2];
143 | var result = scanner.run(start, str);
144 |
145 | expect(result.map((token) => token.t)).to.eql(types);
146 | expect(result.map((token) => token.v)).to.eql(values);
147 | });
148 | }
149 |
150 | // eslint-disable-next-line mocha/no-setup-in-describe
151 | tests.map(makeTest, this);
152 |
153 | it('Correctly sets start and end indexes', () => {
154 | expect(scanner.run(start, 'Hello, World!')).to.eql([
155 | { t: t.WORD, v: 'Hello', s: 0, e: 5 },
156 | { t: t.COMMA, v: ',', s: 5, e: 6 },
157 | { t: t.WS, v: ' ', s: 6, e: 7 },
158 | { t: t.TLD, v: 'World', s: 7, e: 12 },
159 | { t: t.EXCLAMATION, v: '!', s: 12, e: 13 },
160 | ]);
161 | });
162 |
163 | describe('Custom protocols', () => {
164 | before(() => {
165 | const result = scanner.init([
166 | ['twitter', false],
167 | ['steam', true],
168 | ['org', false], // TLD is also a domain
169 | ['geo', false],
170 | ['42', true],
171 | ['view-source', false],
172 | ['ms-settings', true],
173 | ]);
174 | start = result.start;
175 | tokens = result.tokens;
176 | });
177 |
178 | // eslint-disable-next-line mocha/no-setup-in-describe
179 | customSchemeTests.map(makeTest, this);
180 |
181 | it('Updates collections correctly', () => {
182 | expect(tokens.groups.scheme).to.eql([t.SCHEME, '42', 'ms-settings', 'steam']);
183 | expect(tokens.groups.slashscheme).to.eql([t.SLASH_SCHEME, 'geo', 'org', 'twitter', 'view-source']);
184 | expect(tokens.groups.tld).includes('org');
185 | });
186 |
187 | it('Correctly tokenizes a full custom protocols', () => {
188 | expect(scanner.run(start, 'steam://hello')).to.eql([
189 | { t: 'steam', v: 'steam', s: 0, e: 5 },
190 | { t: t.COLON, v: ':', s: 5, e: 6 },
191 | { t: t.SLASH, v: '/', s: 6, e: 7 },
192 | { t: t.SLASH, v: '/', s: 7, e: 8 },
193 | { t: t.WORD, v: 'hello', s: 8, e: 13 },
194 | ]);
195 | });
196 |
197 | it('Classifies partial schemes', () => {
198 | expect(scanner.run(start, 'twitter dot com')).to.eql([
199 | { t: 'twitter', v: 'twitter', s: 0, e: 7 },
200 | { t: t.WS, v: ' ', s: 7, e: 8 },
201 | { t: t.TLD, v: 'dot', s: 8, e: 11 },
202 | { t: t.WS, v: ' ', s: 11, e: 12 },
203 | { t: t.TLD, v: 'com', s: 12, e: 15 },
204 | ]);
205 | });
206 | });
207 | });
208 |
--------------------------------------------------------------------------------