refactor: added link model

This commit is contained in:
iamstanlee 2021-06-03 19:56:41 -07:00
parent 612a2b291b
commit 1a97471f50
10 changed files with 135 additions and 129 deletions

View File

@ -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.
);
}
}

View File

@ -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"

View File

@ -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.

View File

@ -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 {

View File

@ -2,3 +2,4 @@
library linkify_text;
export 'src/linkify.dart';
export 'src/enum.dart';

1
lib/src/enum.dart Normal file
View File

@ -0,0 +1 @@
enum LinkType { url, email, hashTag }

View File

@ -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);
}

14
lib/src/model/link.dart Normal file
View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);