refactor: added link model
This commit is contained in:
parent
612a2b291b
commit
1a97471f50
|
@ -1,113 +1,87 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:linkify_text/linkify_text.dart';
|
||||
|
||||
var scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// 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'),
|
||||
title: 'linkify_text Demo',
|
||||
scaffoldMessengerKey: scaffoldMessengerKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: App(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage 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;
|
||||
|
||||
class App extends StatefulWidget {
|
||||
@override
|
||||
_MyHomePageState createState() => _MyHomePageState();
|
||||
_AppState createState() => _AppState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// 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++;
|
||||
});
|
||||
class _AppState extends State<App> {
|
||||
void showSnackbar(String msg) {
|
||||
scaffoldMessengerKey.currentState!.removeCurrentSnackBar();
|
||||
scaffoldMessengerKey.currentState!
|
||||
.showSnackBar(SnackBar(content: Text("$msg")));
|
||||
}
|
||||
|
||||
final space = SizedBox(height: 8);
|
||||
|
||||
@override
|
||||
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(
|
||||
appBar: AppBar(
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// 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.
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(27.0),
|
||||
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,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'You have pushed the button this many times:',
|
||||
),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: LinkifyText(
|
||||
"01. This text contains a url: https://flutter.dev",
|
||||
linkStyle: TextStyle(color: Colors.blue, fontSize: 16),
|
||||
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
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
linkify_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -151,3 +158,4 @@ packages:
|
|||
version: "2.1.0"
|
||||
sdks:
|
||||
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
|
||||
|
||||
environment:
|
||||
sdk: ">=2.7.0 <3.0.0"
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
linkify_text:
|
||||
path: ../
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:example/main.dart';
|
||||
import 'package:linkify_text_example/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
library linkify_text;
|
||||
|
||||
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/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';
|
||||
|
||||
enum LinkOption { url, email, hashTag }
|
||||
|
||||
/// Linkify [text] containing urls, emails or hashtag
|
||||
class LinkifyText extends StatelessWidget {
|
||||
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);
|
||||
|
||||
/// 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
|
||||
/// ```dart
|
||||
/// LinkifyText("#helloWorld", onTap: (value) {
|
||||
/// // do stuff with value
|
||||
/// print("$value hashtag was tapped");
|
||||
/// LinkifyText("#helloWorld", onTap: (link) {
|
||||
/// // do stuff with link
|
||||
/// 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
|
||||
final List<LinkOption>? options;
|
||||
final List<LinkType>? linkTypes;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// TODO: add all Text properties
|
||||
return Text.rich(
|
||||
linkify(
|
||||
text: text,
|
||||
linkStyle: linkStyle,
|
||||
onTap: onTap,
|
||||
options: options,
|
||||
linkTypes: linkTypes,
|
||||
),
|
||||
key: key,
|
||||
style: linkStyle,
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -51,10 +50,10 @@ class LinkifyText extends StatelessWidget {
|
|||
TextSpan linkify({
|
||||
String text = '',
|
||||
TextStyle? linkStyle,
|
||||
List<LinkOption>? options = const [LinkOption.url],
|
||||
Function(String)? onTap,
|
||||
List<LinkType>? linkTypes,
|
||||
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
|
||||
if (!_regExp.hasMatch(text) || text.isEmpty) return TextSpan(text: text);
|
||||
|
@ -69,12 +68,11 @@ TextSpan linkify({
|
|||
));
|
||||
if (links.length > 0) {
|
||||
RegExpMatch match = links.removeAt(0);
|
||||
String link = match.input.substring(match.start, match.end);
|
||||
|
||||
Link link = Link.fromMatch(match);
|
||||
// add the link
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: link,
|
||||
text: link.value,
|
||||
style: linkStyle,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
|
@ -84,6 +82,5 @@ TextSpan linkify({
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
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/\-?=%.]+';
|
||||
|
||||
|
@ -7,29 +7,29 @@ String hashtagRegExp = r'(#+[a-zA-Z0-9(_)]{1,})';
|
|||
String emailRegExp =
|
||||
r"([a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+)";
|
||||
|
||||
/// construct regexp. pattern from provided link options
|
||||
RegExp constructRegExpFromOptions(List<LinkOption> options) {
|
||||
/// construct regexp. pattern from provided link linkTypes
|
||||
RegExp constructRegExpFromLinkType(List<LinkType> linkTypes) {
|
||||
// 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);
|
||||
|
||||
StringBuffer _regexBuffer = StringBuffer();
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
final o = options[i];
|
||||
final isLast = i == options.length - 1;
|
||||
switch (o) {
|
||||
case LinkOption.url:
|
||||
isLast
|
||||
for (var i = 0; i < linkTypes.length; i++) {
|
||||
final _type = linkTypes[i];
|
||||
final _isLast = i == linkTypes.length - 1;
|
||||
switch (_type) {
|
||||
case LinkType.url:
|
||||
_isLast
|
||||
? _regexBuffer.write("($urlRegExp)")
|
||||
: _regexBuffer.write("($urlRegExp)|");
|
||||
break;
|
||||
case LinkOption.hashTag:
|
||||
isLast
|
||||
case LinkType.hashTag:
|
||||
_isLast
|
||||
? _regexBuffer.write("($hashtagRegExp)")
|
||||
: _regexBuffer.write("($hashtagRegExp)|");
|
||||
break;
|
||||
case LinkOption.email:
|
||||
isLast
|
||||
case LinkType.email:
|
||||
_isLast
|
||||
? _regexBuffer.write("($emailRegExp)")
|
||||
: _regexBuffer.write("($emailRegExp)|");
|
||||
break;
|
||||
|
@ -38,3 +38,15 @@ RegExp constructRegExpFromOptions(List<LinkOption> options) {
|
|||
}
|
||||
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:test/test.dart';
|
||||
|
||||
// TODO: write more testsss
|
||||
void main() {
|
||||
group('Regular Expression', () {
|
||||
/// test values
|
||||
|
@ -69,15 +68,12 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test("Should construct regex pattern from LinkOptions and match", () {
|
||||
RegExp _urlRegExp = constructRegExpFromOptions([LinkOption.url]);
|
||||
RegExp _hashtagRegExp = constructRegExpFromOptions([LinkOption.hashTag]);
|
||||
RegExp _emailRegExp = constructRegExpFromOptions([LinkOption.email]);
|
||||
RegExp _textRegExp = constructRegExpFromOptions(
|
||||
[LinkOption.url, LinkOption.hashTag, LinkOption.email]);
|
||||
// _textRegExp.allMatches(_text).forEach((e) {
|
||||
// print(e.group(0));
|
||||
// });
|
||||
test("Should construct regex pattern from LinkTypes and match", () {
|
||||
RegExp _urlRegExp = constructRegExpFromLinkType([LinkType.url]);
|
||||
RegExp _hashtagRegExp = constructRegExpFromLinkType([LinkType.hashTag]);
|
||||
RegExp _emailRegExp = constructRegExpFromLinkType([LinkType.email]);
|
||||
RegExp _textRegExp = constructRegExpFromLinkType(
|
||||
[LinkType.url, LinkType.hashTag, LinkType.email]);
|
||||
expect(_urlRegExp.allMatches(_urlText).length, 4);
|
||||
expect(_hashtagRegExp.allMatches(_hashtagText).length, 2);
|
||||
expect(_emailRegExp.allMatches(_emailText).length, 2);
|
||||
|
|
Loading…
Reference in New Issue