# emoji

> Find, count, extract and strip emoji in text, with flags, skin tones and joined sequences each counted as one.

This is the Markdown version of https://osl-lang.org/packages/emoji. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:emoji"
```

Emoji are trickier than they look. A flag is two code points, a thumbs up with a skin tone is two, and a family is five (three people joined by two invisible joiners). Counting them with `.len` or splitting a string by character gets this wrong. `std:emoji` knows the rules: each of those is one emoji.

## Detecting and counting

```osl
import "std:emoji"

string message = "Great work 👍🏽 see you in 🇬🇧"
log emoji.hasEmoji(message)
log emoji.hasEmoji("no emoji here")
log emoji.count(message)
```

Output:

```text
true
false
2
```

The text's `.len` counts every code point that went into the emoji:

```osl
import "std:emoji"

string family = "👨‍👩‍👧"
log emoji.count(family)
log family.len
```

Output:

```text
1
5
```

## Extracting

`extract` returns each emoji in order, whole:

```osl
import "std:emoji"

string post = "Hi 👋🏽 from 🇬🇧 and 👨‍👩‍👧, keycap 1️⃣ and 🙂"
log emoji.extract(post)
log emoji.extract("none")
```

Output:

```text
["👋🏽","🇬🇧","👨‍👩‍👧","1️⃣","🙂"]
[]
```

Counting how often each one appears is then a few lines of [map](/packages/map.md):

```osl
import "std:emoji"

string chat = "👍 👍 🎉 👍 🎉 😀"
string[int] tally = {}
for e in emoji.extract(chat) (
  tally[e] = (tally[e] ?? 0) + 1
)
for e in tally.getKeys().toSorted() (
  log e tally[e]
)
```

Output:

```text
🎉 2
👍 3
😀 1
```

## Removing and replacing

`remove` deletes every emoji. `replace` swaps each one for text:

```osl
import "std:emoji"

string text = "Launch day 🚀 !"
log emoji.remove(text)
log emoji.replace(text, "[emoji]")
log emoji.replace("a🙂b🙃c", "")
```

Output:

```text
Launch day  !
Launch day [emoji] !
abc
```

Removing leaves the spaces around the emoji, so tidy up afterwards with `.trim()`.

## Asking what a string is

```osl
import "std:emoji"

log emoji.isFlag("🇬🇧") emoji.isFlag("🙂")
log emoji.isOnlyEmoji("🙂🙂") emoji.isOnlyEmoji("a🙂")
log emoji.isZWJSequence("👨‍👩‍👧") emoji.isZWJSequence("🙂")
```

Output:

```text
true false
true false
true false
```

A ZWJ sequence is several emoji joined by a zero-width joiner into one picture, such as a family or a profession.

## Skin tones

`hasSkinTone` tells whether a skin-tone modifier is present, and `findSkinTone` names the first one: `"light"`, `"medium-light"`, `"medium"`, `"medium-dark"` or `"dark"`, or `null` when there is none:

```osl
import "std:emoji"

log emoji.hasSkinTone("👋🏽")
log emoji.findSkinTone("👋🏽")
log emoji.findSkinTone("👋") ?? "none"
```

Output:

```text
true
medium
none
```

## Validating a reaction

A reaction field should hold exactly one emoji and nothing else. `isValidReaction` checks that:

```osl
import "std:emoji"

string[] tries = ["👍", "👍👍", "a", "🇬🇧", "👨‍👩‍👧"]
for t in tries (
  log t emoji.isValidReaction(t)
)
```

Output:

```text
👍 true
👍👍 false
a false
🇬🇧 true
👨‍👩‍👧 true
```

**Quiz.** What does `emoji.count("🇬🇧")` return?

- [ ] `2`, because a flag is two code points.
- [x] `1`, because the flag is a single emoji.
- [ ] `0`, because flags aren't emoji.

Answer: `std:emoji` counts what a reader sees. `"🇬🇧".len` would be 2.

### Exercise: Clean a status

Finish `clean` so it removes every emoji from a status and trims the spaces they leave behind.

Starting code:

```osl
import "std:emoji"

def clean(string text) string -> text

log clean("Launch 🚀")
log clean("🎉 Party 🎉")
log clean("plain")
```

It should print:

```text
Launch
Party
plain
```

Hint 1: `emoji.remove(text)` deletes the emoji but leaves the spaces.

Hint 2: Chain `.trim()` onto the result.

A solution:

```osl
import "std:emoji"

def clean(string text) string -> emoji.remove(text).trim()

log clean("Launch 🚀")
log clean("🎉 Party 🎉")
log clean("plain")
```

## API reference

## API reference

Every member of `std:emoji`, generated from the source of OSL.

### Functions

#### `emoji.count(string text) int`

count returns how many emoji text contains. A flag, keycap, toned emoji or
ZWJ sequence such as a family counts as one.

#### `emoji.extract(string text) string[]`

extract returns every emoji in text in order, each flag, keycap, toned
emoji or ZWJ sequence as one string; [] when there are none.

#### `emoji.findSkinTone(string text) string?`

findSkinTone returns the name of the first skin tone in text: "light",
"medium-light", "medium", "medium-dark" or "dark", or null when text has
no skin-tone modifier.

#### `emoji.hasEmoji(string text) boolean`

hasEmoji reports whether text contains at least one emoji.

#### `emoji.hasSkinTone(string text) boolean`

hasSkinTone reports whether text contains a skin-tone modifier.

#### `emoji.isEmoji(any text) boolean`

**Deprecated.** Deprecated: use emoji.hasEmoji(text) instead.

isEmoji reports whether text contains at least one emoji.

#### `emoji.isFlag(string text) boolean`

isFlag reports whether text is exactly one flag: a pair of regional
indicator symbols (the pair is two code points, eight bytes) or a tag
sequence flag such as England's.

#### `emoji.isOnlyEmoji(string text) boolean`

isOnlyEmoji reports whether text is one or more emoji with nothing else,
not even spaces. An empty string is not.

#### `emoji.isValidReaction(string text) boolean`

isValidReaction reports whether text can be used as a reaction: exactly
one emoji, or an originChats custom reaction "originChats:<emoji>//"
wrapping exactly one emoji.

#### `emoji.isZWJSequence(string text) boolean`

isZWJSequence reports whether text contains a zero-width joiner (U+200D),
which joins emoji such as a family or a profession into one.

#### `emoji.onlyEmoji(any text) boolean`

**Deprecated.** Deprecated: use emoji.isOnlyEmoji(text) instead.

onlyEmoji reports whether text is one or more emoji and nothing else.

#### `emoji.remove(string text) string`

remove returns text with every emoji removed.

#### `emoji.replace(string text, string replacement) string`

replace returns text with each emoji replaced by replacement, taken
literally: "$1" or "${x}" in it is not expanded.

#### `emoji.skinTone(any text) string`

**Deprecated.** Deprecated: use emoji.findSkinTone(text) instead, which returns null when there is no skin tone.

skinTone returns the name of the first skin tone in text, or "none".
