LinkTextSpans function

TextSpan LinkTextSpans({
  1. required String text,
  2. TextStyle? textStyle,
  3. TextStyle? linkStyle,
  4. LinkTapHandler? onLinkTap,
  5. ThemeData? themeData,
})

Implementation

TextSpan LinkTextSpans(
    {required String text,
    TextStyle? textStyle,
    TextStyle? linkStyle,
    LinkTapHandler? onLinkTap,
    ThemeData? themeData}) {
  Future<void> launchUrlIfHandler(Uri url) async {
    if (onLinkTap != null) {
      onLinkTap(url);
      return;
    }

    if (await canLaunchUrl(url)) {
      await launchUrl(url);
    } else {
      throw 'Could not launch $url';
    }
  }

  textStyle ??= themeData?.textTheme.bodyMedium;
  linkStyle ??= themeData?.textTheme.bodyMedium?.copyWith(
    color: themeData.colorScheme.secondary,
    decoration: TextDecoration.underline,
  );

  // first estimate if we are going to have matches at all
  final estimateMatches = _estimateRegex.allMatches(text);
  if (estimateMatches.isEmpty) {
    return TextSpan(
      text: text,
      style: textStyle,
      children: const [],
    );
  }

  // Our _regex uses lookbehinds for nicer matching, which isn't supported by all browsers yet.
  // Sadly, an error is only thrown on usage. So, we try to match against an empty string to get
  // our error ASAP and then determine the regex we use based on that.
  RegExp regexToUse;
  try {
    _regex.hasMatch('');
    regexToUse = _regex;
  } catch (_) {
    regexToUse = _fallbackRegex;
  }

  List<RegExpMatch>? links;
  List<String>? textParts;
  if (text.length > 300) {
    // we have a super long text, let's try to split it up
    links = [];
    // thing greatly simplify if the textParts.last is already a string
    textParts = [''];
    // now we will separate the `text` into chunks around their matches, and then apply the regex
    // only to those substrings.
    // As we already estimated some matches, we know the for-loop will run at least once, simplifying things
    // we will need to make sure to merge overlapping chunks together
    var curStart = -1; // the current chunk start
    var curEnd = 0; // the current chunk end
    var lastEnd = 0; // the last chunk end, where we stopped parsing
    var abort = false; // should we abort and fall back to the slow method?
    void processChunk() {
      if (textParts == null || links == null) {
        abort = true;
        links = null;
        textParts = null;
        return;
      }
      // we gotta make sure to save the text fragment between the current and the last chunk
      final firstFragment = text.substring(lastEnd, curStart);
      if (firstFragment.isNotEmpty) {
        textParts!.last += firstFragment;
      }
      // fetch our current fragment...
      final fragment = text.substring(curStart, curEnd);
      // add all the links
      links!.addAll(regexToUse.allMatches(fragment));

      // and fetch the text parts
      final fragmentTextParts = fragment.split(regexToUse);
      // if the first of last text part is empty, that means that the chunk wasn't big enough to fit the full URI
      // thus we abort and fall back to the slow method
      if ((fragmentTextParts.first.isEmpty && curStart > 0) ||
          (fragmentTextParts.last.isEmpty && curEnd < text.length)) {
        abort = true;
        links = null;
        textParts = null;
        return;
      }
      // add all the text parts correctly
      textParts!.last += fragmentTextParts.removeAt(0);
      textParts!.addAll(fragmentTextParts);
      // and save the lastEnd for later
      lastEnd = curEnd;
    }
    for (final e in estimateMatches) {
      const int kChunkSize = 120;
      final start = max(e.start - kChunkSize, 0);
      final end = min(e.start + kChunkSize, text.length);
      if (start < curEnd) {
        // merge blocks
        curEnd = end;
      } else {
        // new block! And proccess the last chunk!
        if (curStart != -1) {
          processChunk();
        }
        curStart = start;
        curEnd = end;
      }
      if (abort) {
        break;
      }
    }
    // we musn't forget to proccess the last chunk
    if (!abort) {
      processChunk();
    }
    if (!abort) {
      // and we musn't forget to add the last fragment
      final lastFragment = text.substring(lastEnd, text.length);
      if (lastFragment.isNotEmpty && textParts != null) {
        textParts!.last += lastFragment;
      }
    }
  }
  links ??= regexToUse.allMatches(text).toList();
  if (links!.isEmpty) {
    return TextSpan(
      text: text,
      style: textStyle,
      children: const [],
    );
  }

  textParts ??= text.split(regexToUse);
  final textSpans = <InlineSpan>[];

  int i = 0;
  for (var part in textParts!) {
    textSpans.add(TextSpan(text: part, style: textStyle));

    if (i < links!.length) {
      final element = links![i];
      final linkText = element.group(0) ?? '';
      var link = linkText;
      final scheme = element.group(1);
      final tldUrl = element.group(2);
      final tldEmail = element.group(3);
      var valid = true;
      if (scheme?.isNotEmpty ?? false) {
        // we have to validate the scheme
        valid = kAllSchemes.contains(scheme!.toLowerCase());
      }
      if (valid && (tldUrl?.isNotEmpty ?? false)) {
        // we have to validate if the tld exists
        valid = kAllTlds.contains(tldUrl!.toLowerCase());
        link = 'https://$link';
      }
      if (valid && (tldEmail?.isNotEmpty ?? false)) {
        // we have to validate if the tld exists
        valid = kAllTlds.contains(tldEmail!.toLowerCase());
        link = 'mailto:$link';
      }
      final uri = Uri.tryParse(link);
      if (valid && uri != null) {
        if (kIsWeb) {
          // on web recognizer in TextSpan does not work properly, so we use normal text w/ inkwell
          textSpans.add(
            WidgetSpan(
              child: InkWell(
                onTap: () => launchUrlIfHandler(uri),
                child: Text(linkText, style: linkStyle),
              ),
            ),
          );
        } else {
          textSpans.add(
            LinkTextSpan(
              text: linkText,
              style: linkStyle,
              url: uri,
              onLinkTap: launchUrlIfHandler,
            ),
          );
        }
      } else {
        textSpans.add(TextSpan(text: linkText, style: textStyle));
      }

      i++;
    }
  }
  return TextSpan(text: '', children: textSpans);
}