Skip to content

Commit

Permalink
Improvements
Browse files Browse the repository at this point in the history
- Only add one accessor per emoji instead of alias
- Add DiscordEmoji.findByName and DiscordEmoji.findByUnicode
- Add support for hair styles
- Deprecate Emojis.get
- Make Emojis.all a list
  • Loading branch information
DRSchlaubi committed Mar 1, 2024
1 parent c59086b commit ff60427
Show file tree
Hide file tree
Showing 5 changed files with 3,887 additions and 3,947 deletions.
88 changes: 37 additions & 51 deletions buildSrc/src/main/kotlin/dev/kord/x/emoji/Generator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import org.gradle.kotlin.dsl.register
import kotlin.io.path.Path
import kotlin.io.path.div

val LIST_OF = MemberName("kotlin.collections", "listOf")

sealed class EmojiType {
abstract val name: ClassName

Expand Down Expand Up @@ -51,15 +53,15 @@ class EmojiPlugin : Plugin<Project> {
}
}

private val jsValidName = "[a-zA-Z0-9]+".toRegex()
private val snakeCase = """_\w""".toRegex()

private abstract class GenerateEmojisTask : DefaultTask() {
private val snakeCase = """_\w""".toRegex()
private val jsValidName = "[a-zA-Z0-9]+".toRegex()
private val targetFile = Path(project.rootProject.rootDir.absolutePath, "src/commonMain/kotlin")
private val fileName = "EmojisList"

init {
outputs.file(targetFile / "$fileName.kt")
inputs.file(project.file("buildSrc/src/main/resources/emojis.json"))
}

private fun String.escape() = flatMap { it.code.toString(36).toList() }
Expand All @@ -72,7 +74,7 @@ private abstract class GenerateEmojisTask : DefaultTask() {
val emojis = parseEmojis()
val file = FileSpec("dev.kord.x.emoji", "EmojiList") {
addKotlinDefaultImports(includeJvm = false, includeJs = false)
val emojisObject = addObject("Emojis") {
addObject("Emojis") {
addKdoc(
"""
List of all supported discord emojis.
Expand All @@ -87,13 +89,10 @@ private abstract class GenerateEmojisTask : DefaultTask() {
)
)

//disabled for now
generateMapGetter()
generateMap(emojis)
generateList(emojis)
emojis.forEach { apply(it) }
}

addType(emojisObject)
}

val directory = Path(project.rootProject.rootDir.absolutePath, "src/commonMain/kotlin")
Expand All @@ -111,43 +110,40 @@ private abstract class GenerateEmojisTask : DefaultTask() {
// else emoji
// }

@OptIn(DelicateKotlinPoetApi::class)
fun TypeSpec.Builder.generateMapGetter() {
addFunction("get") {
addAnnotation(
Deprecated(
"Replaced by DiscordEmoji.findByUnicodeOrNull",
ReplaceWith("DiscordEmoji.findByUnicodeOrNull(unicode)", "dev.kord.x.emoji.DiscordEmoji")
)
)
addKdoc("Gets a discord emoji with the given [unicode].")
addModifiers(KModifier.OPERATOR)
addParameter<String>("unicode")
returns(EmojiType.Base.name.copy(nullable = true))
addCode(
"""
val tone = unicode.toSkinTone()
val withoutTone = unicode.removeTone()
val emoji = all[withoutTone]
return if (emoji is %T) emoji.copy(tone = tone!!) else emoji
""".trimIndent(), EmojiType.Diverse.name
)
addCode("""return DiscordEmoji.findByUnicodeOrNull(unicode)""")
}
}

fun TypeSpec.Builder.generateMap(emojis: List<EmojiItem>) {
val type = MAP.parameterizedBy(STRING, EmojiType.Base.name)
val property = addProperty("all", type) {
fun TypeSpec.Builder.generateList(emojis: List<EmojiItem>) {
val type = LIST.parameterizedBy(EmojiType.Base.name)
addProperty("all", type) {
val initializer = emojis
.map {
val name = MemberName("", it.aliases.first().toCamelCase())
CodeBlock.of("%S to %M", it.emoji, name)
val name = MemberName("", it.camelCaseName)
CodeBlock.of("%M", name)
}
.joinToCode(prefix = "mapOf(\n", separator = ",\n", suffix = ")")
.joinToCode(prefix = "listOf(\n", separator = ",\n", suffix = ")")

initializer(initializer)
}

addProperty(property)
}

fun TypeSpec.Builder.apply(item: EmojiItem): Unit = when (item.hasSkinTones) {
true -> applyDiverse(item)
else -> applyGeneric(item)
true -> applyEmoji(item, EmojiType.Diverse.name)
else -> applyEmoji(item, EmojiType.Generic.name)
}

private fun PropertySpec.Builder.applyJsNameIfNeeded(name: String) {
Expand All @@ -159,34 +155,16 @@ private abstract class GenerateEmojisTask : DefaultTask() {
jsName(safeName)
}

fun TypeSpec.Builder.applyDiverse(item: EmojiItem) {
item.aliases.forEach { name ->
val camelCaseName = name.toCamelCase()
addProperty(camelCaseName, EmojiType.Diverse.name) {
applyJsNameIfNeeded(camelCaseName)
getter {
addStatement("""return %T(%S)""", EmojiType.Diverse.name, item.emoji)
}
fun TypeSpec.Builder.applyEmoji(item: EmojiItem, typeName: TypeName) {
addProperty(item.camelCaseName, typeName) {
applyJsNameIfNeeded(item.camelCaseName)
val names = item.aliases.map { CodeBlock.of("%S", it) }.joinToCode(", ")
getter {
addStatement("""return %T(%S, %M(%L))""", typeName, item.emoji, LIST_OF, names)
}
}
}

fun String.toCamelCase() = snakeCase.replace(replaceFirstChar { it.lowercase() }) {
it.value.drop(1).uppercase()
}


fun TypeSpec.Builder.applyGeneric(item: EmojiItem) {
item.aliases.forEach { name ->
val camelCaseName = name.toCamelCase()
addProperty(camelCaseName, EmojiType.Generic.name) {
applyJsNameIfNeeded(camelCaseName)
getter {
addStatement("""return %T(%S)""", EmojiType.Generic.name, item.emoji)
}
}
}
}

private fun parseEmojis(): List<EmojiItem> {
val content = javaClass.classLoader.getResource("emojis.json")!!.readText()
Expand All @@ -196,3 +174,11 @@ private abstract class GenerateEmojisTask : DefaultTask() {
return json.decodeFromString(content)
}
}

private val EmojiItem.camelCaseName: String
get() =
(aliases.firstOrNull { jsValidName.matches(it) } ?: aliases.first()).toCamelCase()

private fun String.toCamelCase() = snakeCase.replace(replaceFirstChar { it.lowercase() }) {
it.value.drop(1).uppercase()
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import dev.kord.rest.builder.component.ButtonBuilder
*/
public var ButtonBuilder.discordEmoji: DiscordEmoji?
get() = emoji?.name?.let { unicode ->
Emojis[unicode]
DiscordEmoji.findByUnicodeOrNull(unicode)
}
set(value) {
emoji = value?.let { DiscordPartialEmoji(name = it.unicode) }
Expand Down
104 changes: 95 additions & 9 deletions src/commonMain/kotlin/dev/kord/x/emoji/DiscordEmoji.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,65 @@ public enum class SkinTone(public val unicode: String) {
MediumLight("\uD83C\uDFFC"),
Light("\uD83C\uDFFB"),
Default("");
}

public companion object
public enum class HairStyle(public val unicode: String) {
RedHair("🦰"),
CurlyHair("🦱"),
WhiteHair("🦳"),
Bald("🦲"),
Default("");
}

/**
* A Unicode emoji supported by Discord's client.
*/
public sealed class DiscordEmoji {
public sealed interface DiscordEmoji {

/**
* The hex value of this emoji.
*/
public abstract val unicode: String
public val unicode: String

/**
* List of names for this emoji.
*/
public val names: List<String>

/**
* The first name for this emoji.
*/
public val name: String get() = names.first()

/**
* An emoji that supports [SkinTones][SkinTone].
*/
public data class Diverse(val code: String, val tone: SkinTone = SkinTone.Default) : DiscordEmoji() {
public fun withTone(tone: SkinTone): Diverse = copy(code = code, tone = tone)
public data class Diverse(
public val code: String,
override val names: List<String>,
val tone: SkinTone = SkinTone.Default,
val hairStyle: HairStyle = HairStyle.Default
) : DiscordEmoji {
@Deprecated("Replaced by with", ReplaceWith("with(tone = tone)"))
public fun withTone(tone: SkinTone): Diverse = copy(tone = tone)

/**
* Creates a new instance of this [DiscordEmoji] with [tone] and [hairStyle].
*/
public fun with(tone: SkinTone = this.tone, hairStyle: HairStyle = this.hairStyle): Diverse =
copy(tone = tone, hairStyle = hairStyle)

override val unicode: String
get() = "$code${tone.unicode}"
get() = buildString {
append(code)
if (tone != SkinTone.Default) {
append(tone.unicode)
}
if (hairStyle != HairStyle.Default) {
append("\u200D")
append(hairStyle.unicode)
}
}

/**
* Checks [other] to be the same emote but ignores [tone].
Expand All @@ -44,7 +81,7 @@ public sealed class DiscordEmoji {
/**
* A generic emoji that does not support [SkinTones][SkinTone].
*/
public data class Generic(override val unicode: String) : DiscordEmoji() {
public data class Generic(override val unicode: String, override val names: List<String>) : DiscordEmoji {
override fun toString(): String = unicode

override fun hashCode(): Int = unicode.hashCode()
Expand All @@ -55,6 +92,51 @@ public sealed class DiscordEmoji {
return unicode == other.unicode
}
}

public companion object {
private val byName: Map<String, DiscordEmoji> = Emojis.all.flatMap { emoji ->
emoji.names.map { name -> name to emoji }
}.toMap()
private val byUnicode: Map<String, DiscordEmoji> = Emojis.all.associateBy {
when (it) {
is Diverse -> it.code
is Generic -> it.unicode
}
}

/**
* Finds an emoji by its [unicode].
*
* @throws IllegalArgumentException if the emoji was not found
*/
public fun findByUnicode(unicode: String): DiscordEmoji =
requireNotNull(findByUnicodeOrNull(unicode)) { "Could not find emoji: $unicode" }

/**
* Finds an emoji by its [unicode] or `null`.
*/
public fun findByUnicodeOrNull(unicode: String): DiscordEmoji? {
val tone = unicode.toSkinTone() ?: SkinTone.Default
val hairStyle = unicode.toHairStyle() ?: HairStyle.Default
val withoutDiversity = unicode.removeTone().removeHairStyle()
val emoji = byUnicode[withoutDiversity]

return if (emoji is Diverse) emoji.copy(tone = tone, hairStyle = hairStyle) else emoji
}

/**
* Finds an emoji by its [name].
*
* @throws IllegalArgumentException if the emoji was not found
*/
public fun findByName(name: String): DiscordEmoji =
requireNotNull(findByNameOrNull(name)) { "Could not find emoji: $name" }

/**
* Finds an emoji by its [name] or `null`.
*/
public fun findByNameOrNull(name: String): DiscordEmoji? = byName[name]
}
}

/**
Expand Down Expand Up @@ -84,9 +166,13 @@ public fun DiscordEmoji.toReaction(): ReactionEmoji.Unicode = ReactionEmoji.Unic
*/
public fun ReactionEmoji.Companion.from(emoji: DiscordEmoji): ReactionEmoji.Unicode = emoji.toReaction()

internal fun String.toSkinTone(): SkinTone? = enumValues<SkinTone>().firstOrNull { this.endsWith(it.unicode) }
internal fun String.toSkinTone(): SkinTone? = SkinTone.entries.firstOrNull { contains(it.unicode) }
internal fun String.toHairStyle(): HairStyle? = HairStyle.entries.firstOrNull { endsWith(it.unicode) }

internal fun String.removeTone(): String = enumValues<SkinTone>().fold(this) { acc, skinTone ->
internal fun String.removeTone(): String = SkinTone.entries.fold(this) { acc, skinTone ->
acc.removeSuffix(skinTone.unicode)
}

internal fun String.removeHairStyle(): String = HairStyle.entries.fold(this) { acc, hairStyle ->
acc.removeSuffix(hairStyle.unicode)
}
Loading

0 comments on commit ff60427

Please sign in to comment.