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

          Line data    Source code
       1             : // Originally from linkify: https://github.com/Cretezy/linkify/blob/dfb3e43b0e56452bad584ddb0bf9b73d8db0589f/lib/src/url.dart
       2             : //
       3             : // Removed handling of `removeWWW` and `humanize`.
       4             : // Removed auto-appending of `http(s)://` to the readable url
       5             : //
       6             : // MIT License
       7             : //
       8             : // Copyright (c) 2019 Charles-William Crete
       9             : //
      10             : // Permission is hereby granted, free of charge, to any person obtaining a copy
      11             : // of this software and associated documentation files (the "Software"), to deal
      12             : // in the Software without restriction, including without limitation the rights
      13             : // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      14             : // copies of the Software, and to permit persons to whom the Software is
      15             : // furnished to do so, subject to the following conditions:
      16             : //
      17             : // The above copyright notice and this permission notice shall be included in all
      18             : // copies or substantial portions of the Software.
      19             : //
      20             : // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      21             : // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      22             : // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      23             : // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      24             : // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      25             : // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
      26             : // SOFTWARE.
      27             : 
      28             : import 'package:cwtch/config.dart';
      29             : 
      30             : import 'linkify.dart';
      31             : 
      32           3 : final _urlRegex = RegExp(r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)', caseSensitive: false, dotAll: true);
      33             : 
      34           0 : final _looseUrlRegex = RegExp(r'^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,16}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*))', caseSensitive: false, dotAll: true);
      35             : 
      36             : class Formatter {
      37             :   final RegExp expression;
      38             :   final LinkifyElement Function(String) element;
      39             : 
      40           1 :   Formatter(this.expression, this.element);
      41             : }
      42             : 
      43             : // regex to match **bold**
      44           3 : final _boldRegex = RegExp(r'^(.*?)(\*\*([^*]+)\*\*)', caseSensitive: false, dotAll: true);
      45             : 
      46             : // regex to match *italic*
      47           3 : final _italicRegex = RegExp(r'^(.*?)(\*([^*]+)\*)', caseSensitive: false, dotAll: true);
      48             : 
      49             : // regex to match ^superscript^
      50           3 : final _superRegex = RegExp(r'^(.*?)(\^([^\^]*)\^)', caseSensitive: false, dotAll: true);
      51             : 
      52             : // regex to match ^subscript^
      53           0 : final _subRegex = RegExp(r'^(.*?)(\_([^\_]*)\_)', caseSensitive: false, dotAll: true);
      54             : 
      55             : // regex to match ~~strikethrough~~
      56           3 : final _strikeRegex = RegExp(r'^(.*?)(\~\~([^\~]*)\~\~)', caseSensitive: false, dotAll: true);
      57             : 
      58             : // regex to match `code`
      59           3 : final _codeRegex = RegExp(r'^(.*?)(\`([^\`]*)\`)', caseSensitive: false, dotAll: true);
      60             : 
      61             : class UrlLinkifier extends Linkifier {
      62           1 :   const UrlLinkifier();
      63             : 
      64           1 :   List<LinkifyElement> replaceAndParse(tle, TextElement element, RegExpMatch match, List<LinkifyElement> list, options) {
      65           3 :     final text = element.text.replaceFirst(match.group(0)!, '');
      66             : 
      67           3 :     if (match.group(1)?.isNotEmpty == true) {
      68           0 :       list.addAll(parse([TextElement(match.group(1)!)], options));
      69             :     }
      70             : 
      71           3 :     if (match.group(2)?.isNotEmpty == true) {
      72           3 :       list.add(tle(match.group(2)!));
      73             :     }
      74             : 
      75           1 :     if (text.isNotEmpty) {
      76           0 :       list.addAll(parse([TextElement(text)], options));
      77             :     }
      78             :     return list;
      79             :   }
      80             : 
      81           1 :   List<LinkifyElement> parseFormatting(element, options) {
      82           1 :     var list = <LinkifyElement>[];
      83             : 
      84             :     // code -> bold -> italic -> super -> sub -> strike
      85             :     // note we don't currently allow combinations of these elements the first
      86             :     // one to match a given set will be the only style applied - this will be fixed
      87             : 
      88             :     // Per #836, URLs should be lower priority than code. Since URL parsing logic
      89             :     // is more complicated than a simple regex and is implemented in the recursing
      90             :     // function, code regex is injected before the URL logic one level up.
      91             :     // To preserve behaviour when the clickable links experiment is disabled,
      92             :     // the code regex is preserved here for the time being.
      93             :     // Eventually this might all be replaced by a more advanced formatting parser.
      94           1 :     final formattingPrecedence = [
      95           2 :       Formatter(_codeRegex, CodeElement.new),
      96           2 :       Formatter(_boldRegex, BoldElement.new),
      97           2 :       Formatter(_italicRegex, ItalicElement.new),
      98           2 :       Formatter(_superRegex, SuperElement.new),
      99             :       // Formatter(_subRegex, SubElement.new),
     100           2 :       Formatter(_strikeRegex, StrikeElement.new),
     101             :     ];
     102             : 
     103             :     // Loop through the formatters in with precedence and break when something is found...
     104           2 :     for (var formatter in formattingPrecedence) {
     105           3 :       var formattingMatch = formatter.expression.firstMatch(element.text);
     106             :       if (formattingMatch != null) {
     107           2 :         list = replaceAndParse(formatter.element, element, formattingMatch, list, options);
     108             :         break;
     109             :       }
     110             :     }
     111             : 
     112             :     // catch all case where we didn't match anything and so need to return back
     113             :     // the unformatted text
     114             :     // conceptually this is Formatter((.*), TextElement.new)
     115           1 :     if (list.isEmpty) {
     116           1 :       list.add(element);
     117             :     }
     118             : 
     119             :     return list;
     120             :   }
     121             : 
     122           1 :   @override
     123             :   List<LinkifyElement> parse(elements, options) {
     124           1 :     var list = <LinkifyElement>[];
     125             : 
     126           2 :     elements.forEach((element) {
     127           1 :       if (element is TextElement) {
     128           4 :         if (options.parseLinks == false && options.messageFormatting == false) {
     129           1 :           list.add(element);
     130           2 :         } else if (options.parseLinks == true) {
     131             :           // Per #836, code block formatting needs to take precedence over URLs.
     132             :           // Only in this combination of conditionals is this additional logic required.
     133           3 :           var codeBlockMatch = _codeRegex.firstMatch(element.text);
     134           2 :           if (codeBlockMatch != null && options.messageFormatting == true) {
     135           3 :             final text = element.text.replaceFirst(codeBlockMatch.group(0)!, '');
     136           3 :             if (codeBlockMatch.group(1)?.isNotEmpty == true) {
     137           0 :               list.addAll(parse([TextElement(codeBlockMatch.group(1)!)], options));
     138             :             }
     139           3 :             list.add(CodeElement(codeBlockMatch.group(2)!));
     140           1 :             if (text.isNotEmpty) {
     141           0 :               list.addAll(parse([TextElement(text)], options));
     142             :             }
     143             :           } else {
     144             :             // check if there is a link...
     145           4 :             var match = options.looseUrl ? _looseUrlRegex.firstMatch(element.text) : _urlRegex.firstMatch(element.text);
     146             : 
     147             :             // if not then we only have to consider formatting...
     148             :             if (match == null) {
     149             :               // only do formatting if message formatting is enabled
     150           0 :               if (options.messageFormatting == false) {
     151           0 :                 list.add(element);
     152             :               } else {
     153             :                 // add all the formatting elements contained in this text
     154           0 :                 list.addAll(parseFormatting(element, options));
     155             :               }
     156             :             } else {
     157           3 :               final text = element.text.replaceFirst(match.group(0)!, '');
     158             : 
     159           3 :               if (match.group(1)?.isNotEmpty == true) {
     160             :                 // we match links first and the feed everything before the link
     161             :                 // back through the parser
     162           0 :                 list.addAll(parse([TextElement(match.group(1)!)], options));
     163             :               }
     164             : 
     165           3 :               if (match.group(2)?.isNotEmpty == true) {
     166           1 :                 var originalUrl = match.group(2)!;
     167             :                 String? end;
     168             : 
     169           5 :                 if ((options.excludeLastPeriod) && originalUrl[originalUrl.length - 1] == ".") {
     170             :                   end = ".";
     171           0 :                   originalUrl = originalUrl.substring(0, originalUrl.length - 1);
     172             :                 }
     173             : 
     174             :                 var url = originalUrl;
     175             : 
     176             :                 // If protocol has not been specified then append a protocol
     177             :                 // to the start of the URL so that it can be opened...
     178           1 :                 if (!url.startsWith("https://") && !url.startsWith("http://")) {
     179           0 :                   url = "https://" + url;
     180             :                 }
     181             : 
     182           2 :                 list.add(UrlElement(url, originalUrl));
     183             : 
     184             :                 if (end != null) {
     185           0 :                   list.add(TextElement(end));
     186             :                 }
     187             :               }
     188             : 
     189           1 :               if (text.isNotEmpty) {
     190           0 :                 list.addAll(parse([TextElement(text)], options));
     191             :               }
     192             :             }
     193             :           }
     194           2 :         } else if (options.messageFormatting == true) {
     195             :           // we can jump straight to message formatting...
     196           2 :           list.addAll(parseFormatting(element, options));
     197             :         } else {
     198             :           // unreachable - if we get here then there is something wrong in the above logic since every combination of
     199             :           // formatting options should have already been accounted for.
     200           0 :           EnvironmentConfig.debugLog("'unreachable' code path in formatting has been triggered. this is very likely a bug - please report $options");
     201             :         }
     202             :       }
     203             :     });
     204             : 
     205             :     return list;
     206             :   }
     207             : }
     208             : 
     209             : /// Represents an element containing a link
     210             : class UrlElement extends LinkableElement {
     211           2 :   UrlElement(String url, [String? text]) : super(text, url);
     212             : 
     213           0 :   @override
     214             :   String toString() {
     215           0 :     return "LinkElement: '$url' ($text)";
     216             :   }
     217             : 
     218           0 :   @override
     219           0 :   bool operator ==(other) => equals(other);
     220             : 
     221           0 :   @override
     222           0 :   bool equals(other) => other is UrlElement && super.equals(other);
     223             : }

Generated by: LCOV version 1.14