├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── linkify_example.dart ├── lib ├── linkify.dart └── src │ ├── email.dart │ ├── phone_number.dart │ ├── url.dart │ └── user_tag.dart ├── pubspec.yaml └── test └── linkify_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | .packages 4 | .pub/ 5 | build/ 6 | doc/api/ 7 | 8 | # Packages should not commit the "pubspec.lock" file. See 9 | # https://stackoverflow.com/a/16136740/8358501 10 | /pubspec.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [5.0.0] - 2023-05-15 2 | 3 | - Upgrade to Dart 3 4 | - Merge Add Origin Text ([#47](https://github.com/Cretezy/linkify/pull/47)) 5 | - Merge Support UserTagLinkifier on Safari ([#48](https://github.com/Cretezy/linkify/pull/48)) 6 | - Merge Add quotation marks to loose url regex ([#50](https://github.com/Cretezy/linkify/pull/50)) 7 | - Merge Remove `pubspec.lock` from `.gitignore` ([#51](https://github.com/Cretezy/linkify/pull/51)) 8 | - Merge Support UserTagLinkifier on Safari ([#52](https://github.com/Cretezy/linkify/pull/52)) 9 | 10 | ## [4.1.0] - 2021-08-02 11 | 12 | - Fix loose URL not being parsed if the text have a non loose URL ([#42](https://github.com/Cretezy/linkify/pull/42), thanks [@EsteveAguilera](https://github.com/EsteveAguilera)!) 13 | - User Tagging Linkifier ([#38](https://github.com/Cretezy/linkify/pull/38), thanks [@HSCOGT](https://github.com/HSCOGT)!) 14 | 15 | ## [4.0.0] - 2021-03-04 16 | 17 | - Add null-safety support. Now required Dart >=2.12 18 | 19 | ## [3.0.0] - 2020-11-05 20 | 21 | - Expand parsing to `www.` URLs ([#21](https://github.com/Cretezy/linkify/pull/21), thanks [@SpencerLindemuth](https://github.com/SpencerLindemuth)!) 22 | - Add `\r` parsing, requires Dart >=2.4 ([#26](https://github.com/Cretezy/linkify/pull/26), thanks [@hpoul](https://github.com/hpoul)!) 23 | - Update loose URL regex to make it more reliable (thanks for [the suggestion](https://github.com/Cretezy/linkify/issues/19#issuecomment-640587130) [@olestole](https://github.com/olestole)!) 24 | 25 | **Major version has been bumped**: 26 | 27 | - Minimum Dart version was upgraded 28 | - Loose URL regex update may change behavior for some use-cases. Please open an issue if you find more issues! 29 | - Non-loose will now parse URLs starting with `www.`, changing behavior 30 | 31 | ## [2.1.0] - 2020-04-24 32 | 33 | - Add loose URL option (`looseUrl`) 34 | - Parses any URL containing `.` 35 | - Defaults to `http` URLs. Can use `https` by enabling the `defaultToHttps` option 36 | - Added `www.` removal (`removeWww`) 37 | - Removes URLs prefixed with `www.` 38 | - Added exclusion of last period (`excludeLastPeriod`, enabled by default) 39 | - Parses `https://example.com.` as `https://example.com` 40 | 41 | ## [2.0.3] - 2020-01-08 42 | 43 | - Fix more minor lint issues 44 | - Remove extra `print` 45 | 46 | ## [2.0.2] - 2019-12-30 47 | 48 | - Fix minor lint issues 49 | 50 | ## [2.0.1] - 2019-12-27 51 | 52 | - Export `defaultLinkifiers` 53 | 54 | ## [2.0.0] - 2019-12-27 55 | 56 | - Change `LinkTypes` to `Linkifier` 57 | - Supports custom linkifiers 58 | - Change `LinkElement` to `UrlElement` to better reflect `UrlLinkifier` (link != URL) 59 | - Change `humanize` option to `LinkifyOptions` 60 | - Enabled `humanize` by default 61 | 62 | ## [1.0.1] - 2019-03-23 63 | 64 | - Republish to fix maintenance score 65 | 66 | ## [1.0.0] - 2019-03-23 67 | 68 | - Initial release 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Charles-William Crete 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `linkify` [![pub package](https://img.shields.io/pub/v/linkify.svg)](https://pub.dartlang.org/packages/linkify) 2 | 3 | Low-level link (text, URLs, emails, phone numbers, user tags) parsing library in Dart. 4 | 5 | Required Dart >=2.12 (has null-safety support). 6 | 7 | [Flutter library](https://github.com/Cretezy/flutter_linkify). 8 | 9 | [Pub](https://pub.dartlang.org/packages/linkify) - [API Docs](https://pub.dartlang.org/documentation/linkify/latest/) - [GitHub](https://github.com/Cretezy/linkify) 10 | 11 | ## Install 12 | 13 | Install by adding this package to your `pubspec.yaml`: 14 | 15 | ```yaml 16 | dependencies: 17 | linkify: ^5.0.0 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```dart 23 | import 'package:linkify/linkify.dart'; 24 | 25 | linkify("Made by https://cretezy.com person@example.com"); 26 | // Output: [TextElement: 'Made by ', UrlElement: 'https://cretezy.com' (cretezy.com), TextElement: ' ', EmailElement: 'person@example.com' (person@example.com)] 27 | ``` 28 | 29 | ### Options 30 | 31 | You can pass `LinkifyOptions` to the `linkify` method to change the humanization of URLs (turning `https://example.com` to `example.com`): 32 | 33 | ```dart 34 | linkify("https://cretezy.com"); 35 | // [UrlElement: 'https://cretezy.com' (cretezy.com)] 36 | 37 | linkify("https://cretezy.com", options: LinkifyOptions(humanize: false)); 38 | // [UrlElement: 'https://cretezy.com' (https://cretezy.com)] 39 | ``` 40 | 41 | - `humanize`: Removes http/https from shown URLs 42 | - `removeWww`: Removes `www.` from shown URLs 43 | - `looseUrl`: Enables loose URL parsing (should parse most URLs such as `abc.com/xyz`) 44 | - `defaultToHttps`: When used with [looseUrl], default to `https` instead of `http` 45 | - `excludeLastPeriod`: Excludes `.` at end of URLs 46 | 47 | ### Linkifiers 48 | 49 | You can pass linkifiers to `linkify` as such: 50 | 51 | ```dart 52 | linkify("@cretezy", linkifiers: [UserTagLinkifier()]); 53 | ``` 54 | 55 | Available linkifiers: 56 | 57 | - `EmailLinkifier` 58 | - `UrlLinkifier` 59 | - `PhoneNumberLinkifier` 60 | - `UserTagLinkifier` 61 | 62 | ## Custom Linkifier 63 | 64 | You can write custom linkifiers for phone numbers or other types of links. Look at the [URL linkifier](./lib/src/url.dart) for an example. 65 | 66 | This is the flow: 67 | 68 | - Calls `parse` in the linkifier with a list of `LinkifyElement`. This starts as `[TextElement(text)]` 69 | - Your parsers then splits each element into it's parts. For example, `[TextElement("Hello https://example.com")]` would become `[TextElement("Hello "), UrlElement("https://example.com")]` 70 | - Each parsers is ran in order of how they are passed to the main `linkify` function. By default, this is URL and email linkifiers 71 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | -------------------------------------------------------------------------------- /example/linkify_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:linkify/linkify.dart'; 2 | 3 | void main() { 4 | print(linkify("Made by https://cretezy.com person@example.com")); 5 | // Output: [TextElement: 'Made by ', UrlElement: 'https://cretezy.com' (cretezy.com), TextElement: ' ', EmailElement: 'person@example.com' (person@example.com)] 6 | } 7 | -------------------------------------------------------------------------------- /lib/linkify.dart: -------------------------------------------------------------------------------- 1 | import 'package:linkify/src/email.dart'; 2 | import 'package:linkify/src/url.dart'; 3 | 4 | export 'package:linkify/src/email.dart' show EmailLinkifier, EmailElement; 5 | export 'package:linkify/src/url.dart' show UrlLinkifier, UrlElement; 6 | export 'package:linkify/src/user_tag.dart' 7 | show UserTagLinkifier, UserTagElement; 8 | export 'package:linkify/src/phone_number.dart' 9 | show PhoneNumberLinkifier, PhoneNumberElement; 10 | 11 | abstract class LinkifyElement { 12 | final String text; 13 | final String originText; 14 | 15 | LinkifyElement(this.text, [String? originText]) 16 | : originText = originText ?? text; 17 | 18 | @override 19 | bool operator ==(other) => equals(other); 20 | 21 | @override 22 | int get hashCode => Object.hash(text, originText); 23 | 24 | bool equals(other) => other is LinkifyElement && other.text == text; 25 | } 26 | 27 | class LinkableElement extends LinkifyElement { 28 | final String url; 29 | 30 | LinkableElement(String? text, this.url, [String? originText]) 31 | : super(text ?? url, originText); 32 | 33 | @override 34 | bool operator ==(other) => equals(other); 35 | 36 | @override 37 | int get hashCode => Object.hash(text, originText, url); 38 | 39 | @override 40 | bool equals(other) => 41 | other is LinkableElement && super.equals(other) && other.url == url; 42 | } 43 | 44 | /// Represents an element containing text 45 | class TextElement extends LinkifyElement { 46 | TextElement(String text) : super(text); 47 | 48 | @override 49 | String toString() { 50 | return "TextElement: '$text'"; 51 | } 52 | 53 | @override 54 | bool operator ==(other) => equals(other); 55 | 56 | @override 57 | int get hashCode => Object.hash(text, originText); 58 | 59 | @override 60 | bool equals(other) => other is TextElement && super.equals(other); 61 | } 62 | 63 | abstract class Linkifier { 64 | const Linkifier(); 65 | 66 | List parse( 67 | List elements, LinkifyOptions options); 68 | } 69 | 70 | class LinkifyOptions { 71 | /// Removes http/https from shown URLs. 72 | final bool humanize; 73 | 74 | /// Removes www. from shown URLs. 75 | final bool removeWww; 76 | 77 | /// Enables loose URL parsing (any string with "." is a URL). 78 | final bool looseUrl; 79 | 80 | /// When used with [looseUrl], default to `https` instead of `http`. 81 | final bool defaultToHttps; 82 | 83 | /// Excludes `.` at end of URLs. 84 | final bool excludeLastPeriod; 85 | 86 | const LinkifyOptions({ 87 | this.humanize = true, 88 | this.removeWww = false, 89 | this.looseUrl = false, 90 | this.defaultToHttps = false, 91 | this.excludeLastPeriod = true, 92 | }); 93 | } 94 | 95 | const _urlLinkifier = UrlLinkifier(); 96 | const _emailLinkifier = EmailLinkifier(); 97 | const defaultLinkifiers = [_urlLinkifier, _emailLinkifier]; 98 | 99 | /// Turns [text] into a list of [LinkifyElement] 100 | /// 101 | /// Use [humanize] to remove http/https from the start of the URL shown. 102 | /// Will default to `false` (if `null`) 103 | /// 104 | /// Uses [linkTypes] to enable some types of links (URL, email). 105 | /// Will default to all (if `null`). 106 | List linkify( 107 | String text, { 108 | LinkifyOptions options = const LinkifyOptions(), 109 | List linkifiers = defaultLinkifiers, 110 | }) { 111 | var list = [TextElement(text)]; 112 | 113 | if (text.isEmpty) { 114 | return []; 115 | } 116 | 117 | if (linkifiers.isEmpty) { 118 | return list; 119 | } 120 | 121 | for (var linkifier in linkifiers) { 122 | list = linkifier.parse(list, options); 123 | } 124 | 125 | return list; 126 | } 127 | -------------------------------------------------------------------------------- /lib/src/email.dart: -------------------------------------------------------------------------------- 1 | import 'package:linkify/linkify.dart'; 2 | 3 | final _emailRegex = RegExp( 4 | r'^(.*?)((mailto:)?[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z][A-Z]+)', 5 | caseSensitive: false, 6 | dotAll: true, 7 | ); 8 | 9 | class EmailLinkifier extends Linkifier { 10 | const EmailLinkifier(); 11 | 12 | @override 13 | List parse(elements, options) { 14 | final list = []; 15 | 16 | for (var element in elements) { 17 | if (element is TextElement) { 18 | final match = _emailRegex.firstMatch(element.text); 19 | 20 | if (match == null) { 21 | list.add(element); 22 | } else { 23 | final text = element.text.replaceFirst(match.group(0)!, ''); 24 | 25 | if (match.group(1)?.isNotEmpty == true) { 26 | list.add(TextElement(match.group(1)!)); 27 | } 28 | 29 | if (match.group(2)?.isNotEmpty == true) { 30 | // Always humanize emails 31 | list.add(EmailElement( 32 | match.group(2)!.replaceFirst(RegExp(r'mailto:'), ''), 33 | )); 34 | } 35 | 36 | if (text.isNotEmpty) { 37 | list.addAll(parse([TextElement(text)], options)); 38 | } 39 | } 40 | } else { 41 | list.add(element); 42 | } 43 | } 44 | 45 | return list; 46 | } 47 | } 48 | 49 | /// Represents an element containing an email address 50 | class EmailElement extends LinkableElement { 51 | final String emailAddress; 52 | 53 | EmailElement(this.emailAddress) : super(emailAddress, 'mailto:$emailAddress'); 54 | 55 | @override 56 | String toString() { 57 | return "EmailElement: '$emailAddress' ($text)"; 58 | } 59 | 60 | @override 61 | bool operator ==(other) => equals(other); 62 | 63 | @override 64 | int get hashCode => Object.hash(text, originText, url, emailAddress); 65 | 66 | @override 67 | bool equals(other) => 68 | other is EmailElement && 69 | super.equals(other) && 70 | other.emailAddress == emailAddress; 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/phone_number.dart: -------------------------------------------------------------------------------- 1 | import 'package:linkify/linkify.dart'; 2 | 3 | final _phoneNumberRegex = RegExp( 4 | r'^(.*?)((tel:)?[+]*[\s/0-9]{8,15})', 5 | caseSensitive: false, 6 | dotAll: true, 7 | ); 8 | 9 | class PhoneNumberLinkifier extends Linkifier { 10 | const PhoneNumberLinkifier(); 11 | 12 | @override 13 | List parse(elements, options) { 14 | final list = []; 15 | 16 | for (var element in elements) { 17 | if (element is TextElement) { 18 | var match = _phoneNumberRegex.firstMatch(element.text); 19 | 20 | if (match == null) { 21 | list.add(element); 22 | } else { 23 | final text = element.text.replaceFirst(match.group(0)!, ''); 24 | 25 | if (match.group(1)?.isNotEmpty == true) { 26 | list.add(TextElement(match.group(1)!)); 27 | } 28 | 29 | if (match.group(2)?.isNotEmpty == true) { 30 | list.add(PhoneNumberElement( 31 | match.group(2)!.replaceFirst(RegExp(r'tel:'), ''), 32 | )); 33 | } 34 | 35 | if (text.isNotEmpty) { 36 | list.addAll(parse([TextElement(text)], options)); 37 | } 38 | } 39 | } else { 40 | list.add(element); 41 | } 42 | } 43 | 44 | return list; 45 | } 46 | } 47 | 48 | /// Represents an element containing a phone number 49 | class PhoneNumberElement extends LinkableElement { 50 | final String phoneNumber; 51 | 52 | PhoneNumberElement(this.phoneNumber) 53 | : super( 54 | phoneNumber, 55 | 'tel:$phoneNumber', 56 | ); 57 | 58 | @override 59 | String toString() { 60 | return "PhoneNumberElement: '$phoneNumber' ($text)"; 61 | } 62 | 63 | @override 64 | bool operator ==(other) => equals(other); 65 | 66 | @override 67 | int get hashCode => Object.hash(text, originText, url, phoneNumber); 68 | 69 | @override 70 | bool equals(other) => 71 | other is PhoneNumberElement && phoneNumber == other.phoneNumber; 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/url.dart: -------------------------------------------------------------------------------- 1 | import 'package:linkify/linkify.dart'; 2 | 3 | final _urlRegex = RegExp( 4 | r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)', 5 | caseSensitive: false, 6 | dotAll: true, 7 | ); 8 | 9 | final _looseUrlRegex = RegExp( 10 | r'''^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//="'`]*))''', 11 | caseSensitive: false, 12 | dotAll: true, 13 | ); 14 | 15 | final _protocolIdentifierRegex = RegExp( 16 | r'^(https?:\/\/)', 17 | caseSensitive: false, 18 | ); 19 | 20 | class UrlLinkifier extends Linkifier { 21 | const UrlLinkifier(); 22 | 23 | @override 24 | List parse(elements, options) { 25 | final list = []; 26 | 27 | for (var element in elements) { 28 | if (element is TextElement) { 29 | var match = options.looseUrl 30 | ? _looseUrlRegex.firstMatch(element.text) 31 | : _urlRegex.firstMatch(element.text); 32 | 33 | if (match == null) { 34 | list.add(element); 35 | } else { 36 | final text = element.text.replaceFirst(match.group(0)!, ''); 37 | 38 | if (match.group(1)?.isNotEmpty == true) { 39 | list.add(TextElement(match.group(1)!)); 40 | } 41 | 42 | if (match.group(2)?.isNotEmpty == true) { 43 | var originalUrl = match.group(2)!; 44 | var originText = originalUrl; 45 | String? end; 46 | 47 | if ((options.excludeLastPeriod) && 48 | originalUrl[originalUrl.length - 1] == ".") { 49 | end = "."; 50 | originText = originText.substring(0, originText.length - 1); 51 | originalUrl = originalUrl.substring(0, originalUrl.length - 1); 52 | } 53 | 54 | var url = originalUrl; 55 | 56 | if (!originalUrl.startsWith(_protocolIdentifierRegex)) { 57 | originalUrl = (options.defaultToHttps ? "https://" : "http://") + 58 | originalUrl; 59 | } 60 | 61 | if ((options.humanize) || (options.removeWww)) { 62 | if (options.humanize) { 63 | url = url.replaceFirst(RegExp(r'https?://'), ''); 64 | } 65 | if (options.removeWww) { 66 | url = url.replaceFirst(RegExp(r'www\.'), ''); 67 | } 68 | 69 | list.add(UrlElement( 70 | originalUrl, 71 | url, 72 | originText, 73 | )); 74 | } else { 75 | list.add(UrlElement(originalUrl, null, originText)); 76 | } 77 | 78 | if (end != null) { 79 | list.add(TextElement(end)); 80 | } 81 | } 82 | 83 | if (text.isNotEmpty) { 84 | list.addAll(parse([TextElement(text)], options)); 85 | } 86 | } 87 | } else { 88 | list.add(element); 89 | } 90 | } 91 | 92 | return list; 93 | } 94 | } 95 | 96 | /// Represents an element containing a link 97 | class UrlElement extends LinkableElement { 98 | UrlElement(String url, [String? text, String? originText]) 99 | : super(text, url, originText); 100 | 101 | @override 102 | String toString() { 103 | return "LinkElement: '$url' ($text)"; 104 | } 105 | 106 | @override 107 | bool operator ==(other) => equals(other); 108 | 109 | @override 110 | int get hashCode => Object.hash(text, originText, url); 111 | 112 | @override 113 | bool equals(other) => other is UrlElement && super.equals(other); 114 | } 115 | -------------------------------------------------------------------------------- /lib/src/user_tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:linkify/linkify.dart'; 2 | 3 | /// For details on how this RegEx works, go to this link. 4 | /// https://regex101.com/r/QN046t/1 5 | final _userTagRegex = RegExp( 6 | r'^(.*?)@([\w@]+(?:[.!][\w@]+)*)', 7 | caseSensitive: false, 8 | dotAll: true, 9 | ); 10 | 11 | class UserTagLinkifier extends Linkifier { 12 | const UserTagLinkifier(); 13 | 14 | @override 15 | List parse(elements, options) { 16 | final list = []; 17 | 18 | for (var element in elements) { 19 | if (element is TextElement) { 20 | var match = _userTagRegex.firstMatch(element.text); 21 | 22 | if (match == null) { 23 | list.add(element); 24 | } else { 25 | var textElement = ''; 26 | var text = element.text.replaceFirst(match.group(0)!, ''); 27 | while (match?.group(1)?.contains(RegExp(r'[\w@]$')) == true) { 28 | textElement += match!.group(0)!; 29 | match = _userTagRegex.firstMatch(text); 30 | if (match == null) { 31 | textElement += text; 32 | text = ''; 33 | } else { 34 | text = text.replaceFirst(match.group(0)!, ''); 35 | } 36 | } 37 | 38 | if (textElement.isNotEmpty || match?.group(1)?.isNotEmpty == true) { 39 | list.add(TextElement(textElement + (match?.group(1) ?? ''))); 40 | } 41 | 42 | if (match?.group(2)?.isNotEmpty == true) { 43 | list.add(UserTagElement('@${match!.group(2)!}')); 44 | } 45 | 46 | if (text.isNotEmpty) { 47 | list.addAll(parse([TextElement(text)], options)); 48 | } 49 | } 50 | } else { 51 | list.add(element); 52 | } 53 | } 54 | 55 | return list; 56 | } 57 | } 58 | 59 | /// Represents an element containing an user tag 60 | class UserTagElement extends LinkableElement { 61 | final String userTag; 62 | 63 | UserTagElement(this.userTag) : super(userTag, userTag); 64 | 65 | @override 66 | String toString() { 67 | return "UserTagElement: '$userTag' ($text)"; 68 | } 69 | 70 | @override 71 | bool operator ==(other) => equals(other); 72 | 73 | @override 74 | int get hashCode => Object.hash(text, originText, url, userTag); 75 | 76 | @override 77 | bool equals(other) => 78 | other is UserTagElement && 79 | super.equals(other) && 80 | other.userTag == userTag; 81 | } 82 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: linkify 2 | description: Low-level link (text, URLs, emails, phone numbers, user tags) parsing library in Dart. 3 | version: 5.0.0 4 | homepage: https://github.com/Cretezy/linkify 5 | 6 | environment: 7 | sdk: ">=2.14.0 <4.0.0" 8 | 9 | dev_dependencies: 10 | collection: ^1.0.0 11 | lints: ^2.1.0 12 | test: ^1.0.0 13 | -------------------------------------------------------------------------------- /test/linkify_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:linkify/linkify.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | final listEqual = const ListEquality().equals; 6 | 7 | void expectListEqual(List actual, List expected) { 8 | expect( 9 | listEqual( 10 | actual, 11 | expected, 12 | ), 13 | true, 14 | reason: "Expected $actual to be $expected", 15 | ); 16 | } 17 | 18 | void main() { 19 | test('Parses only text', () { 20 | expectListEqual( 21 | linkify("Lorem ipsum dolor sit amet"), 22 | [TextElement("Lorem ipsum dolor sit amet")], 23 | ); 24 | }); 25 | 26 | test('Parses only text with multiple lines', () { 27 | expectListEqual( 28 | linkify("Lorem ipsum\ndolor sit amet"), 29 | [TextElement("Lorem ipsum\ndolor sit amet")], 30 | ); 31 | }); 32 | 33 | test('Parses only link', () { 34 | expectListEqual( 35 | linkify("https://example.com"), 36 | [UrlElement("https://example.com", "example.com")], 37 | ); 38 | 39 | expectListEqual( 40 | linkify("https://www.example.com", 41 | options: LinkifyOptions(removeWww: true)), 42 | [UrlElement("https://www.example.com", "example.com")], 43 | ); 44 | }); 45 | 46 | test('Parses only link with no humanize', () { 47 | expectListEqual( 48 | linkify("https://example.com", options: LinkifyOptions(humanize: false)), 49 | [UrlElement("https://example.com")], 50 | ); 51 | }); 52 | 53 | test('Parses only link with removeWwww', () { 54 | expectListEqual( 55 | linkify( 56 | "https://www.example.com", 57 | options: LinkifyOptions(removeWww: true), 58 | ), 59 | [UrlElement("https://www.example.com", "example.com")], 60 | ); 61 | }); 62 | 63 | test('Parses only links with space', () { 64 | expectListEqual( 65 | linkify("https://example.com https://google.com"), 66 | [ 67 | UrlElement("https://example.com", "example.com"), 68 | TextElement(" "), 69 | UrlElement("https://google.com", "google.com"), 70 | ], 71 | ); 72 | }); 73 | 74 | test('Parses links with text', () { 75 | expectListEqual( 76 | linkify( 77 | "Lorem ipsum dolor sit amet https://example.com https://google.com", 78 | ), 79 | [ 80 | TextElement("Lorem ipsum dolor sit amet "), 81 | UrlElement("https://example.com", "example.com"), 82 | TextElement(" "), 83 | UrlElement("https://google.com", "google.com"), 84 | ], 85 | ); 86 | }); 87 | 88 | test('Parses links with text with no humanize', () { 89 | expectListEqual( 90 | linkify( 91 | "Lorem ipsum dolor sit amet https://example.com https://google.com", 92 | options: LinkifyOptions(humanize: false), 93 | ), 94 | [ 95 | TextElement("Lorem ipsum dolor sit amet "), 96 | UrlElement("https://example.com"), 97 | TextElement(" "), 98 | UrlElement("https://google.com"), 99 | ], 100 | ); 101 | }); 102 | 103 | test('Parses links with text with newlines', () { 104 | expectListEqual( 105 | linkify( 106 | "https://google.com\nLorem ipsum\ndolor sit amet\nhttps://example.com", 107 | ), 108 | [ 109 | UrlElement("https://google.com", "google.com"), 110 | TextElement("\nLorem ipsum\ndolor sit amet\n"), 111 | UrlElement("https://example.com", "example.com"), 112 | ], 113 | ); 114 | }); 115 | 116 | test('Parses email', () { 117 | expectListEqual( 118 | linkify("person@example.com"), 119 | [EmailElement("person@example.com")], 120 | ); 121 | }); 122 | 123 | test('Parses email and link', () { 124 | expectListEqual( 125 | linkify("person@example.com at https://google.com"), 126 | [ 127 | EmailElement("person@example.com"), 128 | TextElement(" at "), 129 | UrlElement("https://google.com", "google.com") 130 | ], 131 | ); 132 | }); 133 | 134 | test("Doesn't parses email and link with no linkifiers", () { 135 | expectListEqual( 136 | linkify("person@example.com at https://google.com", linkifiers: []), 137 | [ 138 | TextElement("person@example.com at https://google.com"), 139 | ], 140 | ); 141 | }); 142 | 143 | test('Parses loose URL', () { 144 | expectListEqual( 145 | linkify("example.com/test", options: LinkifyOptions(looseUrl: true)), 146 | [UrlElement("http://example.com/test", "example.com/test")], 147 | ); 148 | 149 | expectListEqual( 150 | linkify("www.example.com", 151 | options: LinkifyOptions( 152 | looseUrl: true, 153 | removeWww: true, 154 | defaultToHttps: true, 155 | )), 156 | [UrlElement("https://www.example.com", "example.com")], 157 | ); 158 | 159 | expectListEqual( 160 | linkify("https://example.com", options: LinkifyOptions(looseUrl: true)), 161 | [UrlElement("https://example.com", "example.com")], 162 | ); 163 | 164 | expectListEqual( 165 | linkify("https://example.com.", options: LinkifyOptions(looseUrl: true)), 166 | [UrlElement("https://example.com", "example.com"), TextElement(".")], 167 | ); 168 | }); 169 | 170 | test('Parses both loose and not URL on the same text', () { 171 | expectListEqual( 172 | linkify('example.com http://example.com', 173 | options: LinkifyOptions(looseUrl: true)), 174 | [ 175 | UrlElement('http://example.com', 'example.com'), 176 | TextElement(' '), 177 | UrlElement('http://example.com', 'example.com') 178 | ], 179 | ); 180 | 181 | expectListEqual( 182 | linkify( 183 | 'This text mixes both loose urls like example.com and not loose urls like http://example.com and http://another.example.com', 184 | options: LinkifyOptions(looseUrl: true)), 185 | [ 186 | TextElement('This text mixes both loose urls like '), 187 | UrlElement('http://example.com', 'example.com'), 188 | TextElement(' and not loose urls like '), 189 | UrlElement('http://example.com', 'example.com'), 190 | TextElement(' and '), 191 | UrlElement('http://another.example.com', 'another.example.com') 192 | ], 193 | ); 194 | }); 195 | 196 | test('Parses ending period', () { 197 | expectListEqual( 198 | linkify("https://example.com/test."), 199 | [ 200 | UrlElement("https://example.com/test", "example.com/test"), 201 | TextElement(".") 202 | ], 203 | ); 204 | }); 205 | 206 | test('Parses CR correctly.', () { 207 | expectListEqual( 208 | linkify('lorem\r\nipsum https://example.com'), 209 | [ 210 | TextElement('lorem\r\nipsum '), 211 | UrlElement('https://example.com', 'example.com'), 212 | ], 213 | ); 214 | }); 215 | 216 | test('Parses user tag', () { 217 | expectListEqual( 218 | linkify( 219 | "@example", 220 | linkifiers: [ 221 | UrlLinkifier(), 222 | EmailLinkifier(), 223 | UserTagLinkifier(), 224 | ], 225 | ), 226 | [UserTagElement("@example")], 227 | ); 228 | }); 229 | 230 | test('Parses email, link, and user tag', () { 231 | expectListEqual( 232 | linkify( 233 | "person@example.com at https://google.com @example", 234 | linkifiers: [ 235 | UrlLinkifier(), 236 | EmailLinkifier(), 237 | UserTagLinkifier(), 238 | ], 239 | ), 240 | [ 241 | EmailElement("person@example.com"), 242 | TextElement(" at "), 243 | UrlElement("https://google.com", "google.com"), 244 | TextElement(" "), 245 | UserTagElement("@example") 246 | ], 247 | ); 248 | }); 249 | 250 | test('Parses invalid phone number', () { 251 | expectListEqual( 252 | linkify( 253 | "This is an invalid numbers 17.00", 254 | linkifiers: [ 255 | UrlLinkifier(), 256 | EmailLinkifier(), 257 | PhoneNumberLinkifier(), 258 | ], 259 | ), 260 | [ 261 | TextElement("This is an invalid numbers 17.00"), 262 | ], 263 | ); 264 | }); 265 | 266 | test('Parses german phone number', () { 267 | expectListEqual( 268 | linkify( 269 | "This is a german example number +49 30 901820", 270 | linkifiers: [ 271 | UrlLinkifier(), 272 | EmailLinkifier(), 273 | PhoneNumberLinkifier(), 274 | ], 275 | ), 276 | [ 277 | TextElement("This is a german example number "), 278 | PhoneNumberElement("+49 30 901820"), 279 | ], 280 | ); 281 | }); 282 | 283 | test('Parses seattle phone number', () { 284 | expectListEqual( 285 | linkify( 286 | "This is a seattle example number +1 206 555 0100", 287 | linkifiers: [ 288 | UrlLinkifier(), 289 | EmailLinkifier(), 290 | PhoneNumberLinkifier(), 291 | ], 292 | ), 293 | [ 294 | TextElement("This is a seattle example number "), 295 | PhoneNumberElement("+1 206 555 0100"), 296 | ], 297 | ); 298 | }); 299 | 300 | test('Parses uk phone number', () { 301 | expectListEqual( 302 | linkify( 303 | "This is an example number from uk: +44 113 496 0000", 304 | linkifiers: [ 305 | UrlLinkifier(), 306 | EmailLinkifier(), 307 | PhoneNumberLinkifier(), 308 | ], 309 | ), 310 | [ 311 | TextElement("This is an example number from uk: "), 312 | PhoneNumberElement("+44 113 496 0000"), 313 | ], 314 | ); 315 | }); 316 | } 317 | --------------------------------------------------------------------------------