Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/src/url.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import 'package:linkify/linkify.dart';

final _urlRegex = RegExp(
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)',
r'^(.*?)((?:https?:\/\/|www\.)[^\s<>\x22\x27\)\]\}]*[^\s<>\x22\x27\)\]\}\.,;:!?])(?=$|[\s<>\x22\x27\)\]\}\.,;:!?])',
caseSensitive: false,
dotAll: true,
);

final _looseUrlRegex = RegExp(
r'''^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//="'`]*))''',
r'^(.*?)((?:https?:\/\/)?(?:localhost(?::\d{2,5})(?:[\/?#][^\s<>\x22\x27]*[^\s<>\x22\x27\)\]\}\.,;:!?])?|(?:[\w.%+-]+(?::[\w.%+-]+)?@)?(?:\[(?:[0-9a-f:.]+)\]|(?:\d{1,3}\.){3}\d{1,3}|(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{2,59}))(?::\d{2,5})?(?:[\/?#][^\s<>\x22\x27]*[^\s<>\x22\x27\)\]\}\.\,;:!?])?))(?=$|[\s<>\x22\x27\)\]\}\.,;:!?])',
caseSensitive: false,
dotAll: true,
);
Expand Down
350 changes: 349 additions & 1 deletion test/linkify_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,276 @@ void main() {
);
});

