LCOV - code coverage report
Current view: top level - lib/third_party/linkify - flutter_linkify.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 88 0.0 %
Date: 2026-07-15 19:28:30 Functions: 0 0 -

          Line data    Source code
       1             : // Code Originally taken from https://github.com/Cretezy/flutter_linkify/blob/201e147e0b07b7ca5c543da8167d712d81760753/lib/flutter_linkify.dart
       2             : //
       3             : // Now uses local `linkify`
       4             : //
       5             : // Original License for this code:
       6             : // MIT License
       7             : //     Copyright (c) 2020 Charles-William Crete
       8             : //
       9             : //     Permission is hereby granted, free of charge, to any person obtaining a copy
      10             : //     of this software and associated documentation files (the "Software"), to deal
      11             : //     in the Software without restriction, including without limitatifon the rights
      12             : //     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      13             : //     copies of the Software, and to permit persons to whom the Software is
      14             : //     furnished to do so, subject to the following conditions:
      15             : //
      16             : //     The above copyright notice and this permission notice shall be included in all
      17             : //     copies or substantial portions of the Software.
      18             : //
      19             : //     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      20             : //     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      21             : //     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      22             : //     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      23             : //     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      24             : //     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
      25             : //     SOFTWARE.
      26             : 
      27             : import 'package:flutter/gestures.dart';
      28             : import 'package:flutter/material.dart';
      29             : 
      30             : import 'linkify.dart';
      31             : 
      32             : export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier, UrlElement, UrlLinkifier;
      33             : 
      34             : /// Callback clicked link
      35             : typedef LinkCallback = void Function(LinkableElement link);
      36             : 
      37             : /// Turns URLs into links
      38             : class SelectableLinkify extends StatelessWidget {
      39             :   /// Text to be linkified
      40             :   final String text;
      41             : 
      42             :   /// The number of font pixels for each logical pixel
      43             :   final textScaleFactor;
      44             : 
      45             :   /// Linkifiers to be used for linkify
      46             :   final List<Linkifier> linkifiers;
      47             : 
      48             :   /// Callback for tapping a link
      49             :   final LinkCallback? onOpen;
      50             : 
      51             :   /// linkify's options.
      52             :   final LinkifyOptions options;
      53             : 
      54             :   // TextSpan
      55             : 
      56             :   /// Style for code text
      57             :   final TextStyle codeStyle;
      58             : 
      59             :   /// Style for non-link text
      60             :   final TextStyle style;
      61             : 
      62             :   /// Style of link text
      63             :   final TextStyle linkStyle;
      64             : 
      65             :   // Text.rich
      66             : 
      67             :   /// How the text should be aligned horizontally.
      68             :   final TextAlign? textAlign;
      69             : 
      70             :   /// Text direction of the text
      71             :   final TextDirection? textDirection;
      72             : 
      73             :   /// The minimum number of lines to occupy when the content spans fewer lines.
      74             :   final int? minLines;
      75             : 
      76             :   /// The maximum number of lines for the text to span, wrapping if necessary
      77             :   final int? maxLines;
      78             : 
      79             :   /// The strut style used for the vertical layout
      80             :   final StrutStyle? strutStyle;
      81             : 
      82             :   /// Defines how to measure the width of the rendered text.
      83             :   final TextWidthBasis? textWidthBasis;
      84             : 
      85             :   // SelectableText.rich
      86             : 
      87             :   /// Defines the focus for this widget.
      88             :   final FocusNode? focusNode;
      89             : 
      90             :   /// Whether to show cursor
      91             :   final bool showCursor;
      92             : 
      93             :   /// Whether this text field should focus itself if nothing else is already focused.
      94             :   final bool autofocus;
      95             : 
      96             :   /// How thick the cursor will be
      97             :   final double cursorWidth;
      98             : 
      99             :   /// How rounded the corners of the cursor should be
     100             :   final Radius? cursorRadius;
     101             : 
     102             :   /// The color to use when painting the cursor
     103             :   final Color? cursorColor;
     104             : 
     105             :   /// Determines the way that drag start behavior is handled
     106             :   final DragStartBehavior dragStartBehavior;
     107             : 
     108             :   /// If true, then long-pressing this TextField will select text and show the cut/copy/paste menu,
     109             :   /// and tapping will move the text caret
     110             :   final bool enableInteractiveSelection;
     111             : 
     112             :   /// Called when the user taps on this selectable text (not link)
     113             :   final GestureTapCallback? onTap;
     114             : 
     115             :   final ScrollPhysics? scrollPhysics;
     116             : 
     117             :   /// Defines how the paragraph will apply TextStyle.height to the ascent of the first line and descent of the last line.
     118             :   final TextHeightBehavior? textHeightBehavior;
     119             : 
     120             :   /// How tall the cursor will be.
     121             :   final double? cursorHeight;
     122             : 
     123             :   /// Optional delegate for building the text selection handles and toolbar.
     124             :   final TextSelectionControls? selectionControls;
     125             : 
     126             :   /// Called when the user changes the selection of text (including the cursor location).
     127             :   final SelectionChangedCallback? onSelectionChanged;
     128             : 
     129             :   final BoxConstraints? constraints;
     130             : 
     131           0 :   const SelectableLinkify({
     132             :     Key? key,
     133             :     required this.text,
     134             :     this.linkifiers = defaultLinkifiers,
     135             :     this.onOpen,
     136             :     this.options = const LinkifyOptions(),
     137             :     // TextSpan
     138             :     required this.style,
     139             :     required this.linkStyle,
     140             :     // RichText
     141             :     this.textAlign,
     142             :     required this.codeStyle,
     143             :     this.textDirection,
     144             :     this.minLines,
     145             :     this.maxLines,
     146             :     // SelectableText
     147             :     this.focusNode,
     148             :     this.textScaleFactor = 1.0,
     149             :     this.strutStyle,
     150             :     this.showCursor = false,
     151             :     this.autofocus = false,
     152             :     this.cursorWidth = 2.0,
     153             :     this.cursorRadius,
     154             :     this.cursorColor,
     155             :     this.dragStartBehavior = DragStartBehavior.start,
     156             :     this.enableInteractiveSelection = true,
     157             :     this.onTap,
     158             :     this.scrollPhysics,
     159             :     this.textWidthBasis,
     160             :     this.textHeightBehavior,
     161             :     this.cursorHeight,
     162             :     this.selectionControls,
     163             :     this.onSelectionChanged,
     164             :     this.constraints,
     165           0 :   }) : super(key: key);
     166             : 
     167           0 :   @override
     168             :   Widget build(BuildContext context) {
     169           0 :     final elements = linkify(text, options: options, linkifiers: linkifiers);
     170             : 
     171           0 :     return Container(
     172           0 :       constraints: constraints,
     173           0 :       child: SelectableText.rich(
     174           0 :         buildTextSpan(
     175             :           elements,
     176           0 :           style: style,
     177           0 :           codeStyle: codeStyle,
     178           0 :           onOpen: onOpen,
     179             :           context: context,
     180           0 :           linkStyle: linkStyle.copyWith(decoration: TextDecoration.underline),
     181           0 :           constraints: constraints,
     182             :         ),
     183           0 :         textAlign: textAlign,
     184           0 :         textDirection: textDirection,
     185           0 :         minLines: minLines,
     186           0 :         maxLines: maxLines,
     187           0 :         focusNode: focusNode,
     188           0 :         strutStyle: strutStyle,
     189           0 :         showCursor: showCursor,
     190           0 :         textScaleFactor: textScaleFactor,
     191           0 :         autofocus: autofocus,
     192           0 :         cursorWidth: cursorWidth,
     193           0 :         cursorRadius: cursorRadius,
     194           0 :         cursorColor: cursorColor,
     195           0 :         dragStartBehavior: dragStartBehavior,
     196           0 :         enableInteractiveSelection: enableInteractiveSelection,
     197           0 :         onTap: onTap,
     198           0 :         scrollPhysics: scrollPhysics,
     199           0 :         textWidthBasis: textWidthBasis,
     200           0 :         textHeightBehavior: textHeightBehavior,
     201           0 :         cursorHeight: cursorHeight,
     202           0 :         selectionControls: selectionControls,
     203           0 :         onSelectionChanged: onSelectionChanged,
     204           0 :         style: style,
     205             :       ),
     206             :     );
     207             :   }
     208             : }
     209             : 
     210             : class LinkableSpan extends WidgetSpan {
     211           0 :   LinkableSpan({required MouseCursor mouseCursor, required InlineSpan inlineSpan, required BuildContext context})
     212           0 :     : super(
     213           0 :         child: MouseRegion(
     214             :           cursor: mouseCursor,
     215           0 :           child: RichText(text: inlineSpan, selectionRegistrar: SelectionContainer.maybeOf(context)),
     216             :         ),
     217             :       );
     218             : }
     219             : 
     220             : /// Raw TextSpan builder for more control on the RichText
     221           0 : TextSpan buildTextSpan(
     222             :   List<LinkifyElement> elements, {
     223             :   TextStyle? style,
     224             :   TextStyle? linkStyle,
     225             :   TextStyle? codeStyle,
     226             :   LinkCallback? onOpen,
     227             :   required BuildContext context,
     228             :   bool useMouseRegion = false,
     229             :   BoxConstraints? constraints,
     230             : }) {
     231             :   // Ok, so the problem here is that Flutter really wants to optimize this function
     232             :   // out of the rebuild process. This is fine when the screen gets smaller because
     233             :   // Flutter forces TextSpan to rebuild with the new constraints automatically.
     234             :   // HOWEVER, when the screen gets larger, Flutter seems to think that it doesn't
     235             :   // need to bother rebuilding this TextSpan because it already fits in the provided constraints.
     236             :   // To force a rebuild here we append a constraint-determined space character to the end of the
     237             :   // text element.
     238             :   // (I tried a few other things, including the docs-sanctioned MediaQuery.sizeOf(context) - which promises a rebuild
     239             :   // but Flutter is pretty good at optimizing "useless" checks out)
     240             :   String inlineText = "\u0020";
     241           0 :   if (constraints != null && constraints.maxWidth % 2 == 0) {
     242             :     inlineText = "\u00A0";
     243             :   }
     244           0 :   elements.add(TextElement(inlineText));
     245           0 :   return TextSpan(
     246             :     style: style,
     247           0 :     children: elements.map<InlineSpan>((element) {
     248           0 :       if (element is LinkableElement) {
     249             :         if (useMouseRegion) {
     250           0 :           return TextSpan(
     251           0 :             text: element.text,
     252             :             style: linkStyle,
     253             :             mouseCursor: SystemMouseCursors.click,
     254           0 :             recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null,
     255           0 :             semanticsLabel: element.text,
     256             :           );
     257             :         } else {
     258           0 :           return TextSpan(text: element.text, style: linkStyle, recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null);
     259             :         }
     260           0 :       } else if (element is BoldElement) {
     261           0 :         return TextSpan(
     262           0 :           text: element.text.replaceAll("*", ""),
     263           0 :           style: style?.copyWith(fontWeight: FontWeight.bold),
     264           0 :           semanticsLabel: element.text,
     265             :         );
     266           0 :       } else if (element is ItalicElement) {
     267           0 :         return TextSpan(
     268           0 :           text: element.text.replaceAll("*", ""),
     269           0 :           style: style?.copyWith(fontStyle: FontStyle.italic),
     270           0 :           semanticsLabel: element.text,
     271             :         );
     272           0 :       } else if (element is SuperElement) {
     273           0 :         return WidgetSpan(
     274           0 :           child: Transform.translate(
     275             :             offset: const Offset(2, -6),
     276           0 :             child: Text(
     277           0 :               element.text.replaceAll("^", ""),
     278             :               //superscript is usually smaller in size
     279             :               textScaleFactor: 0.7,
     280             :               style: style,
     281           0 :               semanticsLabel: element.text,
     282             :             ),
     283             :           ),
     284             :         );
     285           0 :       } else if (element is SubElement) {
     286           0 :         return WidgetSpan(
     287           0 :           child: Transform.translate(
     288             :             offset: const Offset(2, 4),
     289           0 :             child: Text(
     290           0 :               element.text.replaceAll("_", ""),
     291             :               //superscript is usually smaller in size
     292             :               textScaleFactor: 0.7,
     293             :               style: style,
     294           0 :               semanticsLabel: element.text,
     295             :             ),
     296             :           ),
     297             :         );
     298           0 :       } else if (element is StrikeElement) {
     299           0 :         return TextSpan(
     300           0 :           text: element.text.replaceAll("~~", ""),
     301           0 :           style: style?.copyWith(decoration: TextDecoration.lineThrough, decorationColor: style.color, decorationStyle: TextDecorationStyle.solid),
     302           0 :           semanticsLabel: element.text,
     303             :         );
     304           0 :       } else if (element is CodeElement) {
     305           0 :         return TextSpan(
     306           0 :           text: element.text.replaceAll("\`", ""),
     307             :           // monospace fonts at the same size as regular text makes them appear
     308             :           // slightly larger, so we compensate by making them slightly smaller...
     309           0 :           style: codeStyle?.copyWith(fontFamily: "RobotoMono", fontSize: codeStyle.fontSize! - 1.5),
     310           0 :           semanticsLabel: element.text,
     311             :         );
     312             :       } else {
     313           0 :         return TextSpan(text: element.text, style: style);
     314             :       }
     315           0 :     }).toList(),
     316             :   );
     317             : }
     318             : 
     319             : // Show a tooltip over an inlined element in a Rich Text widget.
     320             : class TooltipSpan extends WidgetSpan {
     321           0 :   TooltipSpan({required String message, required InlineSpan inlineSpan, required BuildContext context})
     322           0 :     : super(
     323           0 :         child: Tooltip(
     324             :           message: message,
     325           0 :           child: RichText(text: inlineSpan, selectionRegistrar: SelectionContainer.maybeOf(context)),
     326             :         ),
     327             :       );
     328             : }

Generated by: LCOV version 1.14