Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ascii-to-emoji replacer #194

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"parser": "@babel/eslint-parser",
"env": {
"browser": true,
"es6": true
"es6": true,
"jest/globals": true
},
"settings": {
"react": {
Expand All @@ -21,7 +22,8 @@
"plugins": [
"react",
"prefer-object-spread",
"import"
"import",
"jest"
],
"extends": [
"umbrellio",
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ jobs:
uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
Expand Down
27 changes: 21 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ on:
pull_request

jobs:
tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- run: yarn install --frozen-lockfile

- run: yarn build

- run: yarn test
storybook-build:
runs-on: ubuntu-latest

Expand All @@ -12,9 +27,9 @@ jobs:
uses: actions/checkout@v2

- name: Use Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '16'
node-version: '20'

- name: Run build
run: |
Expand All @@ -31,9 +46,9 @@ jobs:
uses: actions/checkout@v2

- name: Use Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '16'
node-version: '20'

- name: Run lints
run: |
Expand All @@ -49,9 +64,9 @@ jobs:
- uses: actions/checkout@v2
- name: Use Node.js

uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '16'
node-version: '20'

- run: yarn install --frozen-lockfile
- run: yarn build
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deploy-storybook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ jobs:
uses: actions/checkout@v2

- name: Use Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: '20.x'

- name: Build
run: |
Expand Down
11 changes: 11 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-env node */

module.exports = {
bail: 1,
clearMocks: true,
moduleDirectories: ["node_modules"],
moduleFileExtensions: ["js"],
testMatch: [
"**/src/**/*.test.js",
],
}
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cadolabs/sphere-ui",
"version": "6.2.2",
"version": "6.2.3",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
Expand All @@ -21,7 +21,8 @@
"sb:lint": "bin/storybook lint",
"sb:eslint": "bin/storybook eslint",
"sb:stylelint": "bin/storybook stylelint",
"prepublishOnly": "yarn build"
"prepublishOnly": "yarn build",
"test": "jest"
},
"dependencies": {
"@emoji-mart/data": "^1.2.1",
Expand All @@ -46,11 +47,13 @@
"eslint-config-umbrellio": "^5.0.1",
"eslint-formatter-table": "^7.32.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "25.3.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.29.4",
"jest": "^29.7.0",
"postcss": "^8.4.31",
"postcss-scss": "^4.0.3",
"rollup": "^2.79.2",
Expand Down
27 changes: 27 additions & 0 deletions src/components/EmojiPicker/asciiToEmoji/asciiToEmoji.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { asciiToEmoji } from "./index"

const TEST_TEXT = "te=)st =)test test=) =)"

describe("asciiToEmoji", () => {
it("should replace all emojis", () => {
expect(asciiToEmoji(TEST_TEXT))
.toBe("te🙂st 🙂test test🙂 🙂")
})
it("should replace only emojis with spacing before", () => {
expect(asciiToEmoji(TEST_TEXT, { needSpacing: "before" }))
.toBe("te=)st 🙂test test=) 🙂")
})
it("should replace only emojis with spacing after", () => {
expect(asciiToEmoji(TEST_TEXT, { needSpacing: "after" }))
.toBe("te=)st =)test test🙂 🙂")
})
it("should replace only emojis with spacing around", () => {
expect(asciiToEmoji(TEST_TEXT, { needSpacing: "around" }))
.toBe("te=)st =)test test=) 🙂")
})
it("should throw error if inputString has incorrect type", () => {
expect(() => {
asciiToEmoji(91)
}).toThrow("inputString must be a string!")
})
})
168 changes: 168 additions & 0 deletions src/components/EmojiPicker/asciiToEmoji/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import data from "@emoji-mart/data"

const ASCII_TO_SHORTCODE_DICTIONARY = {
"<3": ":heart:",
"</3": ":broken_heart:",
":')": ":joy:",
":'-)": ":joy:",
":D": ":smiley:",
":-D": ":smiley:",
"=D": ":smiley:",
":)": ":slightly_smiling_face:",
":-)": ":slightly_smiling_face:",
"=]": ":slightly_smiling_face:",
"=)": ":slightly_smiling_face:",
":]": ":slightly_smiling_face:",
"':)": ":sweat_smile:",
"':-)": ":sweat_smile:",
"'=)": ":sweat_smile:",
"':D": ":sweat_smile:",
"':-D": ":sweat_smile:",
"'=D": ":sweat_smile:",
">:)": ":laughing:",
">;)": ":laughing:",
">:-)": ":laughing:",
">=)": ":laughing:",
";)": ":wink:",
";-)": ":wink:",
"*-)": ":wink:",
"*)": ":wink:",
";-]": ":wink:",
";]": ":wink:",
";D": ":wink:",
";^)": ":wink:",
"':(": ":sweat:",
"':-(": ":sweat:",
"'=(": ":sweat:",
":*": ":kissing_heart:",
":-*": ":kissing_heart:",
"=*": ":kissing_heart:",
":^*": ":kissing_heart:",
">:P": ":stuck_out_tongue_winking_eye:",
"X-P": ":stuck_out_tongue_winking_eye:",
"x-p": ":stuck_out_tongue_winking_eye:",
">:[": ":disappointed:",
":-(": ":disappointed:",
":(": ":disappointed:",
":-[": ":disappointed:",
":[": ":disappointed:",
"=(": ":disappointed:",
">:(:": ":angry:",
">:-((": ":angry:",
":@": ":angry:",
":'(": ":cry:",
":'-(": ":cry:",
";(": ":cry:",
";-(": ":cry:",
">.<": ":persevere:",
"D:": ":fearful:",
":$": ":flushed:",
"=$": ":flushed:",
"#-)": ":dizzy_face:",
"#)": ":dizzy_face:",
"%-)": ":dizzy_face:",
"%)": ":dizzy_face:",
"X)": ":dizzy_face:",
"X-)": ":dizzy_face:",
"*\\0/*": ":ok_woman:",
"\\0/": ":ok_woman:",
"*\\O/*": ":ok_woman:",
"\\O/": ":ok_woman:",
"O:-)": ":innocent:",
"0:-3": ":innocent:",
"0:3": ":innocent:",
"0:-)": ":innocent:",
"0:)": ":innocent:",
"0;^)": ":innocent:",
"O;-)": ":innocent:",
"O=)": ":innocent:",
"0;-)": ":innocent:",
"O:-3": ":innocent:",
"O:3": ":innocent:",
"B-)": ":sunglasses:",
"B)": ":sunglasses:",
"8)": ":sunglasses:",
"8-)": ":sunglasses:",
"B-D": ":sunglasses:",
"8-D": ":sunglasses:",
"-_-": ":expressionless:",
"-__-": ":expressionless:",
"-___-": ":expressionless:",
">:\\": ":confused:",
">:/": ":confused:",
":-/": ":confused:",
":-.": ":confused:",
":/": ":confused:",
":\\": ":confused:",
"=/": ":confused:",
"=\\": ":confused:",
":L": ":confused:",
"=L": ":confused:",
":P": ":stuck_out_tongue:",
":-P": ":stuck_out_tongue:",
"=P": ":stuck_out_tongue:",
":-p": ":stuck_out_tongue:",
":p": ":stuck_out_tongue:",
"=p": ":stuck_out_tongue:",
":-Þ": ":stuck_out_tongue:",
":Þ": ":stuck_out_tongue:",
":þ": ":stuck_out_tongue:",
":-þ": ":stuck_out_tongue:",
":-b": ":stuck_out_tongue:",
":b": ":stuck_out_tongue:",
}

const ASCII_KEYS = Object.keys(ASCII_TO_SHORTCODE_DICTIONARY)

const transformKeys = (keysArr, transformerFns = []) => {
let result = keysArr

transformerFns.forEach(fn => {
result = fn(result)
})

return result
}

const sortKeysByLength = arr => [...arr].sort((a, b) => b.length - a.length)
const escapeSpecialSymbols = arr => arr.map(key => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
const joinKeys = arr => arr.join("|")
const asciiKeysRegex = transformKeys(ASCII_KEYS, [sortKeysByLength, escapeSpecialSymbols, joinKeys])

const getByAscii = asciiKey => {
const id = ASCII_TO_SHORTCODE_DICTIONARY[asciiKey].replaceAll(":", "")

return data.emojis[id]?.skins[0]?.native
}

/**
* @param {string} inputString text that might contain ascii emoji
* @param {Object} [options]
* @param {"before" | "after" | "around"} [options.needSpacing]
* @returns {string} text with replacements
*/
export const asciiToEmoji = (inputString, options = {}) => {
if (typeof inputString !== "string") throw Error("inputString must be a string!")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут можно было бы возвращать неизмененный текст, но мне показалось это логичнее обрабатывать в каком-нибудь try/catch на месте.

const { needSpacing } = options

const needSpacingBeforeRegex = "(?<=\\s|^)"
const needSpacingAfterRegex = "(?=\\s|$)"

let condition = `${asciiKeysRegex}`

switch (needSpacing) {
case "before":
condition = `${needSpacingBeforeRegex}(${asciiKeysRegex})`
break
case "after":
condition = `(${asciiKeysRegex})${needSpacingAfterRegex}`
break
case "around":
condition = `${needSpacingBeforeRegex}(${asciiKeysRegex})${needSpacingAfterRegex}`
break
}

const regex = new RegExp(condition, "g")

return inputString.replace(regex, match => getByAscii(match))
}
4 changes: 4 additions & 0 deletions src/components/EmojiPicker/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React from "react"
import { Picker } from "emoji-mart"

import data from "@emoji-mart/data"

import { asciiToEmoji } from "./asciiToEmoji"

import translations from "./translations"

// because it has broken and unnecessary
Expand Down Expand Up @@ -40,3 +43,4 @@ const EmojiPicker = props => {
}

export default EmojiPicker
export { asciiToEmoji }
5 changes: 4 additions & 1 deletion storybook/i18n/locales/stories/emojiPicker/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ content:
main: Emoji is used to show emoji.
start:
title: Getting Started
body: The <code>EmojiPicker</code> component is based on <a>emoji-mart</a> Picker but is extended with additional translations.
component: The <code>EmojiPicker</code> component is based on <a>emoji-mart</a> Picker but is extended with additional translations.
converter:
title: Additional utility
description: The <code>asciiToEmoji</code> function replaces ASCII emojis with native browser emojis in the text.
props:
language: 'Available values: "en","bs","bg","hr","ro","sr","sv","de","fi","fr","ja","nl","pt","ru","tr","ar","be","cs","es","fa","hi","it","ko","pl","sa","uk","vi","zh",'
categories: 'Categories to show in the picker. Order is respected. Available values: "frequent", "people", "nature", "foods", "activity", "places", "objects", "symbols", "flags"'
Expand Down
Loading
Loading