/**", "**/node_modules/**"],
11 | "smartStep": true,
12 | "type": "node"
13 | }
14 | ],
15 | "version": "0.2.0"
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.codeActionsOnSave": { "source.fixAll.eslint": true },
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "editor.formatOnSave": true,
5 | "editor.rulers": [80],
6 | "eslint.probe": [
7 | "javascript",
8 | "javascriptreact",
9 | "json",
10 | "jsonc",
11 | "markdown",
12 | "typescript",
13 | "typescriptreact",
14 | "yaml"
15 | ],
16 | "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
17 | "typescript.tsdk": "node_modules/typescript/lib"
18 | }
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [2.2.0](https://github.com/omnidan/node-emoji/compare/v2.1.3...v2.2.0) (2024-12-03)
2 |
3 | ### Features
4 |
5 | - allow RegExp objects to be passed to search that are evaluated for each emoji ([7046cbb](https://github.com/omnidan/node-emoji/commit/7046cbbdda4282b9503ea10c7ac632f39bba74bd))
6 |
7 | ## [2.1.3](https://github.com/omnidan/node-emoji/compare/v2.1.2...v2.1.3) (2023-11-20)
8 |
9 | ### Bug Fixes
10 |
11 | - pin skin-tone to ^2.0.0 and add renovate.json ignoreDeps ([dcd6c43](https://github.com/omnidan/node-emoji/commit/dcd6c43f26b6537fb770b5309574c3e1ff3859c1))
12 |
13 | ## [2.1.2](https://github.com/omnidan/node-emoji/compare/v2.1.1...v2.1.2) (2023-11-20)
14 |
15 | ### Bug Fixes
16 |
17 | - add back CJS support, with a test ([e2856d9](https://github.com/omnidan/node-emoji/commit/e2856d980d1e9335d5e62c7de25c1d22e6956584))
18 |
19 | ## [2.1.1](https://github.com/omnidan/node-emoji/compare/v2.1.0...v2.1.1) (2023-11-18)
20 |
21 | ### Bug Fixes
22 |
23 | - underlines in README.md badges ([05e30ae](https://github.com/omnidan/node-emoji/commit/05e30aed2d9b6fdf2af7aba7ac44d396e3797869))
24 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2014-2023 Daniel Bugl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | node-emoji
2 |
3 | Friendly emoji lookups and parsing utilities for Node.js. 💖
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | `node-emoji` provides a fun, straightforward interface on top of the following excellent libraries:
22 |
23 | - [`emojilib`](https://npmjs.org/package/emojilib): provides a list of emojis and keyword search on top of it
24 | - [`skin-tone`](https://npmjs.org/package/skin-tone): parses out base emojis from skin tones
25 |
26 | ## Install
27 |
28 | ```sh
29 | npm install node-emoji
30 | ```
31 |
32 | ### 2.0 Release 🚀
33 |
34 | This is the new 2.0 release of node-emoji, supporting ESM, new emoji and a new API.
35 |
36 | If you want to use the old version, please check out the [legacy branch](https://github.com/omnidan/node-emoji/tree/legacy).
37 |
38 | ## Usage
39 |
40 | ```js
41 | import * as emoji from 'node-emoji'
42 |
43 | emoji.emojify('I :heart: :coffee:!') // 'I ❤️ ☕️!'
44 |
45 | emoji.find('heart') // { emoji: '❤', name: 'heart' }
46 | emoji.find('❤️') // { emoji: '❤', name: 'heart' }
47 |
48 | emoji.get('unicorn') // 🦄
49 | emoji.get(':unicorn:') // 🦄
50 |
51 | emoji.has(':pizza:') // true
52 | emoji.has('🍕') // true
53 | emoji.has('unknown') // false
54 |
55 | emoji.random() // { name: 'house', emoji: '🏠' }
56 |
57 | emoji.replace('I ❤️ coffee!', 'love', { preserveSpaces: true }) // 'I love coffee!'
58 |
59 | emoji.search(':uni:') // [ { emoji: '🦄', name: 'unicorn' }, ... ]
60 |
61 | emoji.strip('I ❤️ coffee!') // 'I coffee!'
62 |
63 | emoji.unemojify('🍕 for 💃') // ':pizza: for :dancer:'
64 |
65 | emoji.which('🦄') // 'unicorn'
66 | ```
67 |
68 | ## API
69 |
70 | ### emoji.emojify(input, options?)
71 |
72 | Parse all markdown-encoded emojis in a string.
73 |
74 | Parameters:
75 |
76 | 1. **`input`** (`string`): The input string containing the markdown-encoding emojis.
77 | 1. **`options`** _(optional)_:
78 | - **`fallback`** (`string`; default: `""`): The string to fallback to if an emoji was not found.
79 | - **`format`** (`() => (emoji: string, part: string, string: string) => string`; default: `value => value`): Add a middleware layer to modify each matched emoji after parsing.
80 |
81 | ```js
82 | import * as emoji from 'node-emoji'
83 |
84 | console.log(emoji.emojify('The :unicorn: is a fictitious animal.'))
85 | // 'The 🦄 is a fictitious animal.'
86 | ```
87 |
88 | ### emoji.find(emoji)
89 |
90 | Get the name and character of an emoji.
91 |
92 | Parameters:
93 |
94 | 1. **`emoji`** (`string`): The emoji to get the data of.
95 |
96 | ```js
97 | import * as emoji from 'node-emoji'
98 |
99 | console.log(emoji.find('🦄'))
100 | // { name: 'unicorn', emoji: '🦄' }
101 | ```
102 |
103 | ### emoji.get(name)
104 |
105 | Get an emoji from an emoji name.
106 |
107 | Parameters:
108 |
109 | 1. **`name`** (`string`): The name of the emoji to get.
110 |
111 | ```js
112 | import * as emoji from 'node-emoji'
113 |
114 | console.log(emoji.get('unicorn'))
115 | // '🦄'
116 | ```
117 |
118 | ### emoji.has(emoji)
119 |
120 | Check if this library supports a specific emoji.
121 |
122 | Parameters:
123 |
124 | 1. **`emoji`** (`string`): The emoji to check.
125 |
126 | ```js
127 | import * as emoji from 'node-emoji'
128 |
129 | console.log(emoji.has('🦄'))
130 | // true
131 | ```
132 |
133 | ### emoji.random()
134 |
135 | Get a random emoji.
136 |
137 | ```js
138 | import * as emoji from 'node-emoji'
139 |
140 | console.log(emoji.random())
141 | // { name: 'unicorn', emoji: '🦄' }
142 | ```
143 |
144 | ### emoji.replace(input, replacement)
145 |
146 | Replace the emojis in a string.
147 |
148 | Parameters:
149 |
150 | - **`input`** (`string`): The input string.
151 | - **`replacement`** (`string | (emoji: string, index: number, string: string) => string`): The character to replace the emoji with.
152 | Can be either a string or a callback that returns a string.
153 |
154 | ```js
155 | import * as emoji from 'node-emoji'
156 |
157 | console.log(emoji.replace('The 🦄 is a fictitious animal.', 'unicorn'))
158 | // 'The unicorn is a fictitious animal.'
159 | ```
160 |
161 | ### emoji.search(keyword)
162 |
163 | Search for emojis containing the provided name in their name.
164 |
165 | Parameters:
166 |
167 | 1. **`keyword`** (`string`): The keyword to search for.
168 |
169 | ```js
170 | import * as emoji from 'node-emoji'
171 |
172 | console.log(emoji.search('honey'))
173 | // [ { name: 'honeybee', emoji: '🐝' }, { name: 'honey_pot', emoji: '🍯' } ]
174 | ```
175 |
176 | ### emoji.strip(input, options?)
177 |
178 | Remove all of the emojis from a string.
179 |
180 | Parameters:
181 |
182 | 1. **`input`** (`string`): The input string to strip the emojis from.
183 | 1. **`options`** _(optional)_:
184 |
185 | - **`preserveSpaces`** (`boolean`): Whether to keep the extra space after a stripped emoji.
186 |
187 | ```js
188 | import * as emoji from 'node-emoji'
189 |
190 | console.log(emoji.strip('🦄 The unicorn is a fictitious animal.'))
191 | // 'The unicorn is a fictitious animal.'
192 |
193 | console.log(
194 | emoji.strip('🦄 The unicorn is a fictitious animal.', {
195 | preserveSpaces: true,
196 | }),
197 | )
198 | // ' The unicorn is a fictitious animal.'
199 | ```
200 |
201 | ### emoji.unemojify(input)
202 |
203 | Convert all emojis in a string to their markdown-encoded counterparts.
204 |
205 | Parameters:
206 |
207 | 1. **`input`** (`string`): The input string containing the emojis.
208 |
209 | ```js
210 | import * as emoji from 'node-emoji'
211 |
212 | console.log(emoji.unemojify('The 🦄 is a fictitious animal.'))
213 | // 'The :unicorn: is a fictitious animal.'
214 | ```
215 |
216 | ### emoji.which(emoji, options?)
217 |
218 | Get an emoji name from an emoji.
219 |
220 | Parameters:
221 |
222 | 1. **`emoji`** (`string`): The emoji to get the name of.
223 | 1. **`options`** _(optional)_:
224 | - **`markdown`** (`boolean`; default: `false`): Whether to return a `":emoji:"` string instead of `"emoji"`
225 |
226 | ```js
227 | import * as emoji from 'node-emoji'
228 |
229 | console.log(emoji.which('🦄'))
230 | // 'unicorn'
231 | ```
232 |
233 | ## Development
234 |
235 | See _[`.github/Development.md`](./github/Development.md)_.
236 |
237 | ## License
238 |
239 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fomnidan%2Fnode-emoji?ref=badge_large)
240 |
241 | ### Special Thanks
242 |
243 | ...to Anand Chowdhary (@AnandChowdhary) and his company [Pabio](https://github.com/pabio) for sponsoring this project via [GitHub Sponsors](https://github.com/sponsors/omnidan)!
244 |
245 | ## Contributors
246 |
247 |
248 |
249 |
250 |
251 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
--------------------------------------------------------------------------------
/cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "dictionaries": ["typescript"],
3 | "ignorePaths": [
4 | ".github",
5 | "CHANGELOG.md",
6 | "coverage",
7 | "lib",
8 | "node_modules",
9 | "pnpm-lock.yaml"
10 | ],
11 | "words": [
12 | "Anand",
13 | "Bugl",
14 | "Chowdhary",
15 | "Codecov",
16 | "codespace",
17 | "commitlint",
18 | "contributorsrc",
19 | "conventionalcommits",
20 | "couplekiss",
21 | "emojify",
22 | "emojilib",
23 | "knip",
24 | "lcov",
25 | "markdownlintignore",
26 | "npmpackagejsonlintrc",
27 | "omnidan",
28 | "outro",
29 | "Pabio",
30 | "packagejson",
31 | "quickstart",
32 | "sindresorhus",
33 | "smilies",
34 | "tsup",
35 | "unemojify",
36 | "wontfix"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/knip.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/knip@latest/schema.json",
3 | "entry": ["src/index.ts!"],
4 | "ignoreExportsUsedInFile": { "interface": true, "type": true },
5 | "project": ["src/**/*.ts!"]
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-emoji",
3 | "version": "2.2.0",
4 | "description": "Friendly emoji lookups and parsing utilities for Node.js. 💖",
5 | "keywords": [
6 | "emoji",
7 | "simple",
8 | "emoticons",
9 | "emoticon",
10 | "emojis",
11 | "smiley",
12 | "smileys",
13 | "smilies",
14 | "ideogram",
15 | "ideograms"
16 | ],
17 | "bugs": {
18 | "url": "https://github.com/omnidan/node-emoji/issues"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/omnidan/node-emoji"
23 | },
24 | "license": "MIT",
25 | "author": {
26 | "name": "Daniel Bugl",
27 | "email": "me@omnidan.net"
28 | },
29 | "type": "module",
30 | "exports": {
31 | ".": {
32 | "types": {
33 | "import": "./lib/index.d.ts",
34 | "require": "./lib/index.d.cts"
35 | },
36 | "import": "./lib/index.js",
37 | "require": "./lib/index.cjs"
38 | }
39 | },
40 | "main": "./lib/index.js",
41 | "files": [
42 | "lib/",
43 | "package.json",
44 | "LICENSE.md",
45 | "README.md"
46 | ],
47 | "scripts": {
48 | "build": "tsup",
49 | "format": "prettier \"**/*\" --ignore-unknown",
50 | "lint": "eslint . .*js --max-warnings 0 --report-unused-disable-directives",
51 | "lint:knip": "knip",
52 | "lint:md": "markdownlint \"**/*.md\" \".github/**/*.md\" --rules sentences-per-line",
53 | "lint:package-json": "npmPkgJsonLint .",
54 | "lint:packages": "pnpm dedupe --check",
55 | "lint:spelling": "cspell \"**\" \".github/**/*\"",
56 | "prepare": "husky install",
57 | "should-semantic-release": "should-semantic-release --verbose",
58 | "test": "vitest",
59 | "test:cjs": "node ./src/e2e.cjs",
60 | "tsc": "tsc"
61 | },
62 | "lint-staged": {
63 | "*": "prettier --ignore-unknown --write"
64 | },
65 | "dependencies": {
66 | "@sindresorhus/is": "^4.6.0",
67 | "char-regex": "^1.0.2",
68 | "emojilib": "^2.4.0",
69 | "skin-tone": "^2.0.0"
70 | },
71 | "devDependencies": {
72 | "@release-it/conventional-changelog": "^10.0.0",
73 | "@swc/core": "^1.3.58",
74 | "@types/eslint": "^8.44.7",
75 | "@typescript-eslint/eslint-plugin": "^6.10.0",
76 | "@typescript-eslint/parser": "^8.0.0",
77 | "@vitest/coverage-v8": "^0.34.6",
78 | "console-fail-test": "^0.5.0",
79 | "cspell": "^8.0.0",
80 | "eslint": "^8.53.0",
81 | "eslint-plugin-deprecation": "^3.0.0",
82 | "eslint-plugin-eslint-comments": "^3.2.0",
83 | "eslint-plugin-jsdoc": "^50.0.0",
84 | "eslint-plugin-jsonc": "^2.10.0",
85 | "eslint-plugin-markdown": "^3.0.1",
86 | "eslint-plugin-n": "^17.0.0",
87 | "eslint-plugin-no-only-tests": "^3.1.0",
88 | "eslint-plugin-perfectionist": "^2.3.0",
89 | "eslint-plugin-regexp": "^2.1.1",
90 | "eslint-plugin-vitest": "^0.3.9",
91 | "eslint-plugin-yml": "^1.10.0",
92 | "husky": "^8.0.3",
93 | "jsonc-eslint-parser": "^2.4.0",
94 | "knip": "^4.0.0",
95 | "lint-staged": "^15.1.0",
96 | "markdownlint": "^0.37.0",
97 | "markdownlint-cli": "^0.44.0",
98 | "npm-package-json-lint": "^8.0.0",
99 | "npm-package-json-lint-config-default": "^7.0.0",
100 | "prettier": "^3.0.3",
101 | "prettier-plugin-curly": "^0.3.0",
102 | "prettier-plugin-packagejson": "^2.4.6",
103 | "release-it": "^18.0.0",
104 | "sentences-per-line": "^0.3.0",
105 | "should-semantic-release": "^0.2.1",
106 | "tsup": "^8.0.0",
107 | "typescript": "^5.2.2",
108 | "vitest": "^0.34.6",
109 | "yaml-eslint-parser": "^1.2.2"
110 | },
111 | "packageManager": "pnpm@10.2.0",
112 | "engines": {
113 | "node": ">=18"
114 | },
115 | "publishConfig": {
116 | "provenance": true
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/data.ts:
--------------------------------------------------------------------------------
1 | import emojilib from 'emojilib'
2 |
3 | import { normalizeCode } from './utils.js'
4 |
5 | export interface Emoji {
6 | emoji: string
7 | key: string
8 | }
9 |
10 | export const emojiData = Object.entries(emojilib.lib).map(
11 | ([name, { char: emoji }]) => [name, emoji] as const,
12 | )
13 |
14 | export const emojiCodesByName = new Map(emojiData)
15 |
16 | export const emojiNamesByCode = new Map(
17 | emojiData.map(([name, emoji]) => [normalizeCode(emoji), name]),
18 | )
19 |
--------------------------------------------------------------------------------
/src/e2e.cjs:
--------------------------------------------------------------------------------
1 | const { strict: assert } = require('node:assert')
2 |
3 | const emoji = require('../lib/index.cjs')
4 |
5 | assert.equal(emoji.emojify(':wave:'), '👋')
6 |
--------------------------------------------------------------------------------
/src/emojify.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { emojify } from './emojify.js'
4 |
5 | describe('emojify', () => {
6 | it('handles flags correctly', () => {
7 | expect(
8 | emojify('Mexico :mexico: and Morocco :morocco: are not the same'),
9 | ).toBe('Mexico 🇲🇽 and Morocco 🇲🇦 are not the same')
10 | })
11 |
12 | it('leaves unknown emoji when no fallback is provided', () => {
13 | expect(emojify('I :unknown_emoji: :star: :another_one:')).toBe(
14 | 'I :unknown_emoji: ⭐ :another_one:',
15 | )
16 | })
17 |
18 | it('replaces unknown emoji with the fallback when a fallback string is provided', () => {
19 | expect(
20 | emojify('I :unknown_emoji: :star: :another_one:', {
21 | fallback: 'unknown',
22 | }),
23 | ).toBe('I unknown ⭐ unknown')
24 | })
25 |
26 | it('replaces unknown emoji with the fallback when a fallback function is provided', () => {
27 | expect(
28 | emojify('I :unknown_emoji: :star: :another_one:', {
29 | fallback: part => `(${part})`,
30 | }),
31 | ).toBe('I (unknown_emoji) ⭐ (another_one)')
32 | })
33 |
34 | it('parses a single :emoji: in a string when there is only one emoji', () => {
35 | expect(emojify(':coffee:!')).toBe('☕!')
36 | })
37 |
38 | it('parses multiple :emoji: in a string when there are multiple emoji', () => {
39 | expect(
40 | emojify(
41 | 'I :heart: :coffee:! - :hushed::star::heart_eyes: ::: test : : :+1:+',
42 | ),
43 | ).toBe('I ❤️ ☕! - 😯⭐😍 ::: test : : 👍+')
44 | })
45 |
46 | it('formats emoji when given a format function', () => {
47 | expect(
48 | emojify('I :unknown_emoji: :star: :another_one:', {
49 | format: name => `[${name}]`,
50 | }),
51 | ).toBe('I [:unknown_emoji:] [⭐] [:another_one:]')
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/src/emojify.ts:
--------------------------------------------------------------------------------
1 | import is from '@sindresorhus/is'
2 |
3 | import { findByName } from './findByName.js'
4 | import { asFunction, normalizeName } from './utils.js'
5 |
6 | export type EmojifyFormat = (
7 | name: string,
8 | part?: string,
9 | input?: string,
10 | ) => string
11 |
12 | export interface EmojifyOptions {
13 | /**
14 | * The string to fallback to if an emoji was not found.
15 | */
16 | fallback?: ((part: string) => string) | string
17 |
18 | /**
19 | * Adds a middleware layer to modify each matched emoji after parsing.
20 | */
21 | format?: EmojifyFormat
22 | }
23 |
24 | /**
25 | * Parse all markdown-encoded emojis in a string.
26 | */
27 | export const emojify = (
28 | input: string,
29 | { fallback, format = name => name }: EmojifyOptions = {},
30 | ) => {
31 | const fallbackFunction =
32 | fallback === undefined ? fallback : asFunction(fallback)
33 |
34 | is.assert.string(input)
35 | is.assert.any([is.default.undefined, is.default.function_], fallbackFunction)
36 | is.assert.function_(format)
37 |
38 | return input.replace(/:[\w\-+]+:/g, part => {
39 | const found = findByName(part)
40 | if (found) {
41 | return format(found.emoji, part, input)
42 | }
43 |
44 | if (fallbackFunction) {
45 | return format(fallbackFunction(normalizeName(part)))
46 | }
47 |
48 | return format(part)
49 | })
50 | }
51 |
--------------------------------------------------------------------------------
/src/emojilib.d.ts:
--------------------------------------------------------------------------------
1 | // Types for emojilib@v2
2 | declare module 'emojilib' {
3 | export const lib: Record
4 | }
5 |
--------------------------------------------------------------------------------
/src/find.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { find } from './find.js'
4 |
5 | describe('find', () => {
6 | it('returns an emoji when given a name', () => {
7 | expect(find('heart')).toEqual({ emoji: '❤️', key: 'heart' })
8 | })
9 |
10 | it('returns an emoji when given a :name:', () => {
11 | expect(find(':heart:')).toEqual({ emoji: '❤️', key: 'heart' })
12 | })
13 |
14 | it('returns an emoji when given a code', () => {
15 | expect(find('❤')).toEqual({ emoji: '❤', key: 'heart' })
16 | })
17 |
18 | it('returns the base emoji when given an alternate emoji code', () => {
19 | expect(find('❤️')).toEqual({ emoji: '❤', key: 'heart' })
20 | })
21 |
22 | it('returns undefined when given an unknown name', () => {
23 | expect(find('unknown_emoji')).toBeUndefined()
24 | })
25 |
26 | it('returns undefined when given an unknown :name:', () => {
27 | expect(find('unknown_emoji')).toBeUndefined()
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/find.ts:
--------------------------------------------------------------------------------
1 | import { findByCode } from './findByCode.js'
2 | import { findByName } from './findByName.js'
3 |
4 | /**
5 | * Get the name and character of an emoji.
6 | */
7 | export const find = (codeOrName: string) => {
8 | return findByCode(codeOrName) ?? findByName(codeOrName)
9 | }
10 |
--------------------------------------------------------------------------------
/src/findByCode.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { findByCode } from './findByCode.js'
4 |
5 | describe('findByCode', () => {
6 | it('returns undefined when given a name', () => {
7 | expect(findByCode('heart')).toBeUndefined()
8 | })
9 |
10 | it('returns undefined when given a :name:', () => {
11 | expect(findByCode(':heart:')).toBeUndefined()
12 | })
13 |
14 | it('returns the emoji when given an emoji code', () => {
15 | expect(findByCode('❤')).toEqual({ emoji: '❤', key: 'heart' })
16 | })
17 |
18 | it('returns the base emoji when given an alternate emoji code', () => {
19 | expect(findByCode('❤️')).toEqual({ emoji: '❤', key: 'heart' })
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/src/findByCode.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '@sindresorhus/is'
2 |
3 | import { emojiNamesByCode } from './data.js'
4 | import { normalizeCode } from './utils.js'
5 |
6 | export const findByCode = (code: string) => {
7 | assert.string(code)
8 |
9 | const emojiNormalized = normalizeCode(code)
10 | const key = emojiNamesByCode.get(emojiNormalized)
11 |
12 | return key ? { emoji: emojiNormalized, key } : undefined
13 | }
14 |
--------------------------------------------------------------------------------
/src/findByName.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { findByName } from './findByName.js'
4 |
5 | describe('findByName', () => {
6 | it('returns an emoji when given a name', () => {
7 | expect(findByName('heart')).toEqual({ emoji: '❤️', key: 'heart' })
8 | })
9 |
10 | it('returns an emoji when given a :name:', () => {
11 | expect(findByName(':heart:')).toEqual({ emoji: '❤️', key: 'heart' })
12 | })
13 |
14 | it('returns undefined when given an emoji code', () => {
15 | expect(findByName('❤')).toBeUndefined()
16 | })
17 |
18 | it('returns undefined when given an alternate emoji code', () => {
19 | expect(findByName('❤️')).toBeUndefined()
20 | })
21 |
22 | it('returns undefined when given an unknown name', () => {
23 | expect(findByName('unknown_emoji')).toBeUndefined()
24 | })
25 |
26 | it('returns undefined when given an unknown :name:', () => {
27 | expect(findByName('unknown_emoji')).toBeUndefined()
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/findByName.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '@sindresorhus/is'
2 |
3 | import { emojiCodesByName } from './data.js'
4 | import { normalizeName } from './utils.js'
5 |
6 | export const findByName = (name: string) => {
7 | assert.string(name)
8 |
9 | const nameNormalized = normalizeName(name)
10 | const emoji = emojiCodesByName.get(nameNormalized)
11 |
12 | return emoji ? { emoji, key: nameNormalized } : undefined
13 | }
14 |
--------------------------------------------------------------------------------
/src/get.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { get } from './get.js'
4 |
5 | describe('get', () => {
6 | it('returns an emoji code when given a string', () => {
7 | expect(get('coffee')).toBe('☕')
8 | })
9 |
10 | it('returns the contained emoji code when given markdown emoji', () => {
11 | expect(get(':coffee:')).toBe('☕')
12 | })
13 |
14 | it('returns undefined when given an emoji', () => {
15 | expect(get('👯')).toBeUndefined()
16 | })
17 |
18 | it('returns undefined when given an unknown word', () => {
19 | expect(get('unknown')).toBeUndefined()
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/src/get.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '@sindresorhus/is'
2 |
3 | import { emojiCodesByName } from './data.js'
4 | import { normalizeName } from './utils.js'
5 |
6 | /**
7 | * Get an emoji from an emoji name.
8 | */
9 | export const get = (codeOrName: string) => {
10 | assert.string(codeOrName)
11 |
12 | return emojiCodesByName.get(normalizeName(codeOrName))
13 | }
14 |
--------------------------------------------------------------------------------
/src/has.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { has } from './has.js'
4 |
5 | describe('has', () => {
6 | it('returns true when given an emoji', () => {
7 | expect(has('☕')).toBe(true)
8 | })
9 |
10 | it('returns true when given the name of an emoji', () => {
11 | expect(has('coffee')).toBe(true)
12 | })
13 |
14 | it('returns true when given a markdown emoji name', () => {
15 | expect(has(':coffee:')).toBe(true)
16 | })
17 |
18 | it('returns false when given unrelated text', () => {
19 | expect(has('nonexistent')).toBe(false)
20 | })
21 |
22 | it('returns false when given an unknown markdown name', () => {
23 | expect(has(':nonexistent:')).toBe(false)
24 | })
25 |
26 | it('returns true when given a emoji in base form', () => {
27 | expect(has('❤️')).toBe(true)
28 | })
29 |
30 | it('returns true when given an emoji in code text form', () => {
31 | expect(has('❤')).toBe(true)
32 | })
33 |
34 | it('returns false when given multiple emoji codes', () => {
35 | expect(has('🍕❤️💋☕')).toBe(false)
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/src/has.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '@sindresorhus/is'
2 |
3 | import { emojiCodesByName, emojiNamesByCode } from './data.js'
4 | import { normalizeCode, normalizeName } from './utils.js'
5 |
6 | /**
7 | * Check if this library supports a specific emoji.
8 | */
9 | export const has = (codeOrName: string) => {
10 | assert.string(codeOrName)
11 |
12 | return (
13 | emojiCodesByName.has(normalizeName(codeOrName)) ||
14 | emojiNamesByCode.has(normalizeCode(codeOrName))
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './emojify.js'
2 | export * from './find.js'
3 | export * from './get.js'
4 | export * from './has.js'
5 | export * from './random.js'
6 | export * from './replace.js'
7 | export * from './search.js'
8 | export * from './strip.js'
9 | export * from './unemojify.js'
10 | export * from './which.js'
11 |
--------------------------------------------------------------------------------
/src/random.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { get } from './get.js'
4 | import { has } from './has.js'
5 | import { random } from './random.js'
6 |
7 | describe('random', () => {
8 | it('returns a random emoji and the corresponding key', () => {
9 | const result = random()
10 |
11 | expect(has(result.name)).toBe(true)
12 | expect(result.emoji).toBe(get(result.name))
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/random.ts:
--------------------------------------------------------------------------------
1 | import { emojiData } from './data.js'
2 | import { randomItem } from './utils.js'
3 |
4 | /**
5 | * Get a random emoji.
6 | */
7 | export const random = () => {
8 | const [name, emoji] = randomItem(emojiData)
9 | return { emoji, name }
10 | }
11 |
--------------------------------------------------------------------------------
/src/replace.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { replace } from './replace.js'
4 |
5 | describe('replace', () => {
6 | it('returns an empty string when given an empty string', () => {
7 | expect(replace('', 'b')).toBe('')
8 | })
9 |
10 | it('returns the input text when given text without emojis', () => {
11 | expect(replace('no emojis', 'b')).toBe('no emojis')
12 | })
13 |
14 | it('returns a single emoji replacement when given text with one emoji', () => {
15 | expect(replace('a ☕ c', 'b')).toBe('a bc')
16 | })
17 |
18 | it('returns multiple emoji replacement when given text with multiple emojis', () => {
19 | expect(replace('a ☕ c 🍕 d', 'b')).toBe('a bc bd')
20 | })
21 |
22 | it('preserves spaces around emojis when preserveSpaces is true', () => {
23 | expect(replace('a ☕ c 🍕 d', 'b', { preserveSpaces: true })).toBe(
24 | 'a b c b d',
25 | )
26 | })
27 |
28 | it('replaces with the replacer when given a replacer', () => {
29 | expect(replace('a 🌭 c', ({ emoji }) => `>${emoji}<`)).toBe('a >🌭 {
33 | expect(
34 | replace('a 🌭 c', ({ emoji }) => `>${emoji}<`, {
35 | preserveSpaces: true,
36 | }),
37 | ).toBe('a >🌭< c')
38 | })
39 |
40 | it('calls the replacer again when given multiple emojis', () => {
41 | expect(
42 | replace(
43 | 'a 🌭 b 🌭 🍵☕ c',
44 | (() => {
45 | let counter = 0
46 | const letters = ['w', 'x', 'y', 'z']
47 | return () => letters[counter++]
48 | })(),
49 | ),
50 | ).toBe('a wb xyzc') // cspell:disable-line
51 | })
52 |
53 | it('strips complex emojis', () => {
54 | expect(replace('before 👩❤️💋👩 after', '')).toBe('before after')
55 | })
56 |
57 | it('strips flag emojis', () => {
58 | expect(replace('There is no flag 🇲🇽', '')).toBe('There is no flag ')
59 | })
60 |
61 | it('ignores known existing complex emojis', () => {
62 | expect(replace('Some 🍕❤️💋☕ emoji', '')).toBe('Some ❤️💋☕ emoji')
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/src/replace.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '@sindresorhus/is'
2 |
3 | import { Emoji } from './data.js'
4 | import { findByCode } from './findByCode.js'
5 | import { asFunction, charRegexMatcher } from './utils.js'
6 |
7 | export type ReplaceReplacement = (
8 | emoji: Emoji,
9 | index: number,
10 | string: string,
11 | ) => string
12 |
13 | /**
14 | * Replace the emojis in a string.
15 | */
16 | export const replace = (
17 | input: string,
18 | replacement: ReplaceReplacement | string,
19 | { preserveSpaces = false } = {},
20 | ) => {
21 | const replace = asFunction(replacement)
22 |
23 | assert.string(input)
24 | assert.function_(replace)
25 | assert.boolean(preserveSpaces)
26 |
27 | const characters = input.match(charRegexMatcher)
28 | if (characters === null) {
29 | return input
30 | }
31 |
32 | return characters
33 | .map((character, index) => {
34 | const found = findByCode(character)
35 | if (!found) {
36 | return character
37 | }
38 |
39 | if (!preserveSpaces && characters[index + 1] === ' ') {
40 | characters[index + 1] = ''
41 | }
42 |
43 | return replace(found, index, input)
44 | })
45 | .join('')
46 | }
47 |
--------------------------------------------------------------------------------
/src/search.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { search } from './search.js'
4 |
5 | describe('search', () => {
6 | it('returns a single pair when given a one-of emoji name', () => {
7 | expect(search('100')).toEqual([{ emoji: '💯', name: '100' }])
8 | })
9 |
10 | it('returns a single pair when given one-of emoji name as regular expression', () => {
11 | expect(search(/100/)).toEqual([{ emoji: '💯', name: '100' }])
12 | })
13 |
14 | it('returns multiple emojis when given a common substring', () => {
15 | expect(search('cartwheel')).toEqual([
16 | {
17 | emoji: '🤸♀️',
18 | name: 'woman_cartwheeling',
19 | },
20 | {
21 | emoji: '🤸♂️',
22 | name: 'man_cartwheeling',
23 | },
24 | ])
25 | })
26 |
27 | it('returns multiple emojis when given a common regular expression', () => {
28 | expect(search(/cartwheel/)).toEqual([
29 | {
30 | emoji: '🤸♀️',
31 | name: 'woman_cartwheeling',
32 | },
33 | {
34 | emoji: '🤸♂️',
35 | name: 'man_cartwheeling',
36 | },
37 | ])
38 | })
39 |
40 | it('should match when you include the colon', () => {
41 | expect(search(':cartwheel:')).toEqual([
42 | {
43 | emoji: '🤸♀️',
44 | name: 'woman_cartwheeling',
45 | },
46 | {
47 | emoji: '🤸♂️',
48 | name: 'man_cartwheeling',
49 | },
50 | ])
51 | })
52 |
53 | it('should match when you include the colon in the regular expression', () => {
54 | expect(search(/:cartwheel:/)).toEqual([
55 | {
56 | emoji: '🤸♀️',
57 | name: 'woman_cartwheeling',
58 | },
59 | {
60 | emoji: '🤸♂️',
61 | name: 'man_cartwheeling',
62 | },
63 | ])
64 | })
65 |
66 | it('returns an empty array when no matching emojis are found for a string search', () => {
67 | expect(search('notAnEmoji')).toEqual([])
68 | })
69 |
70 | it('returns an empty array when no matching emojis are found for a regular expression search', () => {
71 | expect(search(/notAnEmoji/)).toEqual([])
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/src/search.ts:
--------------------------------------------------------------------------------
1 | import is, { assert } from '@sindresorhus/is'
2 |
3 | import { emojiData } from './data.js'
4 | import { normalizeName } from './utils.js'
5 |
6 | /**
7 | * Search for emojis containing the provided name or pattern in their name.
8 | */
9 | export const search = (keyword: RegExp | string) => {
10 | assert.any([is.default.string, is.default.regExp], keyword)
11 |
12 | if (is.default.string(keyword)) {
13 | keyword = normalizeName(keyword)
14 | }
15 |
16 | if (is.default.regExp(keyword)) {
17 | const normalizedPattern = normalizeName(keyword.source)
18 | keyword = new RegExp(normalizedPattern)
19 | }
20 |
21 | return emojiData
22 | .filter(([name]) => name.match(keyword))
23 | .map(([name, emoji]) => ({ emoji, name }))
24 | }
25 |
--------------------------------------------------------------------------------
/src/strip.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { strip } from './strip.js'
4 |
5 | describe('strip', () => {
6 | it('returns an empty string when given an empty string', () => {
7 | expect(strip('')).toBe('')
8 | })
9 |
10 | it('returns an the input string when given a string with no emojis', () => {
11 | expect(strip('abc')).toBe('abc')
12 | })
13 |
14 | it('returns an input string with an emoji removed when given an input string with one emoji', () => {
15 | expect(strip('a ☕ c')).toBe('a c')
16 | })
17 |
18 | it('returns an input string with multiple emojis removed when given an input string with multiple emojis', () => {
19 | expect(strip('a ☕ b 🍕 c')).toBe('a b c')
20 | })
21 |
22 | it('preserves spaces around emoji when preserveSpaces is true', () => {
23 | expect(strip('a ☕ c', { preserveSpaces: true })).toBe('a c')
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/strip.ts:
--------------------------------------------------------------------------------
1 | import { replace } from './replace.js'
2 |
3 | export interface StripOptions {
4 | /**
5 | * Whether to keep the extra space after a stripped emoji.
6 | */
7 | preserveSpaces?: boolean
8 | }
9 |
10 | /**
11 | * Remove all the emojis from a string.
12 | */
13 | export const strip = (input: string, { preserveSpaces }: StripOptions = {}) =>
14 | replace(input, '', { preserveSpaces })
15 |
--------------------------------------------------------------------------------
/src/unemojify.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { unemojify } from './unemojify.js'
4 |
5 | describe('unemojify', () => {
6 | it('returns a blank string when given a blank string', () => {
7 | expect(unemojify('')).toBe('')
8 | })
9 |
10 | it('returns a replaced emoji name when given a string with one emoji', () => {
11 | expect(unemojify('a ☕ c')).toBe('a :coffee: c')
12 | })
13 |
14 | it('returns multiple replaced emoji names when given a string with multiple emojis', () => {
15 | expect(unemojify('a ☕ 🌭 c')).toBe('a :coffee: :hotdog: c')
16 | })
17 |
18 | it('returns a complex emoji name when given a complex emoji:', () => {
19 | expect(unemojify('before 👩❤️💋👩 after')).toBe(
20 | 'before :couplekiss_woman_woman: after',
21 | )
22 | })
23 |
24 | it('parses emojis with names next to non-space characters', () => {
25 | expect(unemojify('I ❤️ ☕️! - 😯⭐️😍 ::: test : : 👍+')).toBe(
26 | 'I :heart: :coffee:! - :hushed::star::heart_eyes: ::: test : : :+1:+',
27 | )
28 | })
29 |
30 | it('ignores only unknown emoji when given a string with some valid, some unknown emojis', () => {
31 | // Emoji :melting_face: (U+1FAE0) Unicode 14.0 draft: https://emojipedia.org/unicode-14.0
32 | expect(unemojify('I ⭐️ :another_one: 🫠')).toBe(
33 | 'I :star: :another_one: 🫠',
34 | )
35 | })
36 |
37 | // see issue #21
38 | it('should parse flags correctly', () => {
39 | expect(unemojify('The flags of 🇲🇽 and 🇲🇦 are not the same')).toBe(
40 | 'The flags of :mexico: and :morocco: are not the same',
41 | )
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/src/unemojify.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '@sindresorhus/is'
2 |
3 | import { charRegexMatcher } from './utils.js'
4 | import { which } from './which.js'
5 |
6 | /**
7 | * Convert all emojis in a string to their markdown-encoded counterparts.
8 | */
9 | export const unemojify = (input: string) => {
10 | assert.string(input)
11 |
12 | const characters = input.match(charRegexMatcher)
13 | if (characters === null) {
14 | return input
15 | }
16 |
17 | return characters
18 | .map(character => which(character, { markdown: true }) ?? character)
19 | .join('')
20 | }
21 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import charRegex from 'char-regex'
2 |
3 | export const charRegexMatcher = charRegex()
4 |
5 | export function asFunction(
6 | input: ((...args: Args) => T) | T,
7 | ): (...args: Args) => T {
8 | return typeof input === 'function' ? input : () => input
9 | }
10 |
11 | /**
12 | * Non spacing mark contained by some emoticons (65039 - '️' - 0xFE0F).
13 | *
14 | * It's the 'Variant Form', which provides more information so that emoticons
15 | * can be rendered as more colorful graphics. FE0E is a unicode text version
16 | * whereas FE0F should be rendered as a graphical version.
17 | * The code gracefully degrades.
18 | */
19 | const NON_SPACING_MARK = String.fromCharCode(65039)
20 |
21 | const nonSpacingRegex = new RegExp(NON_SPACING_MARK, 'g')
22 |
23 | /**
24 | * Removes the non-spacing-mark from the emoji code.
25 | *
26 | * Never send a stripped version to clients, as it kills graphical emoticons.
27 | */
28 | export function normalizeCode(code: string) {
29 | return code.replace(nonSpacingRegex, '')
30 | }
31 |
32 | export function normalizeName(name: string) {
33 | return /:.+:/.test(name) ? name.slice(1, -1) : name
34 | }
35 |
36 | export function randomItem(array: T[]) {
37 | return array[Math.floor(Math.random() * array.length)]
38 | }
39 |
--------------------------------------------------------------------------------
/src/which.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { which } from './which.js'
4 |
5 | describe('which', () => {
6 | it('returns a simple emoji name when given an emoji', () => {
7 | expect(which('☕')).toBe('coffee')
8 | })
9 |
10 | it('returns a simple emoji name as markdown when specified as markdown', () => {
11 | expect(which('☕', { markdown: true })).toBe(':coffee:')
12 | })
13 |
14 | it('returns a skin toned emoji name when given a skin toned emoji', () => {
15 | expect(which('👍🏾')).toBe('+1')
16 | })
17 |
18 | it('returns a skin toned emoji name as markdown when specified as markdown', () => {
19 | expect(which('👍🏾', { markdown: true })).toBe(':+1:')
20 | })
21 |
22 | // see issue #21
23 | it('should work for flags', () => {
24 | expect(which('🇲🇽')).toBe('mexico')
25 | expect(which('🇲🇦')).toBe('morocco')
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/src/which.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '@sindresorhus/is'
2 | import skinTone from 'skin-tone'
3 |
4 | import { findByCode } from './findByCode.js'
5 |
6 | export interface WhichOptions {
7 | markdown?: boolean
8 | }
9 |
10 | /**
11 | * Get an emoji name from an emoji.
12 | */
13 | export const which = (
14 | emoji: string,
15 | { markdown = false }: WhichOptions = {},
16 | ) => {
17 | assert.string(emoji)
18 | assert.boolean(markdown)
19 |
20 | const result = findByCode(skinTone(emoji, 'none'))
21 | if (result === undefined) {
22 | return undefined
23 | }
24 |
25 | return markdown ? `:${result.key}:` : result.key
26 | }
27 |
--------------------------------------------------------------------------------
/tea.yaml:
--------------------------------------------------------------------------------
1 | # https://tea.xyz/what-is-this-file
2 | ---
3 | version: 1.0.0
4 | codeOwners:
5 | - '0xA11fbA75aBC493b9b2902F9Ccc686DfB277Ae933'
6 | quorum: 1
7 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | { "extends": "./tsconfig.json", "include": ["."] }
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "declarationMap": true,
5 | "esModuleInterop": true,
6 | "module": "NodeNext",
7 | "moduleResolution": "NodeNext",
8 | "noEmit": true,
9 | "outDir": "lib",
10 | "resolveJsonModule": true,
11 | "skipLibCheck": true,
12 | "sourceMap": true,
13 | "strict": true,
14 | "target": "ES2022"
15 | },
16 | "include": ["src"]
17 | }
18 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | export default defineConfig({
4 | clean: true,
5 | dts: true,
6 | entry: ['src/**/index.ts'],
7 | format: ['cjs', 'esm'],
8 | outDir: 'lib',
9 | sourcemap: true,
10 | })
11 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | clearMocks: true,
6 | coverage: {
7 | all: true,
8 | branches: 100,
9 | exclude: ['lib', 'src/*.d.ts', 'src/e2e.cjs', 'src/index.ts'],
10 | functions: 100,
11 | include: ['src'],
12 | lines: 100,
13 | reporter: ['html', 'lcov'],
14 | statements: 100,
15 | },
16 | exclude: ['lib', 'node_modules'],
17 | setupFiles: ['console-fail-test/setup'],
18 | },
19 | })
20 |
--------------------------------------------------------------------------------