├── .circleci
└── config.yml
├── .editorconfig
├── .gitattributes
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .npmignore
├── .prettierignore
├── .watchmanconfig
├── .yarnrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── asset
└── screen.gif
├── babel.config.js
├── example
├── app.json
├── babel.config.js
├── index.js
├── metro.config.js
├── package.json
├── src
│ └── App.tsx
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
├── lefthook.yml
├── package.json
├── scripts
└── bootstrap.js
├── src
├── Hyperlink.tsx
├── __tests__
│ └── index.test.tsx
├── index.tsx
└── types.ts
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | executors:
4 | default:
5 | docker:
6 | - image: circleci/node:16
7 | working_directory: ~/project
8 |
9 | commands:
10 | attach_project:
11 | steps:
12 | - attach_workspace:
13 | at: ~/project
14 |
15 | jobs:
16 | install-dependencies:
17 | executor: default
18 | steps:
19 | - checkout
20 | - attach_project
21 | - restore_cache:
22 | keys:
23 | - dependencies-{{ checksum "package.json" }}
24 | - dependencies-
25 | - restore_cache:
26 | keys:
27 | - dependencies-example-{{ checksum "example/package.json" }}
28 | - dependencies-example-
29 | - run:
30 | name: Install dependencies
31 | command: |
32 | yarn install --cwd example --frozen-lockfile
33 | yarn install --frozen-lockfile
34 | - save_cache:
35 | key: dependencies-{{ checksum "package.json" }}
36 | paths: node_modules
37 | - save_cache:
38 | key: dependencies-example-{{ checksum "example/package.json" }}
39 | paths: example/node_modules
40 | - persist_to_workspace:
41 | root: .
42 | paths: .
43 |
44 | lint:
45 | executor: default
46 | steps:
47 | - attach_project
48 | - run:
49 | name: Lint files
50 | command: |
51 | yarn lint
52 |
53 | typescript:
54 | executor: default
55 | steps:
56 | - attach_project
57 | - run:
58 | name: Typecheck files
59 | command: |
60 | yarn typescript
61 |
62 | unit-tests:
63 | executor: default
64 | steps:
65 | - attach_project
66 | - run:
67 | name: Run unit tests
68 | command: |
69 | yarn test --coverage
70 | - store_artifacts:
71 | path: coverage
72 | destination: coverage
73 |
74 | build-package:
75 | executor: default
76 | steps:
77 | - attach_project
78 | - run:
79 | name: Build package
80 | command: |
81 | yarn prepare
82 |
83 | workflows:
84 | build-and-test:
85 | jobs:
86 | - install-dependencies
87 | - lint:
88 | requires:
89 | - install-dependencies
90 | - typescript:
91 | requires:
92 | - install-dependencies
93 | - unit-tests:
94 | requires:
95 | - install-dependencies
96 | - build-package:
97 | requires:
98 | - install-dependencies
99 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 | # specific for windows script files
3 | *.bat text eol=crlf
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # XDE
6 | .expo/
7 |
8 | # VSCode
9 | .vscode/
10 | jsconfig.json
11 |
12 | dist/
13 | # Xcode
14 | #
15 | build/
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 | xcuserdata
25 | *.xccheckout
26 | *.moved-aside
27 | DerivedData
28 | *.hmap
29 | *.ipa
30 | *.xcuserstate
31 | project.xcworkspace
32 |
33 | # Android/IJ
34 | #
35 | .classpath
36 | .cxx
37 | .gradle
38 | .idea
39 | .project
40 | .settings
41 | local.properties
42 | android.iml
43 |
44 | # Cocoapods
45 | #
46 | example/ios/Pods
47 |
48 | # Ruby
49 | example/vendor/
50 |
51 | # node.js
52 | #
53 | node_modules/
54 | npm-debug.log
55 | yarn-debug.log
56 | yarn-error.log
57 |
58 | # BUCK
59 | buck-out/
60 | \.buckd/
61 | android/app/libs
62 | android/keystores/debug.keystore
63 |
64 | # Expo
65 | .expo/*
66 |
67 | # generated by bob
68 | lib/
69 |
70 | # generated by bob
71 | dist/
72 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | asset
2 | node_modules
3 | .git
4 | .npmignore
5 | package-lock.json
6 | .gitignore
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 |
3 | /dist
4 | /coverage
5 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | # Override Yarn command so we can automatically setup the repo on running `yarn`
2 |
3 | yarn-path "scripts/bootstrap.js"
4 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at greycellofp@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project.
4 |
5 | ## Development workflow
6 |
7 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package:
8 |
9 | ```sh
10 | yarn
11 | ```
12 |
13 | > While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development.
14 |
15 | While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app.
16 |
17 | To start the packager:
18 |
19 | ```sh
20 | yarn example start
21 | ```
22 |
23 | To run the example app on Android:
24 |
25 | ```sh
26 | yarn example android
27 | ```
28 |
29 | To run the example app on iOS:
30 |
31 | ```sh
32 | yarn example ios
33 | ```
34 |
35 | To run the example app on Web:
36 |
37 | ```sh
38 | yarn example web
39 | ```
40 |
41 | Make sure your code passes TypeScript and ESLint. Run the following to verify:
42 |
43 | ```sh
44 | yarn typescript
45 | yarn lint
46 | ```
47 |
48 | To fix formatting errors, run the following:
49 |
50 | ```sh
51 | yarn lint --fix
52 | ```
53 |
54 | ### Commit message convention
55 |
56 | Follow the [conventional commits specification](https://www.conventionalcommits.org/en) for the commit messages:
57 |
58 | - `fix`: bug fixes, e.g. fix crash due to deprecated method.
59 | - `feat`: new features, e.g. add new method to the module.
60 | - `refactor`: code refactor, e.g. migrate from class components to hooks.
61 | - `docs`: changes into documentation, e.g. add usage example for the module..
62 | - `test`: adding or updating tests, e.g. add integration tests using detox.
63 | - `chore`: tooling changes, e.g. change CI config.
64 |
65 | Pre-commit hooks verify that your commit message matches this format when committing.
66 |
67 | ### Linting and tests
68 |
69 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/)
70 |
71 | This library uses [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code.
72 |
73 | The pre-commit hooks verify that the linters pass when committing.
74 |
75 | ### Scripts
76 |
77 | The `package.json` file contains various scripts for common tasks:
78 |
79 | - `yarn bootstrap`: setup project by installing all dependencies and pods.
80 | - `yarn typescript`: type-check files with TypeScript.
81 | - `yarn lint`: lint files with ESLint.
82 | - `yarn test`: run unit tests with Jest.
83 | - `yarn example start`: start the Metro server for the example app.
84 | - `yarn example android`: run the example app on Android.
85 | - `yarn example ios`: run the example app on iOS.
86 |
87 | ### Sending a pull request
88 |
89 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).
90 |
91 | When you're sending a pull request:
92 |
93 | - Prefer small pull requests focused on one change.
94 | - Verify that linters and tests are passing.
95 | - Review the documentation to make sure it looks good.
96 | - Follow the pull request template when opening a pull request.
97 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.
98 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Pawan Kumar
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-hyperlink
2 | [](http://badge.fury.io/js/react-native-hyperlink) [](https://github.com/jondot/awesome-react-native#text--rich-content)
3 |
4 | A `` component for [react-native](http://facebook.github.io/react-native/) & [react-native-web](https://github.com/necolas/react-native-web) that makes urls, fuzzy links, emails etc clickable
5 |
6 | 
7 |
8 | ## Installation
9 | ```sh
10 | npm i --save react-native-hyperlink
11 | ```
12 |
13 | ## Props
14 | | name | desc | type | default
15 | | --- | --- | --- | --- |
16 | | `linkify` | [linkify-it](http://markdown-it.github.io/linkify-it/doc/) object, for custom schema | `object` | `require('linkify-it')()`
17 | | `linkStyle` | highlight clickable text with styles | `Text.propTypes.style` |
18 | | `linkText` | A string or a func to replace parsed text | `oneOfType([ string, func ])` |
19 | | `onPress` | Func to handle click over a clickable text with parsed text as arg | `func` |
20 | | `onLongPress` | Func to handle long click over a clickable text with parsed text as arg | `func` |
21 | |`linkDefault`|A platform specific fallback to handle `onPress`. Uses [Linking](https://facebook.github.io/react-native/docs/linking.html). Disabled by default | `bool`
22 | |`injectViewProps`| Func with url as a param to inject props to the clickable component | `func` | `i => ({})`
23 |
24 | ## Examples
25 | Wrap any component that has `` (works for [nested ](https://facebook.github.io/react-native/docs/text.html#nested-text) text too) in it
26 |
27 | ```jsx
28 | import Hyperlink from 'react-native-hyperlink'
29 |
30 | export const defaultLink = () =>
31 |
32 |
33 | This text will be parsed to check for clickable strings like https://github.com/obipawan/hyperlink and made clickable.
34 |
35 |
36 |
37 | export const regularText = () =>
38 | alert(url + ", " + text) }>
39 |
40 | This text will be parsed to check for clickable strings like https://github.com/obipawan/hyperlink and made clickable.
41 |
42 |
43 |
44 | export const regularTextLongPress = () =>
45 | alert(url + ", " + text) }>
46 |
47 | This text will be parsed to check for clickable strings like https://github.com/obipawan/hyperlink and made clickable for long click.
48 |
49 |
50 |
51 | export const nestedText = () =>
52 | alert(url + ", " + text) }>
53 |
54 |
55 | A nested Text component https://facebook.github.io/react-native/docs/text.html works equally well with https://github.com/obipawan/hyperlink
56 |
57 |
58 |
59 |
60 | export const highlightText = () =>
61 |
62 |
63 | Make clickable strings like https://github.com/obipawan/hyperlink stylable
64 |
65 |
66 |
67 | export const parseAndReplace = () =>
68 | url === 'https://github.com/obipawan/hyperlink' ? 'Hyperlink' : url }
71 | >
72 |
73 | Make clickable strings cleaner with https://github.com/obipawan/hyperlink
74 |
75 |
76 |
77 | export const passPropsText = () =>
78 | ({
81 | testID: url === 'http://link.com' ? 'id1' : 'id2' ,
82 | style: url === 'https://link.com' ? { color: 'red' } : { color: 'blue' },
83 | //any other props you wish to pass to the component
84 | }) }
85 | >
86 | You can pass props to clickable components matched by url.
87 | This url looks red https://link.com
88 | and this url looks blue https://link2.com
89 |
90 | ```
91 |
92 | ### Dependenies
93 | [linkify-it](https://github.com/markdown-it/linkify-it)
94 | ### Development
95 |
96 | PRs highly appreciated
97 |
98 | License
99 | ----
100 | MIT License
101 |
--------------------------------------------------------------------------------
/asset/screen.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/obipawan/react-native-hyperlink/86104b66d2e3e6677ca35b26b39ce609dd76487b/asset/screen.gif
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-hyperlink-example",
3 | "displayName": "Hyperlink Example",
4 | "expo": {
5 | "name": "react-native-hyperlink-example",
6 | "slug": "react-native-hyperlink-example",
7 | "description": "Example app for react-native-hyperlink",
8 | "privacy": "public",
9 | "version": "1.0.0",
10 | "platforms": [
11 | "ios",
12 | "android",
13 | "web"
14 | ],
15 | "ios": {
16 | "supportsTablet": true
17 | },
18 | "jsEngine": "hermes",
19 | "assetBundlePatterns": [
20 | "**/*"
21 | ]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const pak = require('../package.json');
3 |
4 | module.exports = function (api) {
5 | api.cache(true);
6 |
7 | return {
8 | presets: ['babel-preset-expo'],
9 | plugins: [
10 | [
11 | 'module-resolver',
12 | {
13 | extensions: ['.tsx', '.ts', '.js', '.json'],
14 | alias: {
15 | // For development, we want to alias the library to the source
16 | [pak.name]: path.join(__dirname, '..', pak.source),
17 | },
18 | },
19 | ],
20 | ],
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import { registerRootComponent } from 'expo';
2 |
3 | import App from './src/App';
4 |
5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
6 | // It also ensures that whether you load the app in the Expo client or in a native build,
7 | // the environment is set up appropriately
8 | registerRootComponent(App);
9 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const escape = require('escape-string-regexp');
3 | const { getDefaultConfig } = require('@expo/metro-config');
4 | const exclusionList = require('metro-config/src/defaults/exclusionList');
5 | const pak = require('../package.json');
6 |
7 | const root = path.resolve(__dirname, '..');
8 |
9 | const modules = Object.keys({
10 | ...pak.peerDependencies,
11 | });
12 |
13 | const defaultConfig = getDefaultConfig(__dirname);
14 |
15 | module.exports = {
16 | ...defaultConfig,
17 |
18 | projectRoot: __dirname,
19 | watchFolders: [root],
20 |
21 | // We need to make sure that only one version is loaded for peerDependencies
22 | // So we block them at the root, and alias them to the versions in example's node_modules
23 | resolver: {
24 | ...defaultConfig.resolver,
25 |
26 | blacklistRE: exclusionList(
27 | modules.map(
28 | (m) =>
29 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`)
30 | )
31 | ),
32 |
33 | extraNodeModules: modules.reduce((acc, name) => {
34 | acc[name] = path.join(__dirname, 'node_modules', name);
35 | return acc;
36 | }, {}),
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-hyperlink-example",
3 | "description": "Example app for react-native-hyperlink",
4 | "version": "0.0.1",
5 | "private": true,
6 | "main": "index",
7 | "scripts": {
8 | "android": "expo start --android",
9 | "ios": "expo start --ios",
10 | "web": "expo start --web",
11 | "start": "expo start"
12 | },
13 | "dependencies": {
14 | "expo": "^46.0.0",
15 | "expo-splash-screen": "~0.16.1",
16 | "react": "18.0.0",
17 | "react-dom": "18.0.0",
18 | "react-native": "0.69.4",
19 | "react-native-web": "~0.18.7"
20 | },
21 | "devDependencies": {
22 | "@babel/core": "^7.18.6",
23 | "@babel/runtime": "^7.9.6",
24 | "babel-loader": "^8.2.5",
25 | "babel-plugin-module-resolver": "^4.0.0",
26 | "babel-preset-expo": "~9.2.0",
27 | "expo-cli": "^6.0.0 "
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { FlatList, StyleSheet, Text, View } from 'react-native';
4 | import Hyperlink from 'react-native-hyperlink';
5 |
6 | const examples = [
7 |
8 |
9 | This text will be parsed to check for clickable strings like
10 | https://github.com/obipawan/hyperlink and made clickable.
11 |
12 | ,
13 | //@ts-ignore
14 | alert(url + ', ' + text)}>
15 |
16 | This text will be parsed to check for clickable strings like
17 | https://github.com/obipawan/hyperlink and made clickable.
18 |
19 | ,
20 | //@ts-ignore
21 | alert(url + ', ' + text)}>
22 |
23 | This text will be parsed to check for clickable strings like
24 | https://github.com/obipawan/hyperlink and made clickable for long click.
25 |
26 | ,
27 | //@ts-ignore
28 | alert(url + ', ' + text)}>
29 |
30 |
31 | A nested Text component
32 | https://facebook.github.io/react-native/docs/text.html works equally
33 | well with https://github.com/obipawan/hyperlink
34 |
35 |
36 | ,
37 |
41 |
42 | Make clickable strings like https://github.com/obipawan/hyperlink stylable
43 |
44 | ,
45 |
48 | url === 'https://github.com/obipawan/hyperlink' ? 'Hyperlink' : url
49 | }
50 | >
51 |
52 | Make clickable strings cleaner with https://github.com/obipawan/hyperlink
53 |
54 | ,
55 | ({
58 | testID: url === 'http://link.com' ? 'id1' : 'id2',
59 | style: url === 'https://link.com' ? { color: 'red' } : { color: 'blue' },
60 | //any other props you wish to pass to the component
61 | })}
62 | >
63 |
64 | You can pass props to clickable components matched by url.
65 | This url looks red https://link.com and this url looks blue
66 | https://link2.com{' '}
67 |
68 | ,
69 | ];
70 |
71 | const renderItem = ({ index }: { index: number }) => examples[index] || <>>;
72 | export default function App() {
73 | return (
74 |
75 | (
79 |
82 | )}
83 | />
84 |
85 | );
86 | }
87 |
88 | const styles = StyleSheet.create({
89 | container: {
90 | flex: 1,
91 | alignItems: 'center',
92 | justifyContent: 'center',
93 | paddingTop: 100,
94 | },
95 | box: {
96 | width: 60,
97 | height: 60,
98 | marginVertical: 20,
99 | },
100 | });
101 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig",
3 | "compilerOptions": {
4 | // Avoid expo-cli auto-generating a tsconfig
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config');
3 | const { resolver } = require('./metro.config');
4 |
5 | const root = path.resolve(__dirname, '..');
6 | const node_modules = path.join(__dirname, 'node_modules');
7 |
8 | module.exports = async function (env, argv) {
9 | const config = await createExpoWebpackConfigAsync(env, argv);
10 |
11 | config.module.rules.push({
12 | test: /\.(js|jsx|ts|tsx)$/,
13 | include: path.resolve(root, 'src'),
14 | use: 'babel-loader',
15 | });
16 |
17 | // We need to make sure that only one version is loaded for peerDependencies
18 | // So we alias them to the versions in example's node_modules
19 | Object.assign(config.resolve.alias, {
20 | ...resolver.extraNodeModules,
21 | 'react-native-web': path.join(node_modules, 'react-native-web'),
22 | });
23 |
24 | return config;
25 | };
26 |
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | parallel: true
3 | commands:
4 | lint:
5 | files: git diff --name-only @{push}
6 | glob: "*.{js,ts,jsx,tsx}"
7 | run: npx eslint {files}
8 | types:
9 | files: git diff --name-only @{push}
10 | glob: "*.{js,ts, jsx, tsx}"
11 | run: npx tsc --noEmit
12 | commit-msg:
13 | parallel: true
14 | commands:
15 | commitlint:
16 | run: npx commitlint --edit
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-hyperlink",
3 | "version": "0.0.22",
4 | "description": "A component for react-native to make urls, fuzzy links, emails etc clickable",
5 | "main": "dist/commonjs/index.js",
6 | "module": "dist/module/index.js",
7 | "types": "dist/typescript/src/index.d.ts",
8 | "react-native": "src/index.tsx",
9 | "source": "src/index",
10 | "files": [
11 | "src",
12 | "dist",
13 | "!**/__tests__",
14 | "!**/__fixtures__",
15 | "!**/__mocks__",
16 | "lib",
17 | "android",
18 | "ios",
19 | "cpp",
20 | "react-native-hyperlink.podspec",
21 | "!lib/typescript/example",
22 | "!android/build",
23 | "!ios/build"
24 | ],
25 | "scripts": {
26 | "test": "jest",
27 | "typescript": "tsc --noEmit",
28 | "lint": "eslint \"**/*.{js,ts,tsx}\"",
29 | "prepare": "bob build",
30 | "release": "release-it",
31 | "example": "yarn --cwd example",
32 | "bootstrap": "yarn example && yarn && yarn example pods"
33 | },
34 | "keywords": [
35 | "react",
36 | "react-native",
37 | "text",
38 | "link",
39 | "hyperlink",
40 | "autolink",
41 | "url"
42 | ],
43 | "repository": "https://github.com/obipawan/react-native-hyperlink",
44 | "author": "Pawan Kumar",
45 | "license": "MIT",
46 | "bugs": {
47 | "url": "https://github.com/obipawan/react-native-hyperlink/issues"
48 | },
49 | "homepage": "https://github.com/obipawan/react-native-hyperlink#readme",
50 | "publishConfig": {
51 | "registry": "https://registry.npmjs.org/"
52 | },
53 | "devDependencies": {
54 | "@arkweid/lefthook": "^0.7.7",
55 | "@babel/eslint-parser": "^7.18.2",
56 | "@commitlint/config-conventional": "^17.0.2",
57 | "@react-native-community/eslint-config": "^3.0.2",
58 | "@release-it/conventional-changelog": "^5.0.0",
59 | "@types/jest": "^28.1.2",
60 | "@types/linkify-it": "^3.0.2",
61 | "@types/mdurl": "^1.0.2",
62 | "@types/react": "~17.0.21",
63 | "@types/react-native": "0.68.0",
64 | "commitlint": "^17.0.2",
65 | "eslint": "^8.4.1",
66 | "eslint-config-prettier": "^8.5.0",
67 | "eslint-plugin-prettier": "^4.0.0",
68 | "jest": "^28.1.1",
69 | "pod-install": "^0.1.0",
70 | "prettier": "^2.0.5",
71 | "react": "18.0.0",
72 | "react-native": "0.69.4",
73 | "react-native-builder-bob": "^0.18.3",
74 | "release-it": "^15.0.0",
75 | "typescript": "^4.5.2"
76 | },
77 | "resolutions": {
78 | "@types/react": "17.0.21"
79 | },
80 | "peerDependencies": {
81 | "react": "*",
82 | "react-native": "*"
83 | },
84 | "jest": {
85 | "preset": "react-native",
86 | "modulePathIgnorePatterns": [
87 | "/example/node_modules",
88 | "/lib/",
89 | "/dist/"
90 | ]
91 | },
92 | "commitlint": {
93 | "extends": [
94 | "@commitlint/config-conventional"
95 | ]
96 | },
97 | "release-it": {
98 | "git": {
99 | "commitMessage": "chore: release ${version}",
100 | "tagName": "v${version}"
101 | },
102 | "npm": {
103 | "publish": true
104 | },
105 | "github": {
106 | "release": true
107 | },
108 | "plugins": {
109 | "@release-it/conventional-changelog": {
110 | "preset": "angular"
111 | }
112 | }
113 | },
114 | "eslintConfig": {
115 | "root": true,
116 | "parser": "@babel/eslint-parser",
117 | "extends": [
118 | "@react-native-community",
119 | "prettier"
120 | ],
121 | "rules": {
122 | "prettier/prettier": [
123 | "error",
124 | {
125 | "arrowParens": "avoid",
126 | "bracketSameLine": false,
127 | "bracketSpacing": true,
128 | "embeddedLanguageFormatting": "auto",
129 | "htmlWhitespaceSensitivity": "css",
130 | "insertPragma": false,
131 | "jsxSingleQuote": true,
132 | "printWidth": 80,
133 | "proseWrap": "always",
134 | "quoteProps": "consistent",
135 | "requirePragma": false,
136 | "semi": true,
137 | "singleQuote": true,
138 | "tabWidth": 2,
139 | "trailingComma": "all",
140 | "useTabs": true,
141 | "singleAttributePerLine": true
142 | }
143 | ]
144 | }
145 | },
146 | "eslintIgnore": [
147 | "node_modules/",
148 | "lib/",
149 | "dist/"
150 | ],
151 | "prettier": {
152 | "arrowParens": "avoid",
153 | "bracketSameLine": false,
154 | "bracketSpacing": true,
155 | "embeddedLanguageFormatting": "auto",
156 | "htmlWhitespaceSensitivity": "css",
157 | "insertPragma": false,
158 | "jsxSingleQuote": true,
159 | "printWidth": 80,
160 | "proseWrap": "always",
161 | "quoteProps": "consistent",
162 | "requirePragma": false,
163 | "semi": true,
164 | "singleQuote": true,
165 | "tabWidth": 2,
166 | "trailingComma": "all",
167 | "useTabs": true,
168 | "singleAttributePerLine": true
169 | },
170 | "react-native-builder-bob": {
171 | "source": "src",
172 | "output": "dist",
173 | "targets": [
174 | "commonjs",
175 | "module",
176 | "typescript"
177 | ]
178 | },
179 | "dependencies": {
180 | "linkify-it": "^4.0.1",
181 | "mdurl": "^1.0.1"
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/scripts/bootstrap.js:
--------------------------------------------------------------------------------
1 | const os = require('os');
2 | const path = require('path');
3 | const child_process = require('child_process');
4 |
5 | const root = path.resolve(__dirname, '..');
6 | const args = process.argv.slice(2);
7 | const options = {
8 | cwd: process.cwd(),
9 | env: process.env,
10 | stdio: 'inherit',
11 | encoding: 'utf-8',
12 | };
13 |
14 | if (os.type() === 'Windows_NT') {
15 | options.shell = true;
16 | }
17 |
18 | let result;
19 |
20 | if (process.cwd() !== root || args.length) {
21 | // We're not in the root of the project, or additional arguments were passed
22 | // In this case, forward the command to `yarn`
23 | result = child_process.spawnSync('yarn', args, options);
24 | } else {
25 | // If `yarn` is run without arguments, perform bootstrap
26 | result = child_process.spawnSync('yarn', ['bootstrap'], options);
27 | }
28 |
29 | process.exitCode = result.status;
30 |
--------------------------------------------------------------------------------
/src/Hyperlink.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View, Text, Linking, Platform } from 'react-native';
3 | import mdurl from 'mdurl';
4 | import type {
5 | HyperlinkProps,
6 | HyperlinkState,
7 | ReactElementWithType,
8 | } from './types';
9 |
10 | const linkify = require('linkify-it')();
11 |
12 | const { OS } = Platform;
13 |
14 | class Hyperlink extends Component {
15 | public static defaultProps: Partial = {
16 | linkify,
17 | injectViewProps: _ => ({}),
18 | };
19 |
20 | public static getDerivedStateFromProps(
21 | nextProps: HyperlinkProps,
22 | prevState: HyperlinkState,
23 | ): HyperlinkState | null {
24 | return nextProps.linkify !== prevState.linkifyIt
25 | ? { linkifyIt: nextProps.linkify || linkify }
26 | : null;
27 | }
28 |
29 | constructor(props: HyperlinkProps) {
30 | super(props);
31 | this.state = { linkifyIt: props.linkify || linkify };
32 | }
33 |
34 | render() {
35 | const {
36 | onPress,
37 | linkDefault,
38 | onLongPress,
39 | linkStyle,
40 | linkify,
41 | linkText,
42 | ...viewProps
43 | } = this.props;
44 |
45 | return (
46 |
50 | {!this.props.onPress && !this.props.onLongPress && !this.props.linkStyle
51 | ? this.props.children
52 | : //@ts-ignore
53 | this.parse(this).props.children}
54 |
55 | );
56 | }
57 |
58 | isTextNested(component: ReactElementWithType) {
59 | if (!React.isValidElement(component)) throw new Error('Invalid component');
60 | let { type: { displayName } = {} } = component;
61 | if (displayName !== 'Text') throw new Error('Not a Text component');
62 | return typeof component.props.children !== 'string';
63 | }
64 |
65 | linkify = (component: ReactElementWithType) => {
66 | if (
67 | !this.state.linkifyIt.pretest(component.props.children) ||
68 | !this.state.linkifyIt.test(component.props.children)
69 | )
70 | return component;
71 |
72 | let elements = [];
73 | let _lastIndex = 0;
74 |
75 | const componentProps = {
76 | ...component.props,
77 | ref: undefined,
78 | key: undefined,
79 | };
80 |
81 | try {
82 | this.state.linkifyIt
83 | .match(component.props.children)
84 | ?.forEach(({ index, lastIndex, text, url }) => {
85 | let nonLinkedText = component.props.children.substring(
86 | _lastIndex,
87 | index,
88 | );
89 | nonLinkedText && elements.push(nonLinkedText);
90 | _lastIndex = lastIndex;
91 | if (this.props.linkText)
92 | text =
93 | typeof this.props.linkText === 'function'
94 | ? this.props.linkText(url)
95 | : this.props.linkText;
96 |
97 | const clickHandlerProps: {
98 | onPress?: HyperlinkProps['onPress'];
99 | onLongPress?: HyperlinkProps['onLongPress'];
100 | } = {};
101 | if (OS !== 'web') {
102 | clickHandlerProps.onLongPress = this.props.onLongPress
103 | ? () => this.props.onLongPress?.(url, text)
104 | : undefined;
105 | }
106 | clickHandlerProps.onPress = this.props.onPress
107 | ? () => this.props.onPress?.(url, text)
108 | : undefined;
109 |
110 | elements.push(
111 |
118 | {text}
119 | ,
120 | );
121 | });
122 | elements.push(
123 | component.props.children.substring(
124 | _lastIndex,
125 | component.props.children.length,
126 | ),
127 | );
128 | return React.cloneElement(component, componentProps, elements);
129 | } catch (err) {
130 | return component;
131 | }
132 | };
133 |
134 | parse = (component: ReactElementWithType): ReactElementWithType => {
135 | let { props: { children } = { children: undefined } } = component || {};
136 | if (!children) return component;
137 |
138 | const componentProps = {
139 | ...component.props,
140 | ref: undefined,
141 | key: undefined,
142 | };
143 |
144 | return React.cloneElement(
145 | component,
146 | componentProps,
147 | React.Children.map(children, (child: ReactElementWithType) => {
148 | let { type: { displayName } = { displayName: undefined } } =
149 | child || {};
150 | if (typeof child === 'string' && this.state.linkifyIt.pretest(child))
151 | return this.linkify(
152 |
156 | {child}
157 | ,
158 | );
159 | if (displayName === 'Text' && !this.isTextNested(child))
160 | return this.linkify(child);
161 | return this.parse(child);
162 | }),
163 | );
164 | };
165 | }
166 |
167 | export default class extends Component {
168 | constructor(props: HyperlinkProps) {
169 | super(props);
170 | this.handleLink = this.handleLink.bind(this);
171 | }
172 |
173 | handleLink(url: string) {
174 | const urlObject = mdurl.parse(url);
175 | urlObject.protocol = urlObject.protocol.toLowerCase();
176 | const normalizedURL = mdurl.format(urlObject);
177 |
178 | Linking.canOpenURL(normalizedURL).then(
179 | supported => supported && Linking.openURL(normalizedURL),
180 | );
181 | }
182 |
183 | render() {
184 | const onPress = this.handleLink || this.props.onPress;
185 | return this.props.linkDefault ? (
186 |
190 | ) : (
191 |
192 | );
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | it.todo('write a test');
2 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as Hyperlink } from './Hyperlink';
2 | import Hyperlink from './Hyperlink';
3 |
4 | export default Hyperlink;
5 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type LinkifyIt from 'linkify-it';
2 | import type { ReactElement } from 'react';
3 | import type { TextProps, ViewProps } from 'react-native';
4 |
5 | export type ReactElementWithType = ReactElement & {
6 | type?: { displayName?: string };
7 | };
8 |
9 | export type HyperlinkProps = {
10 | linkDefault?: boolean;
11 | linkify?: LinkifyIt.LinkifyIt;
12 | linkStyle?: TextProps['style'];
13 | linkText?: ((url: string) => string) | string;
14 | onPress?: (url: string, text?: string) => void;
15 | onLongPress?: (url: string, text?: string) => void;
16 | injectViewProps?: (url: string) => TextProps;
17 | style?: ViewProps['style'];
18 | children?: ReactElementWithType;
19 | };
20 |
21 | export type HyperlinkState = { linkifyIt: LinkifyIt.LinkifyIt };
22 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "extends": "./tsconfig",
4 | "exclude": ["example"]
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "react-native-hyperlink": ["./src/index"]
6 | },
7 | "allowUnreachableCode": false,
8 | "allowUnusedLabels": false,
9 | "esModuleInterop": true,
10 | "importsNotUsedAsValues": "error",
11 | "forceConsistentCasingInFileNames": true,
12 | "jsx": "react",
13 | "lib": ["esnext"],
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "noFallthroughCasesInSwitch": true,
17 | "noImplicitReturns": true,
18 | "noImplicitUseStrict": false,
19 | "noStrictGenericChecks": false,
20 | "noUncheckedIndexedAccess": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "resolveJsonModule": true,
24 | "skipLibCheck": true,
25 | "strict": true,
26 | "target": "esnext"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------