diff --git a/grapheme.go b/grapheme.go index 1c17c27..6a6e8a9 100644 --- a/grapheme.go +++ b/grapheme.go @@ -103,6 +103,11 @@ func (g *Graphemes) Bytes() []byte { return []byte(g.cluster) } +// IsEmoji returns true if the current grapheme cluster is an emoji. +func (g *Graphemes) IsEmoji() bool { + return IsGraphemeClusterEmoji([]byte(g.cluster), g.Width()) +} + // Positions returns the interval of the current grapheme cluster as byte // positions into the original string. The first returned value "from" indexes // the first byte and the second returned value "to" indexes the first byte that @@ -343,3 +348,63 @@ func FirstGraphemeClusterInString(str string, state int) (cluster, rest string, } } } + +const ( + regionalIndicatorA = 0x1F1E6 + regionalIndicatorZ = 0x1F1FF +) + +// IsGraphemeClusterEmoji returns true if the given byte slice grapheme cluster +// and width is an emoji according to the Unicode Standard Annex #51, Unicode +// Emoji. +func IsGraphemeClusterEmoji(cluster []byte, width int) bool { + if width != 2 { + return false + } + + for i := 0; len(cluster) > 0; i++ { + r, rw := utf8.DecodeRune(cluster) + if i == 0 { + if r >= regionalIndicatorA && r <= regionalIndicatorZ { + return true + } + if propertyGraphemes(r) == prExtendedPictographic && + property(emojiPresentation, r) == prEmojiPresentation { + return true + } + } + if r == vs16 { + return true + } + cluster = cluster[rw:] + } + + return false +} + +// IsGraphemeClusterInStringEmoji is like [IsGraphemeClusterEmoji] but its input +// is a string. +func IsGraphemeClusterInStringEmoji(cluster string, width int) bool { + if width != 2 { + return false + } + + for i := 0; len(cluster) > 0; i++ { + r, rw := utf8.DecodeRuneInString(cluster) + if i == 0 { + if r >= regionalIndicatorA && r <= regionalIndicatorZ { + return true + } + if propertyGraphemes(r) == prExtendedPictographic && + property(emojiPresentation, r) == prEmojiPresentation { + return true + } + } + if r == vs16 { + return true + } + cluster = cluster[rw:] + } + + return false +}