├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .mocharc.yml ├── .npmignore ├── .nycrc.json ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.json ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── packages ├── linkify-element │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── linkify-element.mjs │ └── tsconfig.json ├── linkify-html │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── linkify-html.mjs │ └── tsconfig.json ├── linkify-jquery │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── linkify-jquery.mjs │ └── tsconfig.json ├── linkify-plugin-hashtag │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── hashtag.mjs │ │ └── index.mjs │ └── tsconfig.json ├── linkify-plugin-ip │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.mjs │ │ └── ip.mjs │ └── tsconfig.json ├── linkify-plugin-keyword │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.mjs │ │ └── keyword.mjs │ └── tsconfig.json ├── linkify-plugin-mention │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.mjs │ │ └── mention.mjs │ └── tsconfig.json ├── linkify-plugin-ticket │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.mjs │ │ └── ticket.mjs │ └── tsconfig.json ├── linkify-react │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── linkify-react.mjs │ └── tsconfig.json ├── linkify-string │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── linkify-string.mjs │ └── tsconfig.json └── linkifyjs │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── assign.mjs │ ├── fsm.mjs │ ├── linkify.mjs │ ├── multi.mjs │ ├── options.mjs │ ├── parser.mjs │ ├── regexp.mjs │ ├── scanner.mjs │ ├── text.mjs │ └── tlds.mjs │ └── tsconfig.json ├── rollup.config.js ├── tasks ├── benchmarks.js └── update-tlds.cjs └── test ├── chrome.conf.cjs ├── ci1.conf.cjs ├── ci2.conf.cjs ├── conf.cjs ├── firefox.conf.cjs ├── qunit ├── globals.js └── main.js ├── react-dom.mjs ├── react.mjs ├── setup.mjs └── spec ├── html ├── email.html ├── extra.html ├── linkified-alt.html ├── linkified-validate.html ├── linkified.html ├── options.mjs └── original.html ├── linkify-element.test.mjs ├── linkify-html.test.mjs ├── linkify-jquery.test.mjs ├── linkify-plugin-hashtag.test.mjs ├── linkify-plugin-ip.test.mjs ├── linkify-plugin-keyword.test.mjs ├── linkify-plugin-mention.test.mjs ├── linkify-plugin-ticket.test.mjs ├── linkify-react.test.mjs ├── linkify-string.test.mjs ├── linkifyjs.test.mjs └── linkifyjs ├── fsm.test.mjs ├── multi.test.mjs ├── options.test.mjs ├── parser.test.mjs └── scanner.test.mjs /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | tab_width = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.yml,*.md,package.json] 15 | indent_style = space 16 | tab_width = 2 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 22.x 18 | cache: 'npm' 19 | - run: npm install 20 | - run: npm run lint 21 | 22 | unit: 23 | needs: lint 24 | runs-on: ubuntu-latest 25 | strategy: 26 | matrix: 27 | node-version: [18.x, 20.x, 22.x, '24.x'] 28 | steps: 29 | - uses: actions/checkout@v3 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | cache: 'npm' 35 | - run: npm install 36 | - run: npm test 37 | 38 | integration: 39 | runs-on: ubuntu-latest 40 | needs: unit 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: actions/setup-node@v3 44 | with: 45 | node-version: 'lts/*' 46 | cache: 'npm' 47 | - run: npm install 48 | - name: Run coverage 49 | run: | 50 | npm run build:ci 51 | npm run test:coverage 52 | - name: Run browser tests 53 | if: ${{ github.secret_source == 'Actions' }} 54 | run: | 55 | npm run copy 56 | npm run test:ci 57 | env: 58 | BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} 59 | BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 60 | - name: Coveralls GitHub Action 61 | uses: coverallsapp/github-action@v2 62 | with: 63 | github-token: ${{ secrets.GITHUB_TOKEN }} 64 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: ['*'] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 'lts/*' 15 | cache: 'npm' 16 | - run: npm install 17 | - name: configure npm 18 | run: | 19 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | - name: publish to npm 23 | run: npm publish --workspaces 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | - name: postpublish 27 | run: npm run dist:postpublish 28 | - uses: actions/upload-artifact@v4 29 | with: 30 | name: linkifyjs 31 | path: dist/ 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Hidden system files 2 | .DS_Store 3 | .DS_Store? 4 | ._* 5 | .Trashes 6 | Thumbs.DB 7 | node_modules 8 | bower_components 9 | demo/dist/* 10 | 11 | # Logs 12 | logs 13 | *.log 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directory 37 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 38 | node_modules 39 | 40 | # Build files 41 | build 42 | !test/qunit/build 43 | test/qunit/vendor 44 | dist 45 | lib 46 | vendor 47 | *.tar.gz 48 | *.tgz 49 | 50 | # IntelliJ 51 | .idea 52 | *.iws 53 | *.iml 54 | *.ipr 55 | 56 | # Specific to plugin website branch 57 | .sass-cache 58 | .jekyll-cache 59 | _site 60 | _sass 61 | js 62 | 63 | # Generated typescript declarations 64 | *.d.ts 65 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - 'test/qunit/*' 3 | - 'test/conf.cjs' 4 | - 'test/*.conf.cjs' 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # All compiled code will be in the `lib` folders 2 | .nyc_output 3 | .sass-cache 4 | _sass 5 | _site 6 | amd 7 | assets 8 | bower_components 9 | build 10 | coverage 11 | demo 12 | js 13 | src 14 | templates 15 | test 16 | vendor 17 | 18 | # Files 19 | *.md 20 | *.yml 21 | *.tar.gz 22 | *.tgz 23 | .tags* 24 | .babelrc* 25 | .eslintrc* 26 | .editorconfig 27 | bower.json 28 | gulpfile.js 29 | testem.json 30 | *.config.js* 31 | tasks 32 | tsconfig.json 33 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [".mjs"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "useTabs": true, 6 | "overrides": [ 7 | { 8 | "files": ["*.json", "*.yml", "*.md"], 9 | "options": { 10 | "tabWidth": 2, 11 | "useTabs": false 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **Note:** This guide is a work in progress. Feel free to [submit an issue](https://github.com/nfrasser/linkifyjs/issues/new) if anything is confusing or unclear. 4 | 5 | ## How linkify works 6 | 7 | 8 | 9 | Linkify uses a two stage lexicographical analysis to detect patterns in plain text. 10 | 11 | The first stage, called the scanner, takes the input string and generates encompassing tokens that aggregate different types of components like domain names and TLDs. For example, substings like `http:` or `com` are converted into tokens named `PROTOCOL` and `TLD`, respectively. 12 | 13 | The second stage, the parser, takes this array of tokens and aggregates them into complete entities that are either become links or do not. For example, the tokens `PROTOCOL`, `SLASH`, `SLASH`, `DOMAIN`, `TLD` (appearing in that order) are grouped into a `URL` entity. These groups are called multitokens. 14 | 15 | A multitoken is either a link or not a link. Core linkify comes with these multitokens 16 | 17 | - **`Text`** is plain text (that contains no linkable entities) 18 | - **`Nl`** represents a single newline character 19 | - **`Email`** email address 20 | - **`URL`** 21 | 22 | The latter two are converted to links. `Nl` is in some cases converted to a `
` HTML tag. 23 | 24 | You can use the Token class system to [create plugins](#building-plugins). 25 | 26 | ## Style 27 | 28 | - ES6 Syntax (except tests for now) 29 | - Hard tabs with a width of 4 characters 30 | 31 | Keep your changes consistent with what's already there. 32 | 33 | ## Development 34 | 35 | ### Setup 36 | 37 | 1. Install the latest LTS version of [Node.js](https://nodejs.org/) (v18 or newer) 38 | 2. Fork and clone this repository to your machine 39 | 3. Navigate to the clone folder via command-line 40 | 4. Install dependencies with NPM: 41 | ```sh 42 | npm install 43 | ``` 44 | 45 | ### Building 46 | 47 | Linkify is built and tested in the command line with `npm` scripts. Build tasks have the following format. 48 | 49 | ``` 50 | npm run build 51 | ``` 52 | 53 | This transpiles ES6 to ES5 (via [Babel](http://babeljs.io/)) from `src/` into `dist/`. The Node.js modules have a `.cjs` extension. The dist folder is published to [NPM](https://www.npmjs.com/). Also generates browser-ready scripts (classic globals with `.js` and `.min.js` extensions, and ES modules with `.mjs` extensions) into the `dist/` folder. 54 | 55 | ### Running tests 56 | 57 | These tools are used for testing linkify: 58 | 59 | - [Mocha](https://mochajs.org/) is our primary test case framework 60 | - [ESLint](https://eslint.org) for code linting 61 | - [Istanbul](https://istanbul.js.org/) for code coverage analysis 62 | - [Karma](http://karma-runner.github.io/) is our browser test runner 63 | - [BrowserStack](https://www.browserstack.com) for cross-browser testing 64 | 65 | These are all configured to run in npm scripts. Tasks `npm test` and `npm run lint` are the most basic you can run. Other tasks include: 66 | 67 | - `npm run build` converts the `src` ES source code into browser- and Node.js- compatible JavaScript. It also outputs TypeScript definitions. 68 | - `npm run clean` removes all generated files 69 | - `npm run dist` cleans, builds and copies the final browser distribution bundle into the root `dist` directory 70 | 71 | ### Building plugins 72 | 73 | **Caution:** The plugin development API is in its very early stages and only supports very basic plugins. Updated features, APIs, and docs are in the works. 74 | 75 | Check out the sample [Hashtag plugin](https://github.com/nfrasser/linkifyjs/blob/main/packages/linkify-plugin-hashtag/src/hashtag.js) for an example. Check out the [Keyword plugin](https://github.com/nfrasser/linkifyjs/blob/main/packages/linkify-plugin-keyword/src/keyword.js) for more advanced usage. 76 | 77 | Register a new plugin with [`linkify.registerPlugin()`](https://linkify.js.org/docs/linkifyjs.html#linkifyregisterplugin-name-plugin). 78 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linkify 2 | 3 | [![npm version](https://badge.fury.io/js/linkifyjs.svg)](https://www.npmjs.com/package/linkifyjs) 4 | [![CI](https://github.com/nfrasser/linkifyjs/actions/workflows/ci.yml/badge.svg)](https://github.com/nfrasser/linkifyjs/actions/workflows/ci.yml) 5 | [![BrowserStack Status](https://automate.browserstack.com/badge.svg?badge_key=ekZJeEJEOVlYTjFPR2lYU0VBN0RSSEREbW5rUnJaT0Zzd2pKYzdzNC9qTT0tLW44YkxiSUo4REFHNm9EUC8vdkxBblE9PQ==--45e7e07e1b4fbe9be5d9aaa4af8d183749a4bd25)](https://automate.browserstack.com/public-build/ekZJeEJEOVlYTjFPR2lYU0VBN0RSSEREbW5rUnJaT0Zzd2pKYzdzNC9qTT0tLW44YkxiSUo4REFHNm9EUC8vdkxBblE9PQ==--45e7e07e1b4fbe9be5d9aaa4af8d183749a4bd25) 6 | [![Coverage Status](https://coveralls.io/repos/github/nfrasser/linkifyjs/badge.svg?branch=main)](https://coveralls.io/github/nfrasser/linkifyjs?branch=main) 7 | 8 | Linkify is a JavaScript plugin. Use Linkify to find links in plain-text and 9 | convert them to HTML <a> tags. It automatically highlights URLs, 10 | #hashtags, @mentions and more. 11 | 12 | > [!NOTE] 13 | > In November 2024, linkifyjs was transferred from its previous namespace [Hypercontext](https://github.com/Hypercontext) to its primary maintainer [@nfrasser](https://github.com/nfrasser), as Hypercontext winds down operations. 14 | 15 | **Jump to** 16 | 17 | - [Features](#features) 18 | - [Demo](#demo) 19 | - [Installation and Usage](#installation-and-usage) 20 | - [Browser Support](#browser-support) 21 | - [Node.js Support](#nodejs-support) 22 | - [Downloads](#downloads) 23 | - [API Documentation](#api-documentation) 24 | - [Contributing](#contributing) 25 | - [License](#license) 26 | 27 | ## Features 28 | 29 | - Detect URLs and email addresses 30 | - #hashtag, @mention and #-ticket plugins 31 | - React and jQuery support 32 | - Multi-language and emoji support 33 | - Custom link plugins 34 | - Fast, accurate and small footprint (~20kB minified, ~11kB gzipped) 35 | - 99% test coverage 36 | - Compatible with all modern browsers (Internet Explorer 11 and up) 37 | 38 | ## Demo 39 | 40 | [Launch demo](https://linkify.js.org/#demo) 41 | 42 | ## Installation and Usage 43 | 44 | [View full documentation](https://linkify.js.org/docs/). 45 | 46 | Download the [latest release](https://github.com/nfrasser/linkifyjs/releases) for direct use in the browser, or install via [NPM](https://www.npmjs.com/): 47 | 48 | ``` 49 | npm install linkifyjs linkify-html 50 | ``` 51 | 52 | ### Quick Start 53 | 54 | When developing in an environment with JavaScript module loader such as Webpack, 55 | use an `import` statement: 56 | 57 | ```js 58 | import * as linkify from 'linkifyjs'; 59 | import linkifyHtml from 'linkify-html'; 60 | ``` 61 | 62 | Or in Node.js with CommonJS modules 63 | 64 | ```js 65 | const linkify = require('linkifyjs'); 66 | const linkifyHtml = require('linkify-html'); 67 | ``` 68 | 69 | **Note:** When linkify-ing text that does not contain HTML, install and use the 70 | `linkify-string` package instead of `linkify-html`. [Read more about Linkify's 71 | interfaces](https://linkify.js.org/docs/interfaces.html). 72 | 73 | ### Usage 74 | 75 | #### Example 1: Convert all links to <a> tags in the given string 76 | 77 | ```js 78 | const options = { defaultProtocol: 'https' }; 79 | linkifyHtml('Any links to github.com here? If not, contact test@example.com', options); 80 | ``` 81 | 82 | Returns the following string: 83 | 84 | ```js 85 | 'Any links to github.com here? If not, contact test@example.com'; 86 | ``` 87 | 88 | To modify the resulting links with a target attribute, class name and more, [use 89 | the available options](https://linkify.js.org/docs/options.html). 90 | 91 | #### Example 2: Find all links in the given string 92 | 93 | ```js 94 | linkify.find('Any links to github.com here? If not, contact test@example.com'); 95 | ``` 96 | 97 | Returns the following array 98 | 99 | ```js 100 | [ 101 | { 102 | type: 'url', 103 | value: 'github.com', 104 | isLink: true, 105 | href: 'http://github.com', 106 | start: 13, 107 | end: 23, 108 | }, 109 | { 110 | type: 'email', 111 | value: 'test@example.com', 112 | isLink: true, 113 | href: 'mailto:test@example.com', 114 | start: 46, 115 | end: 62, 116 | }, 117 | ]; 118 | ``` 119 | 120 | #### Example 3: Check whether a string is a valid link: 121 | 122 | Check if as string is a valid URL or email address: 123 | 124 | ```js 125 | linkify.test('github.com'); // true 126 | ``` 127 | 128 | Check if a string is a valid email address: 129 | 130 | ```js 131 | linkify.test('github.com', 'email'); // false 132 | linkify.test('noreply@github.com', 'email'); // true 133 | ``` 134 | 135 | ### Usage with React, jQuery or the browser DOM 136 | 137 | [Read the interface documentation](https://linkify.js.org/docs/interfaces.html) to learn how to use linkify when working with a specific JavaScript environment such as React. 138 | 139 | ### Plugins for @mentions, #hashtags and more 140 | 141 | By default Linkify will only detect and highlight web URLs and e-mail addresses. 142 | Plugins for @mentions, #hashtags and more may be installed separately. [Read the 143 | plugin documentation](https://linkify.js.org/docs/plugins.html). 144 | 145 | ## Browser Support 146 | 147 | Linkify natively supports all modern browsers. Older browsers supported with [Babel](https://babeljs.io). 148 | 149 | ## Node.js Support 150 | 151 | Linkify is tested on Node.js 18 and up. Older Node.js versions are unofficially 152 | supported. 153 | 154 | ## Downloads 155 | 156 | Download the [**latest release**](https://github.com/nfrasser/linkifyjs/releases) 157 | 158 | ## API Documentation 159 | 160 | View full documentation at [linkify.js.org/docs](https://linkify.js.org/docs/) 161 | 162 | ## Contributing 163 | 164 | Check out [CONTRIBUTING.md](https://github.com/nfrasser/linkifyjs/blob/main/CONTRIBUTING.md). 165 | 166 | ## License 167 | 168 | MIT 169 | 170 | ## Authors 171 | 172 | Linkify is made with ❤️ by [@nfrasser](https://github.com/nfrasser) 173 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": false, 7 | "loose": true, 8 | "targets": { 9 | "node": "10", 10 | "browsers": ["defaults", "maintained node versions"] 11 | } 12 | } 13 | ] 14 | ], 15 | "babelrcRoots": [".", "packages/*"] 16 | } 17 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import mocha from 'eslint-plugin-mocha'; 2 | import babel from '@babel/eslint-plugin'; 3 | import globals from 'globals'; 4 | import babelParser from '@babel/eslint-parser'; 5 | import js from '@eslint/js'; 6 | 7 | export default [ 8 | js.configs.recommended, 9 | mocha.configs.recommended, 10 | { 11 | ignores: [ 12 | '**/.DS_Store', 13 | '**/.env', 14 | '**/.env.*', 15 | '!**/.env.example', 16 | '**/_sass', 17 | '**/_site', 18 | '**/coverage', 19 | '**/node_modules', 20 | '**/package-lock.json', 21 | 'dist/*', 22 | 'packages/*/dist/*', 23 | 'test/qunit/vendor/*', 24 | ], 25 | }, 26 | { 27 | plugins: { 28 | '@babel': babel, 29 | }, 30 | 31 | languageOptions: { 32 | globals: { 33 | ...globals.browser, 34 | ...globals.node, 35 | ...globals.jquery, 36 | ...globals.amd, 37 | ...globals.mocha, 38 | __base: false, 39 | expect: false, 40 | }, 41 | 42 | parser: babelParser, 43 | ecmaVersion: 6, 44 | sourceType: 'module', 45 | 46 | parserOptions: { 47 | requireConfigFile: false, 48 | }, 49 | }, 50 | 51 | rules: { 52 | curly: 2, 53 | eqeqeq: ['error', 'smart'], 54 | quotes: [2, 'single', 'avoid-escape'], 55 | semi: 2, 56 | 57 | 'no-unused-vars': [ 58 | 'error', 59 | { 60 | caughtErrors: 'none', 61 | varsIgnorePattern: 'should|expect', 62 | }, 63 | ], 64 | 65 | 'mocha/no-mocha-arrows': 0, 66 | }, 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkify", 3 | "type": "module", 4 | "version": "4.3.1", 5 | "description": "Intelligent link recognition, made easy", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/nfrasser/linkifyjs.git" 9 | }, 10 | "scripts": { 11 | "build": "npm run build --workspaces", 12 | "build:test": "rollup -c rollup.config.js", 13 | "build:ci": "run-s build build:test", 14 | "clean": "rm -rf dist", 15 | "copy": "copyfiles -u 3 packages/*/dist/*.js packages/*/dist/*/LICENSE dist", 16 | "copy:license": "copyfiles LICENSE dist", 17 | "coverage": "nyc report --reporter=text-lcov | coveralls", 18 | "dist": "run-s clean prepack copy copy:license", 19 | "dist:postpublish": "run-s clean copy copy:license", 20 | "lint": "eslint .", 21 | "format": "prettier --plugin-search-dir . --write .", 22 | "prepack": "npm run prepack --workspaces", 23 | "pretest": "npm run build -- --silent", 24 | "test": "mocha --recursive", 25 | "test:coverage": "c8 --reporter=lcov --reporter=text mocha --recursive", 26 | "test:ci": "run-s test:ci1 test:ci2", 27 | "test:ci1": "karma start test/ci1.conf.cjs --single-run", 28 | "test:ci2": "karma start test/ci2.conf.cjs --single-run", 29 | "tlds": "node tasks/update-tlds.cjs" 30 | }, 31 | "author": "Nick Frasser (https://nfrasser.com)", 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@babel/core": "^7.13.10", 35 | "@babel/eslint-parser": "^7.13.10", 36 | "@babel/eslint-plugin": "^7.13.10", 37 | "@babel/preset-env": "^7.13.10", 38 | "@eslint/eslintrc": "^3.1.0", 39 | "@eslint/js": "^9.13.0", 40 | "@nfrasser/simple-html-tokenizer": "==0.5.11-4", 41 | "@rollup/plugin-babel": "^6.0.3", 42 | "@rollup/plugin-commonjs": "^28.0.3", 43 | "@rollup/plugin-node-resolve": "^16.0.1", 44 | "@rollup/plugin-replace": "^6.0.2", 45 | "@rollup/plugin-terser": "^0.4.4", 46 | "c8": "^10.1.2", 47 | "chai": "^5.2.0", 48 | "copyfiles": "^2.4.1", 49 | "coveralls": "^3.1.0", 50 | "eslint": "^9.13.0", 51 | "eslint-plugin-mocha": "^11.0.0", 52 | "globals": "^16.0.0", 53 | "jquery": "^3.6.0", 54 | "jsdom": "^16.5.0", 55 | "karma": "^6.3.16", 56 | "karma-browserstack-launcher": "^1.6.0", 57 | "karma-chrome-launcher": "^3.1.0", 58 | "karma-firefox-launcher": "^2.1.0", 59 | "karma-qunit": "^4.1.2", 60 | "mocha": "^11.2.2", 61 | "npm-run-all": "^4.1.5", 62 | "prettier": "^3.0.3", 63 | "punycode": "^2.1.1", 64 | "qunit": "^2.14.1", 65 | "react": "^18.0.0", 66 | "react-dom": "^18.0.0", 67 | "rollup": "^4.40.2", 68 | "sinon": "^20.0.0", 69 | "typescript": "^5.0.2" 70 | }, 71 | "private": true, 72 | "engines": { 73 | "node": ">=8" 74 | }, 75 | "workspaces": [ 76 | "./packages/linkifyjs", 77 | "./packages/linkify-plugin-*", 78 | "./packages/*" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /packages/linkify-element/.npmignore: -------------------------------------------------------------------------------- 1 | ../../.npmignore -------------------------------------------------------------------------------- /packages/linkify-element/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-element/README.md: -------------------------------------------------------------------------------- 1 | linkify-element 2 | === 3 | 4 | [![npm version](https://badge.fury.io/js/linkify-element.svg)](https://www.npmjs.com/package/linkify-element) 5 | 6 | [Linkify](https://linkify.js.org/) DOM Element Interface. Use `linkify-element` to detect URLs, email addresses and more within native HTML elements and replace them with anchor tags. 7 | 8 | Note that `linkify-element` is included with `linkify-jquery`, so you do not have to install it if you are using `linkify-jquery`. 9 | 10 | ## Installation 11 | 12 | Install from the command line with NPM 13 | 14 | ``` 15 | npm install linkifyjs linkify-element 16 | ``` 17 | 18 | Import into your JavaScript with `require` 19 | ```js 20 | const linkifyElement = require('linkify-element'); 21 | ``` 22 | or with ES modules 23 | ```js 24 | import linkifyElement from 'linkify-element'; 25 | ``` 26 | 27 | ## Usage 28 | 29 | [Read the full documentation](https://linkify.js.org/docs/linkify-element.html). 30 | 31 | ## License 32 | 33 | MIT 34 | -------------------------------------------------------------------------------- /packages/linkify-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkify-element", 3 | "type": "module", 4 | "version": "4.3.1", 5 | "description": "Browser DOM element interface for linkifyjs", 6 | "main": "dist/linkify-element.cjs", 7 | "module": "dist/linkify-element.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-element" 19 | }, 20 | "keywords": [ 21 | "link", 22 | "autolink", 23 | "url", 24 | "email", 25 | "browser" 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 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/linkify-element/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { linkifyInterface } from '../../rollup.config.js'; 2 | 3 | export default linkifyInterface('element', { globalName: 'linkifyElement' }); 4 | -------------------------------------------------------------------------------- /packages/linkify-element/src/linkify-element.mjs: -------------------------------------------------------------------------------- 1 | import { tokenize, Options } from 'linkifyjs'; 2 | 3 | const HTML_NODE = 1, TXT_NODE = 3; 4 | 5 | /** 6 | * @param {HTMLElement} parent 7 | * @param {Text | HTMLElement | ChildNode} oldChild 8 | * @param {Array} newChildren 9 | */ 10 | function replaceChildWithChildren(parent, oldChild, newChildren) { 11 | let lastNewChild = newChildren[newChildren.length - 1]; 12 | parent.replaceChild(lastNewChild, oldChild); 13 | for (let i = newChildren.length - 2; i >= 0; i--) { 14 | parent.insertBefore(newChildren[i], lastNewChild); 15 | lastNewChild = newChildren[i]; 16 | } 17 | } 18 | 19 | /** 20 | * @param {import('linkifyjs').MultiToken[]} tokens 21 | * @param {import('linkifyjs').Options} options 22 | * @param {Document} doc Document implementation 23 | * @returns {Array} 24 | */ 25 | function tokensToNodes(tokens, options, doc) { 26 | const result = []; 27 | for (let i = 0; i < tokens.length; i++) { 28 | const token = tokens[i]; 29 | if (token.t === 'nl' && options.get('nl2br')) { 30 | result.push(doc.createElement('br')); 31 | } else if (!token.isLink || !options.check(token)) { 32 | result.push(doc.createTextNode(token.toString())); 33 | } else { 34 | result.push(options.render(token)); 35 | } 36 | } 37 | 38 | return result; 39 | } 40 | 41 | /** 42 | * Requires document.createElement 43 | * @param {HTMLElement | ChildNode} element 44 | * @param {import('linkifyjs').Options} options 45 | * @param {Document} doc 46 | * @returns {HTMLElement} 47 | */ 48 | function linkifyElementHelper(element, options, doc) { 49 | 50 | // Can the element be linkified? 51 | if (!element || element.nodeType !== HTML_NODE) { 52 | throw new Error(`Cannot linkify ${element} - Invalid DOM Node type`); 53 | } 54 | 55 | // Is this element already a link? 56 | if (element.tagName === 'A' || options.ignoreTags.indexOf(element.tagName) >= 0) { 57 | // No need to linkify 58 | return element; 59 | } 60 | 61 | let childElement = element.firstChild; 62 | 63 | while (childElement) { 64 | let str, tokens, nodes; 65 | 66 | switch (childElement.nodeType) { 67 | case HTML_NODE: 68 | linkifyElementHelper(childElement, options, doc); 69 | break; 70 | case TXT_NODE: { 71 | str = childElement.nodeValue; 72 | tokens = tokenize(str); 73 | 74 | if (tokens.length === 0 || tokens.length === 1 && tokens[0].t === 'text') { 75 | // No node replacement required 76 | break; 77 | } 78 | 79 | nodes = tokensToNodes(tokens, options, doc); 80 | 81 | // Swap out the current child for the set of nodes 82 | replaceChildWithChildren(element, childElement, nodes); 83 | 84 | // so that the correct sibling is selected next 85 | childElement = nodes[nodes.length - 1]; 86 | 87 | break; 88 | } 89 | } 90 | 91 | childElement = childElement.nextSibling; 92 | } 93 | 94 | return element; 95 | } 96 | 97 | /** 98 | * @param {Document} doc The document implementaiton 99 | */ 100 | function getDefaultRender(doc) { 101 | return ({ tagName, attributes, content, eventListeners }) => { 102 | const link = doc.createElement(tagName); 103 | for (const attr in attributes) { 104 | link.setAttribute(attr, attributes[attr]); 105 | } 106 | 107 | if (eventListeners && link.addEventListener) { 108 | for (const event in eventListeners) { 109 | link.addEventListener(event, eventListeners[event]); 110 | } 111 | } 112 | 113 | link.appendChild(doc.createTextNode(content)); 114 | return link; 115 | }; 116 | } 117 | 118 | /** 119 | * Recursively traverse the given DOM node, find all links in the text and 120 | * convert them to anchor tags. 121 | * 122 | * @param {HTMLElement} element A DOM node to linkify 123 | * @param {import('linkifyjs').Opts} [opts] linkify options 124 | * @param {Document} [doc] (optional) window.document implementation, if differs from global 125 | * @returns {HTMLElement} 126 | */ 127 | export default function linkifyElement(element, opts = null, doc = null) { 128 | try { 129 | doc = doc || document || window && window.document || global && global.document; 130 | } catch (_) { /* do nothing for now */ } 131 | 132 | if (!doc) { 133 | throw new Error( 134 | 'Cannot find document implementation. ' + 135 | 'If you are in a non-browser environment like Node.js, ' + 136 | 'pass the document implementation as the third argument to linkifyElement.' 137 | ); 138 | } 139 | 140 | const options = new Options(opts, getDefaultRender(doc)); 141 | return linkifyElementHelper(element, options, doc); 142 | } 143 | 144 | // Maintain reference to the recursive helper and option-normalization for use 145 | // in linkify-jquery 146 | linkifyElement.helper = linkifyElementHelper; 147 | linkifyElement.getDefaultRender = getDefaultRender; 148 | 149 | /** 150 | * @param {import('linkifyjs').Opts | import('linkifyjs').Options} opts 151 | * @param {Document} doc 152 | */ 153 | linkifyElement.normalize = (opts, doc) => new Options(opts, getDefaultRender(doc)); 154 | 155 | -------------------------------------------------------------------------------- /packages/linkify-element/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["dist/linkify-element.cjs", "dist/linkify-element.mjs"], 3 | "exclude": [], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "maxNodeModuleJsDepth": 2 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/linkify-html/.npmignore: -------------------------------------------------------------------------------- 1 | ../../.npmignore -------------------------------------------------------------------------------- /packages/linkify-html/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-html/README.md: -------------------------------------------------------------------------------- 1 | linkify-html 2 | === 3 | 4 | [![npm version](https://badge.fury.io/js/linkify-html.svg)](https://www.npmjs.com/package/linkify-html) 5 | 6 | [Linkify](https://linkify.js.org/) HTML String Interface. Use `linkify-html` to detect URLs, email addresses and more in strings that contain HTML markup and replace them with anchor `` tags. 7 | 8 | ## Installation 9 | 10 | Install from the command line with NPM 11 | 12 | ``` 13 | npm install linkifyjs linkify-html 14 | ``` 15 | 16 | Import into your JavaScript with `require` 17 | ```js 18 | const linkifyHtml = require('linkify-html'); 19 | ``` 20 | or with ES modules 21 | 22 | ```js 23 | import linkifyHtml from 'linkify-html'; 24 | ``` 25 | 26 | ## Usage 27 | 28 | [Read the full documentation](https://linkify.js.org/docs/linkify-html.html). 29 | 30 | ## License 31 | 32 | MIT 33 | -------------------------------------------------------------------------------- /packages/linkify-html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkify-html", 3 | "type": "module", 4 | "version": "4.3.1", 5 | "description": "HTML String interface for linkifyjs", 6 | "main": "dist/linkify-html.cjs", 7 | "module": "dist/linkify-html.mjs", 8 | "scripts": { 9 | "build": "rollup -c rollup.config.js", 10 | "clean": "rm -rf lib dist *.tgz *.d.ts", 11 | "copy:license": "copyfiles -f ../../node_modules/@nfrasser/simple-html-tokenizer/LICENSE dist/simple-html-tokenizer", 12 | "prepack": "run-s clean build tsc copy:license", 13 | "tsc": "tsc", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/nfrasser/linkifyjs.git", 19 | "directory": "packages/linkify-html" 20 | }, 21 | "keywords": [ 22 | "link", 23 | "autolink", 24 | "url", 25 | "email" 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 | "devDependencies": { 34 | "@nfrasser/simple-html-tokenizer": "==0.5.11-4" 35 | }, 36 | "peerDependencies": { 37 | "linkifyjs": "^4.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/linkify-html/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { linkifyInterface } from '../../rollup.config.js'; 2 | export default linkifyInterface('html', { globalName: 'linkifyHtml' }); 3 | -------------------------------------------------------------------------------- /packages/linkify-html/src/linkify-html.mjs: -------------------------------------------------------------------------------- 1 | import { tokenize as htmlTokenize } from '@nfrasser/simple-html-tokenizer'; 2 | import { tokenize, Options } from 'linkifyjs'; 3 | 4 | const LinkifyResult = 'LinkifyResult'; 5 | const StartTag = 'StartTag'; 6 | const EndTag = 'EndTag'; 7 | const Chars = 'Chars'; 8 | const Comment = 'Comment'; 9 | const Doctype = 'Doctype'; 10 | 11 | /** 12 | * @param {string} str html string to link 13 | * @param {import('linkifyjs').Opts} [opts] linkify options 14 | * @returns {string} resulting string 15 | */ 16 | export default function linkifyHtml(str, opts = {}) { 17 | // `tokens` and `token` in this section refer to tokens generated by the 18 | // HTML parser, not linkify's parser 19 | const tokens = htmlTokenize(str); 20 | const linkifiedTokens = []; 21 | const linkified = []; 22 | 23 | const options = new Options(opts, defaultRender); 24 | 25 | // Linkify the tokens given by the parser 26 | for (let i = 0; i < tokens.length; i++) { 27 | const token = tokens[i]; 28 | 29 | if (token.type === StartTag) { 30 | linkifiedTokens.push(token); 31 | 32 | // Ignore all the contents of ignored tags 33 | const tagName = token.tagName.toUpperCase(); 34 | const isIgnored = tagName === 'A' || options.ignoreTags.indexOf(tagName) >= 0; 35 | if (!isIgnored) { 36 | continue; 37 | } 38 | 39 | let preskipLen = linkifiedTokens.length; 40 | skipTagTokens(tagName, tokens, ++i, linkifiedTokens); 41 | i += linkifiedTokens.length - preskipLen - 1; 42 | } else if (token.type !== Chars) { 43 | // Skip this token, it's not important 44 | linkifiedTokens.push(token); 45 | } else { 46 | // Valid text token, linkify it! 47 | const linkifedChars = linkifyChars(token.chars, options); 48 | linkifiedTokens.push.apply(linkifiedTokens, linkifedChars); 49 | } 50 | } 51 | 52 | // Convert the tokens back into a string 53 | for (let i = 0; i < linkifiedTokens.length; i++) { 54 | const token = linkifiedTokens[i]; 55 | switch (token.type) { 56 | case LinkifyResult: 57 | linkified.push(token.rendered); 58 | break; 59 | case StartTag: { 60 | let link = '<' + token.tagName; 61 | if (token.attributes.length > 0) { 62 | link += ' ' + attributeArrayToStrings(token.attributes).join(' '); 63 | } 64 | if (token.selfClosing) { 65 | link += ' /'; 66 | } 67 | link += '>'; 68 | linkified.push(link); 69 | break; 70 | } 71 | case EndTag: 72 | linkified.push(``); 73 | break; 74 | case Chars: 75 | linkified.push(escapeText(token.chars)); 76 | break; 77 | case Comment: 78 | linkified.push(``); 79 | break; 80 | case Doctype: { 81 | let doctype = `'; 89 | linkified.push(doctype); 90 | break; 91 | } 92 | } 93 | } 94 | 95 | return linkified.join(''); 96 | } 97 | 98 | /** 99 | `tokens` and `token` in this section referes to tokens returned by 100 | `linkify.tokenize`. `linkified` will contain HTML Parser-style tokens 101 | @param {string} 102 | @param {import('linkifyjs').Options} 103 | */ 104 | function linkifyChars(str, options) { 105 | const tokens = tokenize(str); 106 | const result = []; 107 | 108 | for (let i = 0; i < tokens.length; i++) { 109 | const token = tokens[i]; 110 | if (token.t === 'nl' && options.get('nl2br')) { 111 | result.push({ 112 | type: StartTag, 113 | tagName: 'br', 114 | attributes: [], 115 | selfClosing: true, 116 | }); 117 | } else if (!token.isLink || !options.check(token)) { 118 | result.push({ type: Chars, chars: token.toString() }); 119 | } else { 120 | result.push({ 121 | type: LinkifyResult, 122 | rendered: options.render(token), 123 | }); 124 | } 125 | } 126 | 127 | return result; 128 | } 129 | 130 | /** 131 | Returns a list of tokens skipped until the closing tag of tagName. 132 | 133 | * `tagName` is the closing tag which will prompt us to stop skipping 134 | * `tokens` is the array of tokens generated by HTML5Tokenizer which 135 | * `i` is the index immediately after the opening tag to skip 136 | * `skippedTokens` is an array which skipped tokens are being pushed into 137 | 138 | Caveats 139 | 140 | * Assumes that i is the first token after the given opening tagName 141 | * The closing tag will be skipped, but nothing after it 142 | * Will track whether there is a nested tag of the same type 143 | */ 144 | function skipTagTokens(tagName, tokens, i, skippedTokens) { 145 | // number of tokens of this type on the [fictional] stack 146 | let stackCount = 1; 147 | 148 | while (i < tokens.length && stackCount > 0) { 149 | let token = tokens[i]; 150 | 151 | if (token.type === StartTag && token.tagName.toUpperCase() === tagName) { 152 | // Nested tag of the same type, "add to stack" 153 | stackCount++; 154 | } else if (token.type === EndTag && token.tagName.toUpperCase() === tagName) { 155 | // Closing tag 156 | stackCount--; 157 | } 158 | 159 | skippedTokens.push(token); 160 | i++; 161 | } 162 | 163 | // Note that if stackCount > 0 here, the HTML is probably invalid 164 | return skippedTokens; 165 | } 166 | 167 | function defaultRender({ tagName, attributes, content }) { 168 | return `<${tagName} ${attributesToString(attributes)}>${escapeText(content)}`; 169 | } 170 | 171 | function escapeText(text) { 172 | return text.replace(//g, '>'); 173 | } 174 | 175 | function escapeAttr(attr) { 176 | return attr.replace(/"/g, '"'); 177 | } 178 | 179 | function attributesToString(attributes) { 180 | const result = []; 181 | for (const attr in attributes) { 182 | const val = attributes[attr] + ''; 183 | result.push(`${attr}="${escapeAttr(val)}"`); 184 | } 185 | return result.join(' '); 186 | } 187 | 188 | function attributeArrayToStrings(attrs) { 189 | const attrStrs = []; 190 | for (let i = 0; i < attrs.length; i++) { 191 | const name = attrs[i][0]; 192 | const value = attrs[i][1] + ''; 193 | attrStrs.push(`${name}="${escapeAttr(value)}"`); 194 | } 195 | return attrStrs; 196 | } 197 | -------------------------------------------------------------------------------- /packages/linkify-html/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["dist/linkify-html.cjs", "dist/linkify-html.mjs"], 3 | "exclude": [], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "maxNodeModuleJsDepth": 1 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/linkify-jquery/.npmignore: -------------------------------------------------------------------------------- 1 | ../../.npmignore -------------------------------------------------------------------------------- /packages/linkify-jquery/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-jquery/README.md: -------------------------------------------------------------------------------- 1 | linkify-jquery 2 | === 3 | 4 | [![npm version](https://badge.fury.io/js/linkify-jquery.svg)](https://www.npmjs.com/package/linkify-jquery) 5 | 6 | [Linkify](https://linkify.js.org/) jQuery plugin. Also available in vanilla JavaScript via `linkify-element`. Use it to detect URLs, email addresses and more and wrap them with anchor `` tags. 7 | 8 | ## Installation 9 | 10 | Install from the command line with NPM 11 | 12 | ``` 13 | npm install linkifyjs linkify-jquery 14 | ``` 15 | 16 | Import into your JavaScript with `require` 17 | ```js 18 | const $ = require('jquery'); 19 | require('linkify-jquery') 20 | ``` 21 | or with ES modules 22 | ```js 23 | import $ from 'jquery'; 24 | import 'linkify-jquery'; 25 | ``` 26 | 27 | If a `window.document` global is not available in your environment, provide it manually instead as follows. 28 | 29 | With `require`: 30 | ```js 31 | require('linkify-jquery')($, document); 32 | ``` 33 | or with ES modules: 34 | ```js 35 | import linkifyJq from 'linkify-jquery'; 36 | linkifyJq($, document); 37 | ``` 38 | 39 | ## Usage 40 | 41 | [Read the full documentation](https://linkify.js.org/docs/linkify-jquery.html). 42 | 43 | ## License 44 | 45 | MIT 46 | -------------------------------------------------------------------------------- /packages/linkify-jquery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkify-jquery", 3 | "type": "module", 4 | "version": "4.3.1", 5 | "description": "jQuery interface for linkifyjs", 6 | "main": "dist/linkify-jquery.cjs", 7 | "module": "dist/linkify-jquery.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-jquery" 19 | }, 20 | "keywords": [ 21 | "link", 22 | "autolink", 23 | "url", 24 | "email", 25 | "jquery", 26 | "browser" 27 | ], 28 | "author": "Nick Frasser (https://nfrasser.com)", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/nfrasser/linkifyjs/issues" 32 | }, 33 | "homepage": "https://linkify.js.org", 34 | "peerDependencies": { 35 | "jquery": ">= 1.11.0", 36 | "linkifyjs": "^4.0.0" 37 | }, 38 | "devDependencies": { 39 | "linkify-element": "*" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/linkify-jquery/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { linkifyInterface } from '../../rollup.config.js'; 2 | 3 | export default linkifyInterface('jquery', { 4 | globalName: false, 5 | globals: { jquery: 'jQuery' }, 6 | external: ['jquery'], 7 | }); 8 | -------------------------------------------------------------------------------- /packages/linkify-jquery/src/linkify-jquery.mjs: -------------------------------------------------------------------------------- 1 | import jQuery from 'jquery'; 2 | import linkifyElement from 'linkify-element'; 3 | 4 | // Applies the plugin to jQuery 5 | /** 6 | * 7 | * @param {any} $ the global jQuery object 8 | * @param {Document} [doc] (optional) browser document implementation 9 | * @returns 10 | */ 11 | export default function apply($, doc = false) { 12 | 13 | $.fn = $.fn || {}; 14 | if (typeof $.fn.linkify === 'function') { 15 | // Already applied 16 | return; 17 | } 18 | 19 | try { 20 | doc = doc || document || (window && window.document) || global && global.document; 21 | } catch (e) { /* do nothing for now */ } 22 | 23 | if (!doc) { 24 | throw new Error( 25 | 'Cannot find document implementation. ' + 26 | 'If you are in a non-browser environment like Node.js, ' + 27 | 'pass the document implementation as the second argument to linkify-jquery' 28 | ); 29 | } 30 | 31 | function jqLinkify(opts) { 32 | const options = linkifyElement.normalize(opts, doc); 33 | return this.each(function () { 34 | linkifyElement.helper(this, options, doc); 35 | }); 36 | } 37 | 38 | $.fn.linkify = jqLinkify; 39 | 40 | $(function () { 41 | $('[data-linkify]').each(function () { 42 | const $this = $(this); 43 | const data = $this.data(); 44 | const target = data.linkify; 45 | const nl2br = data.linkifyNl2br; 46 | 47 | const opts = { 48 | nl2br: !!nl2br && nl2br !== 0 && nl2br !== 'false' 49 | }; 50 | 51 | if ('linkifyAttributes' in data) { 52 | opts.attributes = data.linkifyAttributes; 53 | } 54 | 55 | if ('linkifyDefaultProtocol' in data) { 56 | opts.defaultProtocol = data.linkifyDefaultProtocol; 57 | } 58 | 59 | if ('linkifyEvents' in data) { 60 | opts.events = data.linkifyEvents; 61 | } 62 | 63 | if ('linkifyFormat' in data) { 64 | opts.format = data.linkifyFormat; 65 | } 66 | 67 | if ('linkifyFormatHref' in data) { 68 | opts.formatHref = data.linkifyFormatHref; 69 | } 70 | 71 | if ('linkifyTagname' in data) { 72 | opts.tagName = data.linkifyTagname; 73 | } 74 | 75 | if ('linkifyTarget' in data) { 76 | opts.target = data.linkifyTarget; 77 | } 78 | 79 | if ('linkifyRel' in data) { 80 | opts.rel = data.linkifyRel; 81 | } 82 | 83 | if ('linkifyValidate' in data) { 84 | opts.validate = data.linkifyValidate; 85 | } 86 | 87 | if ('linkifyIgnoreTags' in data) { 88 | opts.ignoreTags = data.linkifyIgnoreTags; 89 | } 90 | 91 | if ('linkifyClassName' in data) { 92 | opts.className = data.linkifyClassName; 93 | } 94 | 95 | const $target = target === 'this' ? $this : $this.find(target); 96 | $target.linkify(opts); 97 | }); 98 | }); 99 | } 100 | 101 | // Try applying to the globally-defined jQuery element, if possible 102 | try { apply(jQuery); } catch (e) { /**/ } 103 | 104 | // Try assigning linkifyElement to the browser scope 105 | try { window.linkifyElement = linkifyElement; } catch (e) { /**/ } 106 | -------------------------------------------------------------------------------- /packages/linkify-jquery/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["dist/linkify-jquery.cjs", "dist/linkify-jquery.mjs"], 3 | "exclude": [], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "maxNodeModuleJsDepth": 1 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/linkify-plugin-hashtag/.npmignore: -------------------------------------------------------------------------------- 1 | ../../.npmignore -------------------------------------------------------------------------------- /packages/linkify-plugin-hashtag/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-plugin-hashtag/README.md: -------------------------------------------------------------------------------- 1 | linkify-plugin-hashtag 2 | === 3 | 4 | [![npm version](https://badge.fury.io/js/linkify-plugin-hashtag.svg)](https://www.npmjs.com/package/linkify-plugin-hashtag) 5 | 6 | Detect and convert Twitter-style #hashtags to `` 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-hashtag 14 | ``` 15 | 16 | Import into your JavaScript with `require` 17 | ```js 18 | const linkify = require('linkifyjs') 19 | require('linkify-plugin-hashtag'); 20 | ``` 21 | or with ES modules 22 | 23 | ```js 24 | import * as linkify from 'linkifyjs'; 25 | import 'linkify-plugin-hashtag'; 26 | ``` 27 | 28 | ## Usage 29 | 30 | [Read the full documentation](https://linkify.js.org/docs/plugin-hashtag.html). 31 | 32 | ## License 33 | 34 | MIT 35 | -------------------------------------------------------------------------------- /packages/linkify-plugin-hashtag/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkify-plugin-hashtag", 3 | "type": "module", 4 | "version": "4.3.1", 5 | "description": "Hashtag plugin for linkifyjs", 6 | "main": "dist/linkify-plugin-hashtag.cjs", 7 | "module": "dist/linkify-plugin-hashtag.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-hashtag" 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-hashtag/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { linkifyPlugin } from '../../rollup.config.js'; 2 | export default linkifyPlugin('hashtag'); 3 | -------------------------------------------------------------------------------- /packages/linkify-plugin-hashtag/src/hashtag.mjs: -------------------------------------------------------------------------------- 1 | import { State, createTokenClass } from 'linkifyjs'; 2 | 3 | // Create a new token that class that the parser emits when it finds a hashtag 4 | const HashtagToken = createTokenClass('hashtag', { isLink: true }); 5 | 6 | /** 7 | * @type {import('linkifyjs').Plugin} 8 | */ 9 | export default function hashtag({ scanner, parser }) { 10 | // Various tokens that may compose a hashtag 11 | const { POUND, UNDERSCORE, FULLWIDTHMIDDLEDOT, ASCIINUMERICAL, ALPHANUMERICAL } = scanner.tokens; 12 | const { alpha, numeric, alphanumeric, emoji } = scanner.tokens.groups; 13 | 14 | // Take or create a transition from start to the '#' sign (non-accepting) 15 | // Take transition from '#' to any text token to yield valid hashtag state 16 | // Account for leading underscore (non-accepting unless followed by alpha) 17 | const Hash = parser.start.tt(POUND); 18 | const HashPrefix = Hash.tt(UNDERSCORE); 19 | const Hashtag = new State(HashtagToken); 20 | 21 | Hash.tt(ASCIINUMERICAL, Hashtag); 22 | Hash.tt(ALPHANUMERICAL, Hashtag); 23 | Hash.ta(numeric, HashPrefix); 24 | Hash.ta(alpha, Hashtag); 25 | Hash.ta(emoji, Hashtag); 26 | Hash.ta(FULLWIDTHMIDDLEDOT, Hashtag); 27 | HashPrefix.tt(ASCIINUMERICAL, Hashtag); 28 | HashPrefix.tt(ALPHANUMERICAL, Hashtag); 29 | HashPrefix.ta(alpha, Hashtag); 30 | HashPrefix.ta(emoji, Hashtag); 31 | HashPrefix.ta(FULLWIDTHMIDDLEDOT, Hashtag); 32 | HashPrefix.ta(numeric, HashPrefix); 33 | HashPrefix.tt(UNDERSCORE, HashPrefix); 34 | Hashtag.ta(alphanumeric, Hashtag); 35 | Hashtag.ta(emoji, Hashtag); 36 | Hashtag.tt(FULLWIDTHMIDDLEDOT, Hashtag); 37 | Hashtag.tt(UNDERSCORE, Hashtag); // Trailing underscore is okay 38 | } 39 | -------------------------------------------------------------------------------- /packages/linkify-plugin-hashtag/src/index.mjs: -------------------------------------------------------------------------------- 1 | import { registerPlugin } from 'linkifyjs'; 2 | import hashtag from './hashtag'; 3 | 4 | registerPlugin('hashtag', hashtag); 5 | -------------------------------------------------------------------------------- /packages/linkify-plugin-hashtag/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["dist/linkify-plugin-hashtag.cjs", "dist/linkify-plugin-hashtag.mjs"], 3 | "exclude": [], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "maxNodeModuleJsDepth": 1 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/linkify-plugin-ip/.npmignore: -------------------------------------------------------------------------------- 1 | ../../.npmignore -------------------------------------------------------------------------------- /packages/linkify-plugin-ip/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-plugin-ip/README.md: -------------------------------------------------------------------------------- 1 | linkify-plugin-ip 2 | === 3 | 4 | [![npm version](https://badge.fury.io/js/linkify-plugin-ip.svg)](https://www.npmjs.com/package/linkify-plugin-ip) 5 | 6 | Detect and convert IPv4 and IPv6 addresses to `` 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-ip 14 | ``` 15 | 16 | Import into your JavaScript with `require` 17 | ```js 18 | const linkify = require('linkifyjs') 19 | require('linkify-plugin-ip'); 20 | ``` 21 | or with ES modules 22 | 23 | ```js 24 | import * as linkify from 'linkifyjs'; 25 | import 'linkify-plugin-ip'; 26 | ``` 27 | 28 | ## Usage 29 | 30 | [Read the full documentation](https://linkify.js.org/docs/plugin-ip.html). 31 | 32 | ## License 33 | 34 | MIT 35 | -------------------------------------------------------------------------------- /packages/linkify-plugin-ip/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkify-plugin-ip", 3 | "type": "module", 4 | "version": "4.3.1", 5 | "description": "IP address plugin for linkifyjs", 6 | "main": "dist/linkify-plugin-ip.cjs", 7 | "module": "dist/linkify-plugin-ip.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-ip" 19 | }, 20 | "keywords": [ 21 | "link", 22 | "autolink", 23 | "url", 24 | "email", 25 | "ip", 26 | "ipv4", 27 | "ipv6" 28 | ], 29 | "author": "Nick Frasser (https://nfrasser.com)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/nfrasser/linkifyjs/issues" 33 | }, 34 | "homepage": "https://linkify.js.org", 35 | "peerDependencies": { 36 | "linkifyjs": "^4.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/linkify-plugin-ip/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { linkifyPlugin } from '../../rollup.config.js'; 2 | export default linkifyPlugin('ip'); 3 | -------------------------------------------------------------------------------- /packages/linkify-plugin-ip/src/index.mjs: -------------------------------------------------------------------------------- 1 | import { registerTokenPlugin, registerPlugin } from 'linkifyjs'; 2 | import { ipv4Tokens, ipv6Tokens, ip } from './ip'; 3 | 4 | registerTokenPlugin('ipv4', ipv4Tokens); 5 | registerTokenPlugin('ipv6', ipv6Tokens); 6 | registerPlugin('ip', ip); 7 | -------------------------------------------------------------------------------- /packages/linkify-plugin-ip/src/ip.mjs: -------------------------------------------------------------------------------- 1 | import { createTokenClass, State, options, multi } from 'linkifyjs'; 2 | 3 | const B_IPV6_B = 'B_IPV6_B'; // 'bracket [', IPV6, '] bracket' 4 | 5 | const IPv4Token = createTokenClass('ipv4', { 6 | isLink: true, 7 | toHref(scheme = options.defaults.defaultProtocol) { 8 | return `${scheme}://${this.v}`; 9 | }, 10 | }); 11 | 12 | /** 13 | * @type {import('linkifyjs').TokenPlugin} 14 | */ 15 | export function ipv4Tokens({ scanner }) { 16 | const { start } = scanner; 17 | const flags = { byte: true, numeric: true }; 18 | 19 | // States for [0, 9] 20 | const Digits = []; 21 | for (let i = 0; i < 10; i++) { 22 | const x = start.tt(`${i}`, `${i}`, flags); 23 | Digits.push(x); 24 | } 25 | 26 | // States [10, 99] 27 | for (let i = 1; i < 10; i++) { 28 | const x = Digits[i]; 29 | for (let j = 0; j < 10; j++) { 30 | x.tt(`${j}`, `${i}${j}`, flags); 31 | } 32 | } 33 | 34 | // States for [100, 199] 35 | for (let i = 0; i < 10; i++) { 36 | let xx = Digits[1].tt(`${i}`); 37 | for (let j = 0; j < 10; j++) { 38 | xx.tt(`${j}`, `1${i}${j}`, flags); 39 | } 40 | } 41 | 42 | // States for [200, 249] 43 | for (let i = 0; i < 5; i++) { 44 | let xx = Digits[2].tt(`${i}`); 45 | for (let j = 0; j < 10; j++) { 46 | xx.tt(`${j}`, `2${i}${j}`, flags); 47 | } 48 | } 49 | 50 | // States for [250, 255] 51 | let xx = Digits[2].tt('5'); 52 | for (let i = 0; i < 6; i++) { 53 | xx.tt(`${i}`, `25${i}`, flags); 54 | } 55 | } 56 | 57 | /** 58 | * @type {import('linkifyjs').TokenPlugin} 59 | */ 60 | export const ipv6Tokens = ({ scanner }) => { 61 | const { start } = scanner; 62 | 63 | const HEX = /[0-9a-f]/; 64 | let z = start.tt('['); // [ 65 | let _ = z.tt(':'); // [: 66 | 67 | let x = z.tr(HEX); 68 | let x_ = x.tt(':'); 69 | let x_x = x_.tr(HEX); 70 | let x_x_ = x_x.tt(':'); 71 | let x_x_x = x_x_.tr(HEX); 72 | let x_x_x_ = x_x_x.tt(':'); 73 | let x_x_x_x = x_x_x_.tr(HEX); 74 | let x_x_x_x_ = x_x_x_x.tt(':'); 75 | let x_x_x_x_x = x_x_x_x_.tr(HEX); 76 | let x_x_x_x_x_ = x_x_x_x_x.tt(':'); 77 | let x_x_x_x_x_x = x_x_x_x_x_.tr(HEX); 78 | let x_x_x_x_x_x_ = x_x_x_x_x_x.tt(':'); 79 | let x_x_x_x_x_x_x = x_x_x_x_x_x_.tr(HEX); 80 | let x_x_x_x_x_x_x_ = x_x_x_x_x_x_x.tt(':'); 81 | let x_x_x_x_x_x_x_x = x_x_x_x_x_x_x_.tr(HEX); 82 | 83 | let BIpv6B = x_x_x_x_x_x_x_x.tt(']', B_IPV6_B); 84 | x_x_x_x_x_x_x_.tt(']', BIpv6B); 85 | 86 | // Note: This isn't quite right because it allows unlimited components but 87 | // it's proved difficult to come up with a correct implementation. 88 | let __ = _.tt(':'); // [:: 89 | let __x = __.tr(HEX); 90 | let __x_ = __x.tt(':'); 91 | __x_.tt(':', __); 92 | __x_.tr(HEX, __x); 93 | 94 | x_.tt(':', __); 95 | x_x_.tt(':', __); 96 | x_x_x_.tt(':', __); 97 | x_x_x_x_.tt(':', __); 98 | x_x_x_x_x_.tt(':', __); 99 | x_x_x_x_x_x_.tt(':', __); 100 | 101 | _.tr(HEX, x_x); 102 | __.tt(']', BIpv6B); 103 | __x.tt(']', BIpv6B); 104 | __x_.tt(']', BIpv6B); 105 | 106 | // Ensures max of 4 items per component are allowed 107 | for (let i = 1; i < 4; i++) { 108 | x = x.tr(HEX); 109 | x_x = x_x.tr(HEX); 110 | x_x_x = x_x_x.tr(HEX); 111 | x_x_x_x = x_x_x_x.tr(HEX); 112 | x_x_x_x_x = x_x_x_x_x.tr(HEX); 113 | x_x_x_x_x_x = x_x_x_x_x_x.tr(HEX); 114 | x_x_x_x_x_x_x = x_x_x_x_x_x_x.tr(HEX); 115 | x_x_x_x_x_x_x_x = x_x_x_x_x_x_x_x.tr(HEX); 116 | 117 | 118 | x.tt(':', x_); 119 | x_x.tt(':', x_x_); 120 | x_x_x.tt(':', x_x_x_); 121 | x_x_x_x.tt(':', x_x_x_x_); 122 | x_x_x_x_x.tt(':', x_x_x_x_x_); 123 | x_x_x_x_x_x.tt(':', x_x_x_x_x_x_); 124 | x_x_x_x_x_x_x.tt(':', x_x_x_x_x_x_x_); 125 | x_x_x_x_x_x_x_x.tt(']', BIpv6B); 126 | 127 | __x = __x.tr(HEX); 128 | __x.tt(':', __x_); 129 | __x.tt(']', BIpv6B); 130 | } 131 | }; 132 | 133 | /** 134 | * @type {import('linkifyjs').Plugin} 135 | */ 136 | export function ip({ scanner, parser }) { 137 | const { COLON, DOT, SLASH, LOCALHOST, SLASH_SCHEME, groups } = 138 | scanner.tokens; 139 | 140 | const ByteDot = new State(); 141 | const ByteDotByte = new State(); 142 | const ByteDotByteDotByte = new State(); 143 | const IPv4 = new State(IPv4Token); 144 | 145 | for (let i = 0; i < groups.byte.length; i++) { 146 | parser.start.tt(groups.byte[i]).tt(DOT, ByteDot); 147 | } 148 | 149 | ByteDot.ta(groups.byte, ByteDotByte); 150 | ByteDotByte.tt(DOT).ta(groups.byte, ByteDotByteDotByte); 151 | ByteDotByteDotByte.tt(DOT).ta(groups.byte, IPv4); 152 | 153 | // If IP followed by port or slash, make URL. Get existing URL state 154 | const Url = parser.start.go(LOCALHOST).go(SLASH); 155 | IPv4.tt(SLASH, Url); 156 | 157 | const IPv4Colon = IPv4.tt(COLON); 158 | const IPv4ColonPort = new State(multi.Url); 159 | IPv4Colon.ta(groups.numeric, IPv4ColonPort); 160 | IPv4ColonPort.tt(SLASH, Url); 161 | 162 | // Detect IPv6 when followed by URL prefix 163 | const UriPrefix = parser.start.go(SLASH_SCHEME).go(COLON).go(SLASH).go(SLASH); 164 | const UriPrefixIPv6 = UriPrefix.tt(B_IPV6_B, multi.Url); 165 | UriPrefixIPv6.tt(SLASH, Url); 166 | const UriPrefixIPv6Colon = UriPrefixIPv6.tt(COLON); 167 | const UriPrefixIPv6ColonPort = new State(multi.Url); 168 | UriPrefixIPv6Colon.ta(groups.numeric, UriPrefixIPv6ColonPort); 169 | UriPrefixIPv6ColonPort.tt(SLASH, Url); 170 | } 171 | -------------------------------------------------------------------------------- /packages/linkify-plugin-ip/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["dist/linkify-plugin-ip.cjs", "dist/linkify-plugin-ip.mjs"], 3 | "exclude": [], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "maxNodeModuleJsDepth": 1 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/linkify-plugin-keyword/.npmignore: -------------------------------------------------------------------------------- 1 | ../../.npmignore -------------------------------------------------------------------------------- /packages/linkify-plugin-keyword/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-plugin-keyword/README.md: -------------------------------------------------------------------------------- 1 | linkify-plugin-keyword 2 | === 3 | 4 | [![npm version](https://badge.fury.io/js/linkify-plugin-keyword.svg)](https://www.npmjs.com/package/linkify-plugin-keyword) 5 | 6 | Detect and convert arbitrary keywords to `` 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-keyword 14 | ``` 15 | 16 | Import into your JavaScript with `require` 17 | ```js 18 | const linkify = require('linkifyjs') 19 | const registerKeywords = require('linkify-plugin-keyword'); 20 | ``` 21 | or with ES modules 22 | 23 | ```js 24 | import * as linkify from 'linkifyjs'; 25 | import registerKeywords from 'linkify-plugin-keyword'; 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```js 31 | registerKeywords(['foo', 'bar', 'baz']) 32 | linkify.find('Any foo keywords here?') 33 | ``` 34 | 35 | [Read the full documentation](https://linkify.js.org/docs/plugin-keyword.html). 36 | 37 | ## License 38 | 39 | MIT 40 | -------------------------------------------------------------------------------- /packages/linkify-plugin-keyword/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkify-plugin-keyword", 3 | "type": "module", 4 | "version": "4.3.1", 5 | "description": "Keyword plugin for linkifyjs", 6 | "main": "dist/linkify-plugin-keyword.cjs", 7 | "module": "dist/linkify-plugin-keyword.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-keyword" 19 | }, 20 | "keywords": [ 21 | "link", 22 | "autolink", 23 | "url", 24 | "email", 25 | "keyword" 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 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/linkify-plugin-keyword/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { linkifyPlugin } from '../../rollup.config.js'; 2 | export default linkifyPlugin('keyword', { globalName: 'linkifyRegisterKeywords' }); 3 | -------------------------------------------------------------------------------- /packages/linkify-plugin-keyword/src/index.mjs: -------------------------------------------------------------------------------- 1 | import { registerPlugin, registerTokenPlugin } from 'linkifyjs'; 2 | import { keyword, tokens, registerKeywords } from './keyword'; 3 | 4 | registerTokenPlugin('keyword', tokens); 5 | registerPlugin('keyword', keyword); 6 | 7 | export default registerKeywords; 8 | -------------------------------------------------------------------------------- /packages/linkify-plugin-keyword/src/keyword.mjs: -------------------------------------------------------------------------------- 1 | import { createTokenClass, stringToArray } from 'linkifyjs'; 2 | 3 | /** 4 | * Tokenize will emit token classes of this type 5 | */ 6 | const Keyword = createTokenClass('keyword', { isLink: true }); 7 | 8 | 9 | /** 10 | * Keys are registered tokens recognized by the scanner in the plugin 11 | * definition, associated with one or more collections. Values are the list of 12 | * keywords that get scanned into those tokens. 13 | * 14 | * Organized this way to ensure other links that rely on these collections are 15 | * still recognized by the parser. 16 | */ 17 | const registeredKeywordsGroups = { 18 | numeric: [], 19 | ascii: [], 20 | asciinumeric: [], 21 | alpha: [], 22 | alphanumeric: [], 23 | domain: [], 24 | keyword: [], 25 | }; 26 | 27 | // Additional pre-processing regular expressions 28 | // Clone from existing but add global flag 29 | const ALL_LETTERS = /\p{L}/gu; 30 | const ALL_EMOJIS = /\p{Emoji}/gu; 31 | const ALL_EMOJI_VARIATIONS = /\ufe0f/g; 32 | 33 | function pushIfMissing(item, list) { 34 | if (list.indexOf(item) < 0) { 35 | list.push(item); 36 | } 37 | } 38 | 39 | /** 40 | * Return the number of regexp matches in the given string 41 | * @param {string} str 42 | * @param {RegExp} regexp 43 | * @returns {number} 44 | */ 45 | function nMatch(str, regexp) { 46 | const matches = str.match(regexp); 47 | return matches ? matches.length : 0; 48 | } 49 | 50 | /** 51 | * 52 | * @param {string[]} keywords Keywords to linkify 53 | */ 54 | export function registerKeywords(keywords) { 55 | // validate all keywords 56 | for (let i = 0; i < keywords.length; i++) { 57 | const keyword = keywords[i]; 58 | if (typeof keyword !== 'string' || !keyword) { 59 | throw new Error(`linkify-plugin-keyword: Invalid keyword: ${keyword}`); 60 | } 61 | } 62 | 63 | for (let i = 0; i < keywords.length; i++) { 64 | const keyword = keywords[i].toLowerCase(); 65 | if (/^[0-9]+$/.test(keyword)) { 66 | pushIfMissing(keyword, registeredKeywordsGroups.numeric); 67 | continue; 68 | } 69 | 70 | if (/^[a-z]+$/.test(keyword)) { 71 | pushIfMissing(keyword, registeredKeywordsGroups.ascii); 72 | continue; 73 | } 74 | 75 | if (/^[0-9a-z]+$/.test(keyword)) { 76 | pushIfMissing(keyword, registeredKeywordsGroups.asciinumeric); 77 | continue; 78 | } 79 | 80 | const nLetters = nMatch(keyword, ALL_LETTERS); 81 | if (nLetters === keyword.length) { 82 | pushIfMissing(keyword, registeredKeywordsGroups.alpha); 83 | continue; 84 | } 85 | 86 | const nNumbers = nMatch(keyword, /[0-9]/g); 87 | if (nLetters + nNumbers === keyword.length) { 88 | pushIfMissing(keyword, registeredKeywordsGroups.alphanumeric); 89 | continue; 90 | } 91 | 92 | const nEmojis = nMatch(keyword, ALL_EMOJIS) + nMatch(keyword, ALL_EMOJI_VARIATIONS); 93 | const nHyphens = nMatch(keyword, /-/g); 94 | if (nLetters + nNumbers + nEmojis + nHyphens === keyword.length && !/(^-|-$|--)/.test(keyword)) { 95 | // Composed of letters, numbers hyphens or emojis. No leading, 96 | // trailing or consecutive hyphens. Valid domain name. 97 | pushIfMissing(keyword, registeredKeywordsGroups.domain); 98 | continue; 99 | } 100 | 101 | // Keyword does not match any existing tokens that the scanner may recognize 102 | pushIfMissing(keyword, registeredKeywordsGroups.keyword); 103 | } 104 | } 105 | 106 | /** 107 | * @type import('linkifyjs').TokenPlugin 108 | */ 109 | export function tokens({ scanner }) { 110 | for (const group in registeredKeywordsGroups) { 111 | const keywords = registeredKeywordsGroups[group]; 112 | for (let i = 0; i < keywords.length; i++) { 113 | const chars = stringToArray(keywords[i]); 114 | scanner.start.ts(chars, keywords[i], { keyword: true, [group]: true }); 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * @type import('linkifyjs').Plugin 121 | */ 122 | export function keyword({ scanner, parser }) { 123 | // Create parser transitions from all registered tokens 124 | const group = scanner.tokens.groups.keyword; 125 | if (group && group.length > 0) { 126 | parser.start.ta(group, Keyword); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /packages/linkify-plugin-keyword/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["dist/linkify-plugin-keyword.cjs", "dist/linkify-plugin-keyword.mjs"], 3 | "exclude": [], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "maxNodeModuleJsDepth": 1 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/linkify-plugin-mention/.npmignore: -------------------------------------------------------------------------------- 1 | ../../.npmignore -------------------------------------------------------------------------------- /packages/linkify-plugin-mention/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-plugin-mention/README.md: -------------------------------------------------------------------------------- 1 | linkify-plugin-mention 2 | === 3 | 4 | [![npm version](https://badge.fury.io/js/linkify-plugin-mention.svg)](https://www.npmjs.com/package/linkify-plugin-mention) 5 | 6 | Detect and convert Twitter- and GitHub- style "at"-mentions (@mentions) to `` 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-mention 14 | ``` 15 | 16 | Import into your JavaScript with `require` 17 | ```js 18 | const linkify = require('linkifyjs') 19 | require('linkify-plugin-mention'); 20 | ``` 21 | or with ES modules 22 | 23 | ```js 24 | import * as linkify from 'linkifyjs'; 25 | import 'linkify-plugin-mention'; 26 | ``` 27 | 28 | ## Usage 29 | 30 | [Read the full documentation](https://linkify.js.org/docs/plugin-mention.html). 31 | 32 | ## License 33 | 34 | MIT 35 | -------------------------------------------------------------------------------- /packages/linkify-plugin-mention/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkify-plugin-mention", 3 | "type": "module", 4 | "version": "4.3.1", 5 | "description": "@mentions plugin for linkifyjs", 6 | "main": "dist/linkify-plugin-mention.cjs", 7 | "module": "dist/linkify-plugin-mention.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-mention" 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-mention/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { linkifyPlugin } from '../../rollup.config.js'; 2 | export default linkifyPlugin('mention'); 3 | -------------------------------------------------------------------------------- /packages/linkify-plugin-mention/src/index.mjs: -------------------------------------------------------------------------------- 1 | import { registerPlugin } from 'linkifyjs'; 2 | import mention from './mention'; 3 | 4 | registerPlugin('mention', mention); 5 | 6 | -------------------------------------------------------------------------------- /packages/linkify-plugin-mention/src/mention.mjs: -------------------------------------------------------------------------------- 1 | import { createTokenClass } from 'linkifyjs'; 2 | 3 | const MentionToken = createTokenClass('mention', { 4 | isLink: true, 5 | toHref() { 6 | return '/' + this.toString().slice(1); 7 | } 8 | }); 9 | 10 | /** 11 | * Mention parser plugin for linkify 12 | * @type {import('linkifyjs').Plugin} 13 | */ 14 | export default function mention({ scanner, parser }) { 15 | const { HYPHEN, SLASH, UNDERSCORE, AT } = scanner.tokens; 16 | const { domain } = scanner.tokens.groups; 17 | 18 | // @ 19 | const At = parser.start.tt(AT); // @ 20 | 21 | // Begin with hyphen (not mention unless contains other characters) 22 | const AtHyphen = At.tt(HYPHEN); 23 | AtHyphen.tt(HYPHEN, AtHyphen); 24 | 25 | // Valid mention (not made up entirely of symbols) 26 | const Mention = At.tt(UNDERSCORE, MentionToken); 27 | 28 | At.ta(domain, Mention); 29 | AtHyphen.tt(UNDERSCORE, Mention); 30 | AtHyphen.ta(domain, Mention); 31 | 32 | // More valid mentions 33 | Mention.ta(domain, Mention); 34 | Mention.tt(HYPHEN, Mention); 35 | Mention.tt(UNDERSCORE, Mention); 36 | 37 | // Mention with a divider 38 | const MentionDivider = Mention.tt(SLASH); 39 | 40 | // Once we get a word token, mentions can start up again 41 | MentionDivider.ta(domain, Mention); 42 | MentionDivider.tt(UNDERSCORE, Mention); 43 | MentionDivider.tt(HYPHEN, Mention); 44 | } 45 | -------------------------------------------------------------------------------- /packages/linkify-plugin-mention/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["dist/linkify-plugin-mention.cjs", "dist/linkify-plugin-mention.mjs"], 3 | "exclude": [], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "maxNodeModuleJsDepth": 1 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/linkify-plugin-ticket/.npmignore: -------------------------------------------------------------------------------- 1 | ../../.npmignore -------------------------------------------------------------------------------- /packages/linkify-plugin-ticket/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-plugin-ticket/README.md: -------------------------------------------------------------------------------- 1 | linkify-plugin-ticket 2 | === 3 | 4 | [![npm version](https://badge.fury.io/js/linkify-plugin-ticket.svg)](https://www.npmjs.com/package/linkify-plugin-ticket) 5 | 6 | Detect and convert GitHub-style issue or ticket numbers (e.g., `#123`) to `` 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 | [![npm version](https://badge.fury.io/js/linkify-react.svg)](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 | [![npm version](https://badge.fury.io/js/linkify-string.svg)](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)}`; 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 | [![npm version](https://badge.fury.io/js/linkifyjs.svg)](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!
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}`; 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 | --------------------------------------------------------------------------------