refactor: added link model
This commit is contained in:
parent
612a2b291b
commit
1a97471f50
|
@ -1,113 +1,87 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:linkify_text/linkify_text.dart';
|
||||||
|
|
||||||
|
var scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||||
void main() {
|
void main() {
|
||||||
runApp(MyApp());
|
runApp(MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
// This widget is the root of your application.
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Flutter Demo',
|
title: 'linkify_text Demo',
|
||||||
theme: ThemeData(
|
scaffoldMessengerKey: scaffoldMessengerKey,
|
||||||
// This is the theme of your application.
|
debugShowCheckedModeBanner: false,
|
||||||
//
|
home: App(),
|
||||||
// Try running your application with "flutter run". You'll see the
|
|
||||||
// application has a blue toolbar. Then, without quitting the app, try
|
|
||||||
// changing the primarySwatch below to Colors.green and then invoke
|
|
||||||
// "hot reload" (press "r" in the console where you ran "flutter run",
|
|
||||||
// or simply save your changes to "hot reload" in a Flutter IDE).
|
|
||||||
// Notice that the counter didn't reset back to zero; the application
|
|
||||||
// is not restarted.
|
|
||||||
primarySwatch: Colors.blue,
|
|
||||||
),
|
|
||||||
home: MyHomePage(title: 'Flutter Demo Home Page'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget {
|
class App extends StatefulWidget {
|
||||||
MyHomePage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
// This widget is the home page of your application. It is stateful, meaning
|
|
||||||
// that it has a State object (defined below) that contains fields that affect
|
|
||||||
// how it looks.
|
|
||||||
|
|
||||||
// This class is the configuration for the state. It holds the values (in this
|
|
||||||
// case the title) provided by the parent (in this case the App widget) and
|
|
||||||
// used by the build method of the State. Fields in a Widget subclass are
|
|
||||||
// always marked "final".
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MyHomePageState createState() => _MyHomePageState();
|
_AppState createState() => _AppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _AppState extends State<App> {
|
||||||
int _counter = 0;
|
void showSnackbar(String msg) {
|
||||||
|
scaffoldMessengerKey.currentState!.removeCurrentSnackBar();
|
||||||
void _incrementCounter() {
|
scaffoldMessengerKey.currentState!
|
||||||
setState(() {
|
.showSnackBar(SnackBar(content: Text("$msg")));
|
||||||
// This call to setState tells the Flutter framework that something has
|
|
||||||
// changed in this State, which causes it to rerun the build method below
|
|
||||||
// so that the display can reflect the updated values. If we changed
|
|
||||||
// _counter without calling setState(), then the build method would not be
|
|
||||||
// called again, and so nothing would appear to happen.
|
|
||||||
_counter++;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final space = SizedBox(height: 8);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// This method is rerun every time setState is called, for instance as done
|
|
||||||
// by the _incrementCounter method above.
|
|
||||||
//
|
|
||||||
// The Flutter framework has been optimized to make rerunning build methods
|
|
||||||
// fast, so that you can just rebuild anything that needs updating rather
|
|
||||||
// than having to individually change instances of widgets.
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
body: Padding(
|
||||||
// Here we take the value from the MyHomePage object that was created by
|
padding: const EdgeInsets.all(27.0),
|
||||||
// the App.build method, and use it to set our appbar title.
|
|
||||||
title: Text(widget.title),
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
// Center is a layout widget. It takes a single child and positions it
|
|
||||||
// in the middle of the parent.
|
|
||||||
child: Column(
|
child: Column(
|
||||||
// Column is also a layout widget. It takes a list of children and
|
|
||||||
// arranges them vertically. By default, it sizes itself to fit its
|
|
||||||
// children horizontally, and tries to be as tall as its parent.
|
|
||||||
//
|
|
||||||
// Invoke "debug painting" (press "p" in the console, choose the
|
|
||||||
// "Toggle Debug Paint" action from the Flutter Inspector in Android
|
|
||||||
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
|
|
||||||
// to see the wireframe for each widget.
|
|
||||||
//
|
|
||||||
// Column has various properties to control how it sizes itself and
|
|
||||||
// how it positions its children. Here we use mainAxisAlignment to
|
|
||||||
// center the children vertically; the main axis here is the vertical
|
|
||||||
// axis because Columns are vertical (the cross axis would be
|
|
||||||
// horizontal).
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
Text(
|
children: [
|
||||||
'You have pushed the button this many times:',
|
Center(
|
||||||
),
|
child: LinkifyText(
|
||||||
Text(
|
"01. This text contains a url: https://flutter.dev",
|
||||||
'$_counter',
|
linkStyle: TextStyle(color: Colors.blue, fontSize: 16),
|
||||||
style: Theme.of(context).textTheme.headline4,
|
onTap: (link) {
|
||||||
),
|
showSnackbar(link.value!);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
space,
|
||||||
|
Center(
|
||||||
|
child: LinkifyText(
|
||||||
|
"02. This text contains an email hello@flutter.dev",
|
||||||
|
linkTypes: [LinkType.email],
|
||||||
|
linkStyle: TextStyle(color: Colors.blue, fontSize: 16),
|
||||||
|
onTap: (link) {
|
||||||
|
showSnackbar(link.value!);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
space,
|
||||||
|
Center(
|
||||||
|
child: LinkifyText(
|
||||||
|
"03. This contains an #hashtag",
|
||||||
|
linkTypes: [LinkType.hashTag],
|
||||||
|
linkStyle: TextStyle(color: Colors.blue, fontSize: 16),
|
||||||
|
onTap: (link) {
|
||||||
|
showSnackbar(link.value!);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
space,
|
||||||
|
Center(
|
||||||
|
child: LinkifyText(
|
||||||
|
"04. Flutter is #trending, goto https://flutter.dev to check it out. hey@pub.dev",
|
||||||
|
linkTypes: [LinkType.hashTag, LinkType.url, LinkType.email],
|
||||||
|
linkStyle: TextStyle(color: Colors.blue, fontSize: 16),
|
||||||
|
onTap: (link) {
|
||||||
|
showSnackbar(link.value!);
|
||||||
|
},
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: _incrementCounter,
|
|
||||||
tooltip: 'Increment',
|
|
||||||
child: Icon(Icons.add),
|
|
||||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,13 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
linkify_text:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: ".."
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -151,3 +158,4 @@ packages:
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.12.0 <3.0.0"
|
dart: ">=2.12.0 <3.0.0"
|
||||||
|
flutter: ">=1.17.0"
|
||||||
|
|
|
@ -18,11 +18,14 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.7.0 <3.0.0"
|
sdk: '>=2.12.0 <3.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
linkify_text:
|
||||||
|
path: ../
|
||||||
|
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:example/main.dart';
|
import 'package:linkify_text_example/main.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
library linkify_text;
|
library linkify_text;
|
||||||
|
|
||||||
export 'src/linkify.dart';
|
export 'src/linkify.dart';
|
||||||
|
export 'src/enum.dart';
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
enum LinkType { url, email, hashTag }
|
|
@ -1,13 +1,13 @@
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:linkify_text/src/enum.dart';
|
||||||
|
import 'package:linkify_text/src/model/link.dart';
|
||||||
import 'package:linkify_text/src/utils/regex.dart';
|
import 'package:linkify_text/src/utils/regex.dart';
|
||||||
|
|
||||||
enum LinkOption { url, email, hashTag }
|
|
||||||
|
|
||||||
/// Linkify [text] containing urls, emails or hashtag
|
/// Linkify [text] containing urls, emails or hashtag
|
||||||
class LinkifyText extends StatelessWidget {
|
class LinkifyText extends StatelessWidget {
|
||||||
const LinkifyText(this.text,
|
const LinkifyText(this.text,
|
||||||
{this.textStyle, this.linkStyle, this.options, this.onTap, Key? key})
|
{this.textStyle, this.linkStyle, this.linkTypes, this.onTap, Key? key})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
/// text to be linkified
|
/// text to be linkified
|
||||||
|
@ -21,29 +21,28 @@ class LinkifyText extends StatelessWidget {
|
||||||
|
|
||||||
/// called when a formatted link is pressed, it returns the link as a parameter
|
/// called when a formatted link is pressed, it returns the link as a parameter
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// LinkifyText("#helloWorld", onTap: (value) {
|
/// LinkifyText("#helloWorld", onTap: (link) {
|
||||||
/// // do stuff with value
|
/// // do stuff with link
|
||||||
/// print("$value hashtag was tapped");
|
/// print("${link.value} hashtag was tapped");
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
final void Function(String)? onTap;
|
final void Function(Link)? onTap;
|
||||||
|
|
||||||
/// option to override the links to be formatted in the text, defaults to `[LinkOption.url]`
|
/// option to override the links to be formatted in the text, defaults to `[LinkType.url]`
|
||||||
/// so only urls are being linkified in the text
|
/// so only urls are being linkified in the text
|
||||||
final List<LinkOption>? options;
|
final List<LinkType>? linkTypes;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// TODO: add all Text properties
|
|
||||||
return Text.rich(
|
return Text.rich(
|
||||||
linkify(
|
linkify(
|
||||||
text: text,
|
text: text,
|
||||||
linkStyle: linkStyle,
|
linkStyle: linkStyle,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
options: options,
|
linkTypes: linkTypes,
|
||||||
),
|
),
|
||||||
key: key,
|
key: key,
|
||||||
style: linkStyle,
|
style: textStyle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,10 +50,10 @@ class LinkifyText extends StatelessWidget {
|
||||||
TextSpan linkify({
|
TextSpan linkify({
|
||||||
String text = '',
|
String text = '',
|
||||||
TextStyle? linkStyle,
|
TextStyle? linkStyle,
|
||||||
List<LinkOption>? options = const [LinkOption.url],
|
List<LinkType>? linkTypes,
|
||||||
Function(String)? onTap,
|
Function(Link)? onTap,
|
||||||
}) {
|
}) {
|
||||||
RegExp _regExp = constructRegExpFromOptions(options!);
|
RegExp _regExp = constructRegExpFromLinkType(linkTypes ?? [LinkType.url]);
|
||||||
|
|
||||||
// return the full text if there's no match or if empty
|
// return the full text if there's no match or if empty
|
||||||
if (!_regExp.hasMatch(text) || text.isEmpty) return TextSpan(text: text);
|
if (!_regExp.hasMatch(text) || text.isEmpty) return TextSpan(text: text);
|
||||||
|
@ -69,12 +68,11 @@ TextSpan linkify({
|
||||||
));
|
));
|
||||||
if (links.length > 0) {
|
if (links.length > 0) {
|
||||||
RegExpMatch match = links.removeAt(0);
|
RegExpMatch match = links.removeAt(0);
|
||||||
String link = match.input.substring(match.start, match.end);
|
Link link = Link.fromMatch(match);
|
||||||
|
|
||||||
// add the link
|
// add the link
|
||||||
spans.add(
|
spans.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: link,
|
text: link.value,
|
||||||
style: linkStyle,
|
style: linkStyle,
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () {
|
||||||
|
@ -84,6 +82,5 @@ TextSpan linkify({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TextSpan(children: spans);
|
return TextSpan(children: spans);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import 'package:linkify_text/src/enum.dart';
|
||||||
|
import 'package:linkify_text/src/utils/regex.dart';
|
||||||
|
|
||||||
|
class Link {
|
||||||
|
String? _value;
|
||||||
|
LinkType? _type;
|
||||||
|
String? get value => _value;
|
||||||
|
LinkType? get type => _type;
|
||||||
|
|
||||||
|
/// construct link from matched regExp
|
||||||
|
Link.fromMatch(RegExpMatch match)
|
||||||
|
: this._type = getMatchedType(match),
|
||||||
|
this._value = match.input.substring(match.start, match.end);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:linkify_text/linkify_text.dart';
|
import 'package:linkify_text/src/enum.dart';
|
||||||
|
|
||||||
String urlRegExp = r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+';
|
String urlRegExp = r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+';
|
||||||
|
|
||||||
|
@ -7,29 +7,29 @@ String hashtagRegExp = r'(#+[a-zA-Z0-9(_)]{1,})';
|
||||||
String emailRegExp =
|
String emailRegExp =
|
||||||
r"([a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+)";
|
r"([a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+)";
|
||||||
|
|
||||||
/// construct regexp. pattern from provided link options
|
/// construct regexp. pattern from provided link linkTypes
|
||||||
RegExp constructRegExpFromOptions(List<LinkOption> options) {
|
RegExp constructRegExpFromLinkType(List<LinkType> linkTypes) {
|
||||||
// default case where we always want to match url strings
|
// default case where we always want to match url strings
|
||||||
if (options.length == 1 && options.first == LinkOption.url)
|
if (linkTypes.length == 1 && linkTypes.first == LinkType.url)
|
||||||
return RegExp(urlRegExp);
|
return RegExp(urlRegExp);
|
||||||
|
|
||||||
StringBuffer _regexBuffer = StringBuffer();
|
StringBuffer _regexBuffer = StringBuffer();
|
||||||
for (var i = 0; i < options.length; i++) {
|
for (var i = 0; i < linkTypes.length; i++) {
|
||||||
final o = options[i];
|
final _type = linkTypes[i];
|
||||||
final isLast = i == options.length - 1;
|
final _isLast = i == linkTypes.length - 1;
|
||||||
switch (o) {
|
switch (_type) {
|
||||||
case LinkOption.url:
|
case LinkType.url:
|
||||||
isLast
|
_isLast
|
||||||
? _regexBuffer.write("($urlRegExp)")
|
? _regexBuffer.write("($urlRegExp)")
|
||||||
: _regexBuffer.write("($urlRegExp)|");
|
: _regexBuffer.write("($urlRegExp)|");
|
||||||
break;
|
break;
|
||||||
case LinkOption.hashTag:
|
case LinkType.hashTag:
|
||||||
isLast
|
_isLast
|
||||||
? _regexBuffer.write("($hashtagRegExp)")
|
? _regexBuffer.write("($hashtagRegExp)")
|
||||||
: _regexBuffer.write("($hashtagRegExp)|");
|
: _regexBuffer.write("($hashtagRegExp)|");
|
||||||
break;
|
break;
|
||||||
case LinkOption.email:
|
case LinkType.email:
|
||||||
isLast
|
_isLast
|
||||||
? _regexBuffer.write("($emailRegExp)")
|
? _regexBuffer.write("($emailRegExp)")
|
||||||
: _regexBuffer.write("($emailRegExp)|");
|
: _regexBuffer.write("($emailRegExp)|");
|
||||||
break;
|
break;
|
||||||
|
@ -38,3 +38,15 @@ RegExp constructRegExpFromOptions(List<LinkOption> options) {
|
||||||
}
|
}
|
||||||
return RegExp(_regexBuffer.toString());
|
return RegExp(_regexBuffer.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LinkType getMatchedType(RegExpMatch match) {
|
||||||
|
late LinkType _type;
|
||||||
|
if (RegExp(urlRegExp).hasMatch(match.input)) {
|
||||||
|
_type = LinkType.url;
|
||||||
|
} else if (RegExp(hashtagRegExp).hasMatch(match.input)) {
|
||||||
|
_type = LinkType.hashTag;
|
||||||
|
} else if (RegExp(hashtagRegExp).hasMatch(match.input)) {
|
||||||
|
_type = LinkType.email;
|
||||||
|
}
|
||||||
|
return _type;
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import 'package:linkify_text/linkify_text.dart';
|
import 'package:linkify_text/src/enum.dart';
|
||||||
import 'package:linkify_text/src/utils/regex.dart';
|
import 'package:linkify_text/src/utils/regex.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
// TODO: write more testsss
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Regular Expression', () {
|
group('Regular Expression', () {
|
||||||
/// test values
|
/// test values
|
||||||
|
@ -69,15 +68,12 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Should construct regex pattern from LinkOptions and match", () {
|
test("Should construct regex pattern from LinkTypes and match", () {
|
||||||
RegExp _urlRegExp = constructRegExpFromOptions([LinkOption.url]);
|
RegExp _urlRegExp = constructRegExpFromLinkType([LinkType.url]);
|
||||||
RegExp _hashtagRegExp = constructRegExpFromOptions([LinkOption.hashTag]);
|
RegExp _hashtagRegExp = constructRegExpFromLinkType([LinkType.hashTag]);
|
||||||
RegExp _emailRegExp = constructRegExpFromOptions([LinkOption.email]);
|
RegExp _emailRegExp = constructRegExpFromLinkType([LinkType.email]);
|
||||||
RegExp _textRegExp = constructRegExpFromOptions(
|
RegExp _textRegExp = constructRegExpFromLinkType(
|
||||||
[LinkOption.url, LinkOption.hashTag, LinkOption.email]);
|
[LinkType.url, LinkType.hashTag, LinkType.email]);
|
||||||
// _textRegExp.allMatches(_text).forEach((e) {
|
|
||||||
// print(e.group(0));
|
|
||||||
// });
|
|
||||||
expect(_urlRegExp.allMatches(_urlText).length, 4);
|
expect(_urlRegExp.allMatches(_urlText).length, 4);
|
||||||
expect(_hashtagRegExp.allMatches(_hashtagText).length, 2);
|
expect(_hashtagRegExp.allMatches(_hashtagText).length, 2);
|
||||||
expect(_emailRegExp.allMatches(_emailText).length, 2);
|
expect(_emailRegExp.allMatches(_emailText).length, 2);
|
||||||
|
|
Loading…
Reference in New Issue