Cretezy / flutter_linkify

Turns text URLs and emails into clickable inline links in text for Flutter
https://pub.dartlang.org/packages/flutter_linkify
MIT License
262 stars 99 forks source link

#Hashtag support #90

Closed mohammedbabelly closed 2 years ago

mohammedbabelly commented 2 years ago

Could you please add the hashtag support, so it colors the hashtag like emails or allows us to use widgets in the text property so we use other packages like Hatagable

VictorUvarov commented 2 years ago

You can create your own custom Linkifier and use the linkifiers property on the Linkify widget.

class HashtagLinkifier extends Linkifier { ... }

class HashtagElement extends LinkableElement { ... }
Linkify(
  linkifiers: const [
    ...defaultLinkifiers,
    HashtagLinkifier(),
  ],
  ...
),
francis-legaspi commented 2 years ago

@VictorUvarov I hope this is not too much, but can you give me snippets on how you dot it? need to support hashtags, email and urls for my text description, Thank you very much!

VictorUvarov commented 2 years ago

HashtagLinkifier and HashtagElement. Just update the regex to also support @.

import 'package:linkify/linkify.dart';

final _atUsernameRegex = RegExp(
  r'(.*?)(#\w+)',
  caseSensitive: false,
  dotAll: true,
);

class HashtagLinkifier extends Linkifier {
  const HashtagLinkifier();

  @override
  List<LinkifyElement> parse(elements, options) {
    final list = <LinkifyElement>[];

    for (var element in elements) {
      if (element is TextElement) {
        var match = _atUsernameRegex.firstMatch(element.text);

        if (match == null) {
          list.add(element);
        } else {
          // create the preceding TextElement
          if (match.group(1)?.isNotEmpty ?? false) {
            list.add(TextElement(match.group(1)!));
          }

          // create the AtElement
          if (match.group(2)?.isNotEmpty ?? false) {
            var withHash = match.group(2)!;
            var element = HashtagElement(withHash);
            list.add(element);
          }

          // create the following TextElement
          final textWithoutMatch =
              element.text.replaceFirst(match.group(0)!, '');
          if (textWithoutMatch.isNotEmpty) {
            list.addAll(parse([TextElement(textWithoutMatch)], options));
          }
        }
      } else {
        list.add(element);
      }
    }

    return list;
  }
}

class HashtagElement extends LinkableElement {
  HashtagElement(String tag) : super(tag, tag);

  @override
  String toString() {
    return "HashtagElement: '$url' ($text)";
  }

  @override
  bool operator ==(other) => equals(other);

  @override
  bool equals(other) =>
      identical(this, other) ||
      other is HashtagElement &&
          runtimeType == other.runtimeType &&
          text == other.text &&
          url == other.url;

  @override
  int get hashCode => text.hashCode ^ url.hashCode;
}

Tests

group('HashtagLinkifier', () {
    test('Parses only hashtag', () {
      expectListEqual(
        linkify('#trending', linkifiers: [const HashtagLinkifier()]),
        [HashtagElement('#trending')],
      );
    });

    test('Parses hashtags with text', () {
      expectListEqual(
        linkify(
          'Lorem ipsum dolor sit amet #tag #stuff',
          linkifiers: [const HashtagLinkifier()],
        ),
        [
          TextElement('Lorem ipsum dolor sit amet '),
          HashtagElement('#tag'),
          TextElement(' '),
          HashtagElement('#stuff'),
        ],
      );

      expectListEqual(
        linkify(
          'Lorem ipsum dolor sit amet #tag #stuff some more text',
          linkifiers: [const HashtagLinkifier()],
        ),
        [
          TextElement('Lorem ipsum dolor sit amet '),
          HashtagElement('#tag'),
          TextElement(' '),
          HashtagElement('#stuff'),
          TextElement(' some more text'),
        ],
      );
    });
  });
francis-legaspi commented 2 years ago

HashtagLinkifier and HashtagElement. Just update the regex to also support @.

import 'package:linkify/linkify.dart';

final _atUsernameRegex = RegExp(
  r'(.*?)(#\w+)',
  caseSensitive: false,
  dotAll: true,
);

class HashtagLinkifier extends Linkifier {
  const HashtagLinkifier();

  @override
  List<LinkifyElement> parse(elements, options) {
    final list = <LinkifyElement>[];

    for (var element in elements) {
      if (element is TextElement) {
        var match = _atUsernameRegex.firstMatch(element.text);

        if (match == null) {
          list.add(element);
        } else {
          // create the preceding TextElement
          if (match.group(1)?.isNotEmpty ?? false) {
            list.add(TextElement(match.group(1)!));
          }

          // create the AtElement
          if (match.group(2)?.isNotEmpty ?? false) {
            var withHash = match.group(2)!;
            var element = HashtagElement(withHash);
            list.add(element);
          }

          // create the following TextElement
          final textWithoutMatch =
              element.text.replaceFirst(match.group(0)!, '');
          if (textWithoutMatch.isNotEmpty) {
            list.addAll(parse([TextElement(textWithoutMatch)], options));
          }
        }
      } else {
        list.add(element);
      }
    }

    return list;
  }
}