test('Parses ending period', () {
test('Does not parse invalid URLs with consecutive periods', () {
expectListEqual(
linkify('awdaw....aw', options: LinkifyOptions(looseUrl: true)),
[TextElement('awdaw....aw')],
);

expectListEqual(
linkify('awdaw...wad...wadw', options: LinkifyOptions(looseUrl: true)),
[TextElement('awdaw...wad...wadw')],
);

expectListEqual(
linkify('test..example.com', options: LinkifyOptions(looseUrl: true)),
[TextElement('test..'), UrlElement('http://example.com', 'example.com')],
);

expectListEqual(
linkify(
'....and i am a sentence',
options: LinkifyOptions(looseUrl: true),
),
[TextElement('....and i am a sentence')],
);
});

test('Parses subdomains correctly', () {
expectListEqual(
linkify('https://subdomain.example.com'),
[UrlElement('https://subdomain.example.com', 'subdomain.example.com')],
);

expectListEqual(
linkify('https://api.subdomain.example.com'),
[
UrlElement(
'https://api.subdomain.example.com',
'api.subdomain.example.com',
)
],
);

expectListEqual(
linkify('subdomain.example.com', options: LinkifyOptions(looseUrl: true)),
[UrlElement('http://subdomain.example.com', 'subdomain.example.com')],
);

expectListEqual(
linkify(
'Check out api.subdomain.example.com for more info',
options: LinkifyOptions(looseUrl: true),
),
[
TextElement('Check out '),
UrlElement(
'http://api.subdomain.example.com',
'api.subdomain.example.com',
),
TextElement(' for more info'),
],
);
});

test('Parses localhost URLs', () {
expectListEqual(
linkify('http://localhost'),
[UrlElement('http://localhost', 'localhost')],
);

expectListEqual(
linkify('http://localhost:3000'),
[UrlElement('http://localhost:3000', 'localhost:3000')],
);

expectListEqual(
linkify('http://localhost:8080/api/test'),
[UrlElement('http://localhost:8080/api/test', 'localhost:8080/api/test')],
);

expectListEqual(
linkify('localhost', options: LinkifyOptions(looseUrl: true)),
[TextElement('localhost')],
);

expectListEqual(
linkify(
'Check out localhost for testing',
options: LinkifyOptions(looseUrl: true),
),
[TextElement('Check out localhost for testing')],
);

expectListEqual(
linkify('localhost:3000', options: LinkifyOptions(looseUrl: true)),
[UrlElement('http://localhost:3000', 'localhost:3000')],
);
});

test('Parses URLs with ports', () {
expectListEqual(
linkify('https://example.com:8080'),
[UrlElement('https://example.com:8080', 'example.com:8080')],
);

expectListEqual(
linkify('https://api.example.com:3000/path'),
[
UrlElement(
'https://api.example.com:3000/path',
'api.example.com:3000/path',
)
],
);

expectListEqual(
linkify('example.com:8080', options: LinkifyOptions(looseUrl: true)),
[UrlElement('http://example.com:8080', 'example.com:8080')],
);
});

test('Parses IP address URLs', () {
expectListEqual(
linkify('http://192.168.1.1'),
[UrlElement('http://192.168.1.1', '192.168.1.1')],
);

expectListEqual(
linkify('http://192.168.1.1:8080'),
[UrlElement('http://192.168.1.1:8080', '192.168.1.1:8080')],
);

expectListEqual(
linkify('https://10.0.0.1:3000/api'),
[UrlElement('https://10.0.0.1:3000/api', '10.0.0.1:3000/api')],
);

expectListEqual(
linkify('192.168.1.1:8080', options: LinkifyOptions(looseUrl: true)),
[UrlElement('http://192.168.1.1:8080', '192.168.1.1:8080')],
);

expectListEqual(
linkify(
'Check out 192.168.1.1:8080 for the dashboard',
options: LinkifyOptions(looseUrl: true),
),
[
TextElement('Check out '),
UrlElement('http://192.168.1.1:8080', '192.168.1.1:8080'),
TextElement(' for the dashboard'),
],
);
});

test('Parses punycode domains', () {
// xn--n3h.com is ☃.com (snowman emoji domain)
expectListEqual(
linkify('https://xn--n3h.com'),
[UrlElement('https://xn--n3h.com', 'xn--n3h.com')],
);

// xn--bcher-kva.com is bücher.com (books in German)
expectListEqual(
linkify('https://xn--bcher-kva.com'),
[UrlElement('https://xn--bcher-kva.com', 'xn--bcher-kva.com')],
);

expectListEqual(
linkify('xn--n3h.com', options: LinkifyOptions(looseUrl: true)),
[UrlElement('http://xn--n3h.com', 'xn--n3h.com')],
);

expectListEqual(
linkify('Visit xn--bcher-kva.com for more',
options: LinkifyOptions(looseUrl: true)),
[
TextElement('Visit '),
UrlElement('http://xn--bcher-kva.com', 'xn--bcher-kva.com'),
TextElement(' for more'),
],
);
});

test('Parses TLDs with more than 4 letters', () {
expectListEqual(
linkify('https://example.design'),
[UrlElement('https://example.design', 'example.design')],
);

expectListEqual(
linkify('https://example.travel'),
[UrlElement('https://example.travel', 'example.travel')],
);

expectListEqual(
linkify('https://example.cloud'),
[UrlElement('https://example.cloud', 'example.cloud')],
);

expectListEqual(
linkify('example.design', options: LinkifyOptions(looseUrl: true)),
[UrlElement('http://example.design', 'example.design')],
);

expectListEqual(
linkify('example.travel', options: LinkifyOptions(looseUrl: true)),
[UrlElement('http://example.travel', 'example.travel')],
);

expectListEqual(
linkify('example.cloud', options: LinkifyOptions(looseUrl: true)),
[UrlElement('http://example.cloud', 'example.cloud')],
);

expectListEqual(
linkify(
'Check out example.design for more info',
options: LinkifyOptions(looseUrl: true),
),
[
TextElement('Check out '),
UrlElement('http://example.design', 'example.design'),
TextElement(' for more info'),
],
);
});

test('Parses ending period and trailing punctuation', () {
expectListEqual(
linkify("https://example.com/test."),
[
UrlElement("https://example.com/test", "example.com/test"),
TextElement(".")
],
);

expectListEqual(
linkify('Check out https://example.com!'),
[
TextElement('Check out '),
UrlElement('https://example.com', 'example.com'),
TextElement('!'),
],
);

expectListEqual(
linkify('Visit https://example.com, then come back.'),
[
TextElement('Visit '),
UrlElement('https://example.com', 'example.com'),
TextElement(', then come back.'),
],
);

expectListEqual(
linkify('See https://example.com?'),
[
TextElement('See '),
UrlElement('https://example.com', 'example.com'),
TextElement('?'),
],
);

expectListEqual(
linkify('Go to example.com.', options: LinkifyOptions(looseUrl: true)),
[
TextElement('Go to '),
UrlElement('http://example.com', 'example.com'),
TextElement('.'),
],
);
});

test('Parses CR correctly.', () {
Expand Down Expand Up @@ -313,4 +575,90 @@ void main() {
],
);
});

test('Excludes wrapping parentheses from URLs', () {
expectListEqual(
linkify('Some text before (https://github.com/Cretezy/flutter_linkify).'),
[
TextElement('Some text before ('),
UrlElement(
'https://github.com/Cretezy/flutter_linkify',
'github.com/Cretezy/flutter_linkify',
),
TextElement(').'),
],
);

expectListEqual(
linkify('Check this out (https://example.com)'),
[
TextElement('Check this out ('),
UrlElement('https://example.com', 'example.com'),
TextElement(')'),
],
);

expectListEqual(
linkify('Link: [https://example.com]'),
[
TextElement('Link: ['),
UrlElement('https://example.com', 'example.com'),
TextElement(']'),
],
);

expectListEqual(
linkify('Code: {https://example.com}'),
[
TextElement('Code: {'),
UrlElement('https://example.com', 'example.com'),
TextElement('}'),
],
);
});

test('Excludes wrapping brackets from loose URLs', () {
expectListEqual(
linkify(
'Some text before (example.com/path).',
options: LinkifyOptions(looseUrl: true),
),
[
TextElement('Some text before ('),
UrlElement('http://example.com/path', 'example.com/path'),
TextElement(').'),
],
);

expectListEqual(
linkify(
'Check [example.com]',
options: LinkifyOptions(looseUrl: true),
),
[
TextElement('Check ['),
UrlElement('http://example.com', 'example.com'),
TextElement(']'),
],
);
});

test('Does not exclude non-wrapping closing brackets', () {
expectListEqual(
linkify('https://example.com/path)'),
[
UrlElement('https://example.com/path', 'example.com/path'),
TextElement(')'),
],
);

expectListEqual(
linkify('No opening bracket https://example.com]'),
[
TextElement('No opening bracket '),
UrlElement('https://example.com', 'example.com'),
TextElement(']'),
],
);
});
}