linkified_text.dart
1 import 'package:flutter/gestures.dart'; 2 import 'package:flutter/material.dart'; 3 import 'package:url_launcher/url_launcher.dart'; 4 5 /// A widget that renders text with tappable URLs highlighted in blue. 6 class LinkifiedText extends StatelessWidget { 7 final String text; 8 final TextStyle? style; 9 10 const LinkifiedText(this.text, {super.key, this.style}); 11 12 static final _urlRegex = RegExp(r'https?://[^\s<>")\]]+'); 13 14 @override 15 Widget build(BuildContext context) { 16 final matches = _urlRegex.allMatches(text).toList(); 17 if (matches.isEmpty) { 18 return Text(text, style: style); 19 } 20 21 final spans = <InlineSpan>[]; 22 int lastEnd = 0; 23 24 for (final match in matches) { 25 // Add text before the URL 26 if (match.start > lastEnd) { 27 spans.add(TextSpan(text: text.substring(lastEnd, match.start), style: style)); 28 } 29 30 // Strip trailing punctuation that's likely not part of the URL 31 var url = match.group(0)!; 32 var trailingPunct = ''; 33 while (url.isNotEmpty && '.,:;!?)'.contains(url[url.length - 1])) { 34 trailingPunct = url[url.length - 1] + trailingPunct; 35 url = url.substring(0, url.length - 1); 36 } 37 38 // Add the URL span 39 spans.add(TextSpan( 40 text: url, 41 style: style?.copyWith( 42 color: Colors.blue, 43 decoration: TextDecoration.underline, 44 decorationColor: Colors.blue.withAlpha(120), 45 ) ?? const TextStyle( 46 color: Colors.blue, 47 decoration: TextDecoration.underline, 48 ), 49 recognizer: TapGestureRecognizer() 50 ..onTap = () => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication), 51 )); 52 53 // Add trailing punctuation as plain text 54 if (trailingPunct.isNotEmpty) { 55 spans.add(TextSpan(text: trailingPunct, style: style)); 56 } 57 58 lastEnd = match.end; 59 } 60 61 // Add remaining text after last URL 62 if (lastEnd < text.length) { 63 spans.add(TextSpan(text: text.substring(lastEnd), style: style)); 64 } 65 66 return RichText(text: TextSpan(children: spans)); 67 } 68 }