class HashtagElement extends LinkableElement {
  HashtagElement(String tag) : super(tag, tag);

  @override
  String toString() {
    return "HashtagElement: '$url' ($text)";
  }

  @override
  bool operator ==(other) => equals(other);

  @override
  bool equals(other) =>
      identical(this, other) ||
      other is HashtagElement &&
          runtimeType == other.runtimeType &&
          text == other.text &&
          url == other.url;

  @override
  int get hashCode => text.hashCode ^ url.hashCode;
}

Tests

group('HashtagLinkifier', () {
    test('Parses only hashtag', () {
      expectListEqual(
        linkify('#trending', linkifiers: [const HashtagLinkifier()]),
        [HashtagElement('#trending')],
      );
    });

    test('Parses hashtags with text', () {
      expectListEqual(
        linkify(
          'Lorem ipsum dolor sit amet #tag #stuff',
          linkifiers: [const HashtagLinkifier()],
        ),
        [
          TextElement('Lorem ipsum dolor sit amet '),
          HashtagElement('#tag'),
          TextElement(' '),
          HashtagElement('#stuff'),
        ],
      );

      expectListEqual(
        linkify(
          'Lorem ipsum dolor sit amet #tag #stuff some more text',
          linkifiers: [const HashtagLinkifier()],
        ),
        [
          TextElement('Lorem ipsum dolor sit amet '),
          HashtagElement('#tag'),
          TextElement(' '),
          HashtagElement('#stuff'),
          TextElement(' some more text'),
        ],
      );
    });
  });

Thank You @VictorUvarov I appreciate your fast response!

petchgabriel commented 2 years ago

HashtagLinkifier and HashtagElement. Just update the regex to also support @.

import 'package:linkify/linkify.dart';

final _atUsernameRegex = RegExp(
  r'(.*?)(#\w+)',
  caseSensitive: false,
  dotAll: true,
);

class HashtagLinkifier extends Linkifier {
  const HashtagLinkifier();

  @override
  List<LinkifyElement> parse(elements, options) {
    final list = <LinkifyElement>[];

    for (var element in elements) {
      if (element is TextElement) {
        var match = _atUsernameRegex.firstMatch(element.text);

        if (match == null) {
          list.add(element);
        } else {
          // create the preceding TextElement
          if (match.group(1)?.isNotEmpty ?? false) {
            list.add(TextElement(match.group(1)!));
          }

          // create the AtElement
          if (match.group(2)?.isNotEmpty ?? false) {
            var withHash = match.group(2)!;
            var element = HashtagElement(withHash);
            list.add(element);
          }

          // create the following TextElement
          final textWithoutMatch =
              element.text.replaceFirst(match.group(0)!, '');
          if (textWithoutMatch.isNotEmpty) {
            list.addAll(parse([TextElement(textWithoutMatch)], options));
          }
        }
      } else {
        list.add(element);
      }
    }

    return list;
  }
}

class HashtagElement extends LinkableElement {
  HashtagElement(String tag) : super(tag, tag);

  @override
  String toString() {
    return "HashtagElement: '$url' ($text)";
  }

  @override
  bool operator ==(other) => equals(other);

  @override
  bool equals(other) =>
      identical(this, other) ||
      other is HashtagElement &&
          runtimeType == other.runtimeType &&
          text == other.text &&
          url == other.url;

  @override
  int get hashCode => text.hashCode ^ url.hashCode;
}

Tests

group('HashtagLinkifier', () {
    test('Parses only hashtag', () {
      expectListEqual(
        linkify('#trending', linkifiers: [const HashtagLinkifier()]),
        [HashtagElement('#trending')],
      );
    });

    test('Parses hashtags with text', () {
      expectListEqual(
        linkify(
          'Lorem ipsum dolor sit amet #tag #stuff',
          linkifiers: [const HashtagLinkifier()],
        ),
        [
          TextElement('Lorem ipsum dolor sit amet '),
          HashtagElement('#tag'),
          TextElement(' '),
          HashtagElement('#stuff'),
        ],
      );

      expectListEqual(
        linkify(
          'Lorem ipsum dolor sit amet #tag #stuff some more text',
          linkifiers: [const HashtagLinkifier()],
        ),
        [
          TextElement('Lorem ipsum dolor sit amet '),
          HashtagElement('#tag'),
          TextElement(' '),
          HashtagElement('#stuff'),
          TextElement(' some more text'),
        ],
      );
    });
  });

Could you help me on RegExp for supporting Thai language? Thank you.

Update I have change regexp to this RegExp(r'(.*?)(#[\u0E00-\u0E7F\a-zA-Za-zA-Z]+)',caseSensitive: false, dotAll: true, );

It works fine. Thank you.