15 Commits

Author SHA1 Message Date
renovate[bot]
18020f3bc8 ⬆️ Upgrade src/dismoji/raw digest to 7876468 (#28)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 23:11:47 +02:00
7a42ead179 🔧 Update renovate incorrect base branch (#27) 2025-09-01 23:10:53 +02:00
1d9d14f1ed 🐛 Fix missing comma in renovate.json (#26) 2025-09-01 23:07:38 +02:00
cc7d78a47a feat: Update Renovate configuration and extend settings (#24) 2025-09-01 23:04:05 +02:00
renovate[bot]
fe3bcffa24 ⬆️ Upgrade actions/checkout action to v5 (#23)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 16:11:23 +02:00
renovate[bot]
b01368182f ⬆️ Upgrade hashicorp/setup-copywrite digest to 32f9f1c (#22)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-22 22:37:56 +02:00
9fb0c776d7 📝 Add acknowledgements section to README for API design inspiration (#20) 2025-05-29 14:44:52 +02:00
cfa8812081 🐛 Fix some complex emojis not being converted and enhance test coverage (#19) 2025-05-29 14:41:33 +02:00
34739a077c Refactor emojize and demojize functions to use shared replacement functions for improved performance (#18) 2025-05-29 13:52:50 +02:00
47aa6a3fc1 Add __all__ declaration to expose public API for emojize and demojize functions (#17) 2025-05-29 13:41:52 +02:00
ad98dd9a58 Implement demojize function for bidirectional emoji conversion (#16)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-29 13:38:56 +02:00
a1d2592ee1 ⬆️ Update subproject commit reference in dismoji/raw (#9) 2025-05-19 10:54:14 +02:00
6dc46f06d6 Optimize memory usage by removing EMOJIS variable after mapping (#10) 2025-05-19 10:51:33 +02:00
8af0deea07 🔧 Add pydocstyle convention and clean up pyproject.toml (#11) 2025-05-19 10:48:59 +02:00
2014fb4a61 Fix emoji pattern to work without spaces and enhance test coverage for emojize function (#8) 2025-05-04 01:35:32 +02:00
8 changed files with 181 additions and 42 deletions

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: pypi environment: pypi
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
submodules: true submodules: true

View File

@@ -9,9 +9,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Copywrite - name: Setup Copywrite
uses: hashicorp/setup-copywrite@5e3e8a26d7b9f8a508848ad0a069dfd2f7aa5339 uses: hashicorp/setup-copywrite@32f9f1c86f661b8a51100768976a06f1b281a035
- name: Check Header Compliance - name: Check Header Compliance
run: copywrite headers --plan --config .copywrite.hcl run: copywrite headers --plan --config .copywrite.hcl
@@ -35,7 +35,7 @@ jobs:
name: "Python 3.13" name: "Python 3.13"
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
submodules: true submodules: true
@@ -75,7 +75,7 @@ jobs:
name: ${{ matrix.name }} name: ${{ matrix.name }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: "Install uv" - name: "Install uv"
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6

View File

@@ -12,7 +12,8 @@
<!-- end badges --> <!-- end badges -->
A Python library for converting Discord emoji names to their Unicode equivalents. A Python library for converting Discord emoji names to their Unicode equivalents and
vice versa.
</div> </div>
@@ -31,9 +32,9 @@ A Python library for converting Discord emoji names to their Unicode equivalents
## Overview ## Overview
Dismoji is a lightweight Python library that provides a simple way to convert Discord Dismoji is a lightweight Python library that provides a simple way to convert Discord
emoji names to their Unicode equivalents. With just a single function call, you can emoji names to their Unicode equivalents and vice versa. With just two function calls,
transform text containing Discord-style emoji codes (like `:smile:`) into text with you can transform text containing Discord-style emoji codes (like `:smile:`) into text
actual Unicode emoji characters (like "😄"). with actual Unicode emoji characters (like "😄") and back again.
This library uses This library uses
[Paillat-dev/discord-emojis](https://github.com/Paillat-dev/discord-emojis) as the [Paillat-dev/discord-emojis](https://github.com/Paillat-dev/discord-emojis) as the
@@ -56,16 +57,23 @@ import dismoji
text = "Hello, :wave: I'm excited! :partying_face:" text = "Hello, :wave: I'm excited! :partying_face:"
converted_text = dismoji.emojize(text) converted_text = dismoji.emojize(text)
print(converted_text) # Output: "Hello, 👋 I'm excited! 🥳" print(converted_text) # Output: "Hello, 👋 I'm excited! 🥳"
# Convert Unicode emojis back to Discord emoji names
emoji_text = "Hello, 👋 I'm excited! 🥳"
named_text = dismoji.demojize(emoji_text)
print(named_text) # Output: "Hello, :wave: I'm excited! :partying_face:"
``` ```
## Features ## Features
- **Simple API**: Just one function to remember - `dismoji.emojize()` - **Simple API**: Just two functions to remember - `dismoji.emojize()` and
`dismoji.demojize()`
- **Discord Compatible**: Supports Discord's emoji naming conventions - **Discord Compatible**: Supports Discord's emoji naming conventions
- **Comprehensive**: Includes all standard emojis available on Discord - **Comprehensive**: Includes all standard emojis available on Discord
- **Type Safe**: Fully type-annotated for better IDE integration - **Type Safe**: Fully type-annotated for better IDE integration
- **Zero Dependencies**: Lightweight with no external dependencies - **Zero Dependencies**: Lightweight with no external dependencies
- **Fast**: Optimized for quick emoji replacement - **Fast**: Optimized for quick emoji replacement
- **Bidirectional**: Convert between emoji names and characters in both directions
## Getting Help ## Getting Help
@@ -94,6 +102,10 @@ If you encounter issues or have questions about dismoji:
- **HashiCorp Copywrite**: For managing license headers - **HashiCorp Copywrite**: For managing license headers
- **basedpyright**: For type checking - **basedpyright**: For type checking
## Acknowledgements
- [`emoji`](https://pypi.org/project/emoji/) as inspiration for the API design
## License ## License
MIT License - Copyright (c) 2025 Paillat-dev MIT License - Copyright (c) 2025 Paillat-dev

View File

@@ -120,6 +120,7 @@ exclude = [
"src/dismoji/_version.py" "src/dismoji/_version.py"
] ]
[tool.ruff.lint] [tool.ruff.lint]
select = ["ALL"] select = ["ALL"]
per-file-ignores = { "examples/**/*" = ["INP001", "ARG002", "T201"], "tests/**/*" = ["S101"], "src/dismoji/_version.py" = ["I001", "Q000", "UP005", "UP006", "UP035"] } per-file-ignores = { "examples/**/*" = ["INP001", "ARG002", "T201"], "tests/**/*" = ["S101"], "src/dismoji/_version.py" = ["I001", "Q000", "UP005", "UP006", "UP035"] }
@@ -151,6 +152,7 @@ extend-ignore = [
"C901", "C901",
"ISC003" # conflicts with basedpyright reportImplicitStringConcatenation "ISC003" # conflicts with basedpyright reportImplicitStringConcatenation
] ]
pydocstyle.convention = "google"
[tool.uv.sources] [tool.uv.sources]
py-cord = { git = "https://github.com/Pycord-Development/pycord", rev = "c0c0b7c58f7b489983a159f5e0eea2c0dab0b0c8" } py-cord = { git = "https://github.com/Pycord-Development/pycord", rev = "c0c0b7c58f7b489983a159f5e0eea2c0dab0b0c8" }

View File

@@ -1,25 +1,18 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"], "extends": [
"baseBranches": ["master"], "local>nicebots-xyz/renovate-config",
"labels": ["deps"], ":semanticPrefixFixDepsChoreOthers",
"ignorePaths": ["requirements.txt"], ":dependencyDashboard"
"commitMessagePrefix": "⬆️", ],
"commitMessageAction": "Upgrade", "git-submodules": {
"packageRules": [ "enabled": true
{
"updateTypes": ["pin"],
"commitMessagePrefix": "📌",
"commitMessageAction": "Pin"
}, },
{ "forkProcessing": "enabled",
"updateTypes": ["rollback"], "baseBranchPatterns": [
"commitMessagePrefix": "⬇️", "master"
"commitMessageAction": "Downgrade" ],
}, "lockFileMaintenance": {
{ "enabled": true
"matchDatasources": ["pypi"],
"addLabels": ["pypi"]
} }
]
} }

View File

@@ -12,9 +12,32 @@ EMOJIS_PATH = Path(__file__).parent / "raw" / "build" / "emojis.json"
with EMOJIS_PATH.open("r", encoding="utf-8") as f: with EMOJIS_PATH.open("r", encoding="utf-8") as f:
EMOJIS = json.load(f) EMOJIS = json.load(f)
EMOJI_MAPPING: dict[str, str] = {k: EMOJIS["emojis"][v]["surrogates"] for k, v in EMOJIS["nameToEmoji"].items()} _VARIATION_SELECTOR = "\ufe0f" # We remove this as it is not needed by discord and causes issues with tests
EMOJI_MAPPING: dict[str, str] = {
k: EMOJIS["emojis"][v]["surrogates"].replace(_VARIATION_SELECTOR, "") for k, v in EMOJIS["nameToEmoji"].items()
}
EMOJI_PATTERN = re.compile(r"(?<!\w):([a-zA-Z0-9_-]+):(?!\w)") REVERSE_EMOJI_MAPPING: dict[str, str] = {}
for emoji_index_str, emoji_index in sorted(EMOJIS["surrogateToEmoji"].items(), key=lambda x: len(x[0]), reverse=True):
# Get the first name in the list as the preferred name
e = EMOJIS["emojis"][emoji_index]
# If it has multiple diversity parents, use the last name because it is the most specific one
# e.g. :handshake_light_skin_tone_dark_skin_tone: vs :handshake_tone1_tone5:
REVERSE_EMOJI_MAPPING[emoji_index_str] = e["names"][-1 if e.get("hasMultiDiversityParent") else 0]
del EMOJIS, _VARIATION_SELECTOR # Clean up to save memory
EMOJI_PATTERN = re.compile(r":([\w+-]+):")
EMOJI_CHARS_PATTERN = re.compile("|".join(map(re.escape, REVERSE_EMOJI_MAPPING.keys())))
def _replace(match: re.Match[str]) -> str:
emoji_name = match.group(1)
if emoji_name in EMOJI_MAPPING:
return EMOJI_MAPPING[emoji_name]
return match.group(0)
def emojize(s: str) -> str: def emojize(s: str) -> str:
@@ -27,11 +50,30 @@ def emojize(s: str) -> str:
str: The input string with emoji names replaced by emoji characters. str: The input string with emoji names replaced by emoji characters.
""" """
return EMOJI_PATTERN.sub(_replace, s)
def replace(match: re.Match[str]) -> str:
emoji_name = match.group(1)
if emoji_name in EMOJI_MAPPING:
return EMOJI_MAPPING[emoji_name]
return match.group(0)
return EMOJI_PATTERN.sub(replace, s) def _reverse_replace(match: re.Match[str]) -> str:
emoji = match.group(0)
return f":{REVERSE_EMOJI_MAPPING[emoji]}:"
def demojize(s: str) -> str:
"""Convert a string with emoji characters to a string with emoji names.
Args:
s (str): The input string containing emoji characters.
Returns:
str: The input string with emoji characters replaced by emoji names.
"""
return EMOJI_CHARS_PATTERN.sub(_reverse_replace, s)
__all__ = (
"EMOJI_MAPPING",
"REVERSE_EMOJI_MAPPING",
"demojize",
"emojize",
)

View File

@@ -1,7 +1,26 @@
# Copyright (c) Paillat-dev # Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from dismoji import emojize from dismoji import EMOJI_MAPPING, REVERSE_EMOJI_MAPPING, demojize, emojize
def are_equal(a: str, b: str) -> bool:
"""Check if two emojis are equal.
Allows for comparing emojis with modifiers even when they are in different orders.
Args:
a (str): First emoji string.
b (str): Second emoji string.
Returns:
bool: True if the emojis are equal, False otherwise.
"""
if len(a) != len(b):
return False
if len(a) == 1:
return a == b
return a[0] == b[0] and set(a[1:]) == set(b[1:])
def test_basic() -> None: def test_basic() -> None:
@@ -41,3 +60,74 @@ def test_multiple_emojis() -> None:
def test_complex_sentence() -> None: def test_complex_sentence() -> None:
"""Test emojize function with a complex sentence.""" """Test emojize function with a complex sentence."""
assert emojize("Hello :wave:, what's up? :smile: :white_check_mark: :smile:") == "Hello 👋, what's up? 😄 ✅ 😄" assert emojize("Hello :wave:, what's up? :smile: :white_check_mark: :smile:") == "Hello 👋, what's up? 😄 ✅ 😄"
def test_spaces() -> None:
"""Test emojize function with spaces."""
space_tests = [
("Hello :smile::smile:", "Hello 😄😄"),
("Hii what's up :wave:?", "Hii what's up 👋?"),
("Hello:wave: :smile:", "Hello👋 😄"),
("Hellooo :wave:hru?", "Hellooo 👋hru?"),
("Hii:wave:hru?", "Hii👋hru?"),
]
for input_str, expected_output in space_tests:
assert emojize(input_str) == expected_output
def test_emoji_with_special_characters() -> None:
"""Test emojize function with special characters."""
special_char_tests = [
("Hello :smile:!", "Hello 😄!"),
("Hello :smile:?", "Hello 😄?"),
("Hello :smile::smile:!", "Hello 😄😄!"),
("Hello :smile::smile:?", "Hello 😄😄?"),
("Hello :smile::smile::smile:!", "Hello 😄😄😄!"),
("Hello :smile::smile::smile:?", "Hello 😄😄😄?"),
]
for input_str, expected_output in special_char_tests:
assert emojize(input_str) == expected_output
def test_emojize_all() -> None:
for name, emoji in EMOJI_MAPPING.items():
assert are_equal(emojize(f":{name}:"), emoji)
def test_demojize_basic() -> None:
"""Test basic functionality of demojize function."""
assert demojize("Hello 😄") == "Hello :smile:"
def test_demojize_no_match() -> None:
"""Test demojize function with no matches."""
assert demojize("Hello world") == "Hello world"
def test_demojize_multiple_emojis() -> None:
"""Test demojize function with multiple emojis."""
assert demojize("😄 👋") == ":smile: :wave:"
def test_demojize_complex_sentence() -> None:
"""Test demojize function with a complex sentence."""
assert demojize("Hello 👋, what's up? 😄 ✅ 😄") == "Hello :wave:, what's up? :smile: :white_check_mark: :smile:"
def test_demojize_surrogate() -> None:
"""Test demojize function with surrogate pairs."""
surrogate_pairs = [
("🫱🏻‍🫲🏿", ":handshake_light_skin_tone_dark_skin_tone:"),
("🫱🏿‍🫲🏻", ":handshake_dark_skin_tone_light_skin_tone:"),
("🫱🏽‍🫲🏻", ":handshake_medium_skin_tone_light_skin_tone:"),
("🫱🏼‍🫲🏿", ":handshake_medium_light_skin_tone_dark_skin_tone:"),
("🫱🏾‍🫲🏻", ":handshake_medium_dark_skin_tone_light_skin_tone:"),
]
for surrogate, emoji_name in surrogate_pairs:
assert demojize(surrogate) == emoji_name
def test_demojize_all() -> None:
for emoji, name in REVERSE_EMOJI_MAPPING.items():
assert demojize(emoji) == f":{name}:"