diff --git a/app/src/main/kotlin/io/sweers/catchup/ui/about/ChangelogFragment.kt b/app/src/main/kotlin/io/sweers/catchup/ui/about/ChangelogFragment.kt index 2d25fa4d0..e00d9c0a7 100644 --- a/app/src/main/kotlin/io/sweers/catchup/ui/about/ChangelogFragment.kt +++ b/app/src/main/kotlin/io/sweers/catchup/ui/about/ChangelogFragment.kt @@ -37,6 +37,8 @@ import io.sweers.catchup.R import io.sweers.catchup.R.layout import io.sweers.catchup.data.LinkManager import io.sweers.catchup.data.github.RepoReleasesQuery +import io.sweers.catchup.gemoji.EmojiMarkdownConverter +import io.sweers.catchup.gemoji.replaceMarkdownEmojisIn import io.sweers.catchup.service.api.UrlMeta import io.sweers.catchup.ui.Scrollable import io.sweers.catchup.ui.base.CatchUpItemViewHolder @@ -57,6 +59,8 @@ class ChangelogFragment : InjectableBaseFragment(), Scrollable { lateinit var apolloClient: ApolloClient @Inject internal lateinit var linkManager: LinkManager + @Inject + internal lateinit var markdownConverter: EmojiMarkdownConverter private val progressBar by bindView(R.id.progress) private val recyclerView by bindView(R.id.list) @@ -129,6 +133,12 @@ class ChangelogFragment : InjectableBaseFragment(), Scrollable { ) } } + .map { + it.copy( + name = markdownConverter.replaceMarkdownEmojisIn(it.name), + description = markdownConverter.replaceMarkdownEmojisIn(it.description) + ) + } .toList() } diff --git a/app/src/main/kotlin/io/sweers/catchup/ui/about/LicensesFragment.kt b/app/src/main/kotlin/io/sweers/catchup/ui/about/LicensesFragment.kt index cb647d71c..8deced38d 100644 --- a/app/src/main/kotlin/io/sweers/catchup/ui/about/LicensesFragment.kt +++ b/app/src/main/kotlin/io/sweers/catchup/ui/about/LicensesFragment.kt @@ -58,6 +58,8 @@ import io.sweers.catchup.data.github.ProjectOwnersByIdsQuery.AsUser import io.sweers.catchup.data.github.RepositoriesByIdsQuery import io.sweers.catchup.data.github.RepositoryByNameAndOwnerQuery import io.sweers.catchup.flowbinding.safeOffer +import io.sweers.catchup.gemoji.EmojiMarkdownConverter +import io.sweers.catchup.gemoji.replaceMarkdownEmojisIn import io.sweers.catchup.service.api.TemporaryScopeHolder import io.sweers.catchup.service.api.UrlMeta import io.sweers.catchup.service.api.temporaryScope @@ -116,6 +118,9 @@ class LicensesFragment : InjectableBaseFragment(), Scrollable { @Inject internal lateinit var linkManager: LinkManager + @Inject + internal lateinit var markdownConverter: EmojiMarkdownConverter + private val dimenSize by lazy { resources.getDimensionPixelSize(R.dimen.avatar) } @@ -249,6 +254,17 @@ class LicensesFragment : InjectableBaseFragment(), Scrollable { .groupBy { it.author } .sortBy { it.first } .flatMapConcat { it.second.asFlow().sortBy { it.name } } + .map { + // TODO use CopyDynamic when 0.3.0 is out + it.copy( + author = markdownConverter.replaceMarkdownEmojisIn(it.author), + name = markdownConverter.replaceMarkdownEmojisIn(it.name), + description = it.description?.let { + markdownConverter.replaceMarkdownEmojisIn(it) + } ?: it.description + ) + } + .flowOn(Dispatchers.IO) .toList() .let { val collector = mutableListOf() diff --git a/libraries/gemoji/src/main/kotlin/io/sweers/catchup/gemoji/EmojiMarkdownConverter.kt b/libraries/gemoji/src/main/kotlin/io/sweers/catchup/gemoji/EmojiMarkdownConverter.kt index 647489838..d981aff77 100644 --- a/libraries/gemoji/src/main/kotlin/io/sweers/catchup/gemoji/EmojiMarkdownConverter.kt +++ b/libraries/gemoji/src/main/kotlin/io/sweers/catchup/gemoji/EmojiMarkdownConverter.kt @@ -26,50 +26,92 @@ internal class GemojiEmojiMarkdownConverter( private val gemojiDao: GemojiDao ) : EmojiMarkdownConverter { override fun convert(alias: String): String? { - return if (alias.startsWith(':') && alias.endsWith(":")) { - gemojiDao.getEmoji(alias.substring(1, alias.lastIndex)) - } else { - null - } + return gemojiDao.getEmoji(alias) + } +} + +fun Sequence.asString(): String { + return buildString { + this@asString.forEach { append(it) } + } +} + +fun Sequence.asString(capacity: Int): String { + return buildString(capacity) { + this@asString.forEach { append(it) } } } /** * Returns a [String] that replaces occurrences of markdown emojis with android render-able emojis. */ -fun replaceMarkdownEmojis(markdown: String, converter: EmojiMarkdownConverter): String { - val sb = StringBuilder(markdown.length) - var potentialAliasStart = -1 +fun EmojiMarkdownConverter.replaceMarkdownEmojisIn(markdown: String): String { + return replaceMarkdownEmojisIn(markdown.asSequence()).asString(markdown.length) +} + +/** + * This is the longest possible alias length, so we can use its length for our aliasBuilder var + * below to reuse it and never have to resize it. + */ +private const val MAX_ALIAS_LENGTH = "south_georgia_south_sandwich_islands".length - markdown.forEachIndexed { index, char -> - if (char == ':') { - potentialAliasStart = if (potentialAliasStart == -1) { - // If we have no potential start, any : is a potential start - index +/** + * Returns a [Sequence][Sequence] that replaces occurrences of markdown emojis with android + * render-able emojis. + */ +fun EmojiMarkdownConverter.replaceMarkdownEmojisIn(markdown: Sequence): Sequence { + val aliasBuilder = StringBuilder(MAX_ALIAS_LENGTH) + var startAlias = false + return sequence { + markdown.forEach { char -> + if (startAlias || aliasBuilder.isNotEmpty()) { + if (startAlias && char == ':') { + // Double ::, so emit a colon and keep startAlias set + yield(':') + return@forEach + } + startAlias = false + when (char) { + ' ' -> { + // Aliases can't have spaces, so bomb out and restart + yield(':') + yieldAll(aliasBuilder.asSequence()) + yield(' ') + aliasBuilder.setLength(0) + } + ':' -> { + val potentialAlias = aliasBuilder.toString() + val potentialEmoji = convert(potentialAlias) + // If we find an emoji append it and reset alias start, if we don't find an emoji + // append between the potential start and this index *and* consider this index the new + // potential start. + if (potentialEmoji != null) { + yieldAll(potentialEmoji.asSequence()) + } else { + yield(':') + yieldAll(potentialAlias.asSequence()) + // Start a new alias from this colon as we didn't have a match with the existing close + startAlias = true + } + aliasBuilder.setLength(0) + } + else -> aliasBuilder.append(char) + } } else { - val potentialAlias = markdown.substring(potentialAliasStart, index + 1) - val potentialEmoji = converter.convert(potentialAlias) - // If we find an emoji append it and reset alias start, if we don't find an emoji - // append between the potential start and this index *and* consider this index the new - // potential start. - if (potentialEmoji != null) { - sb.append(potentialEmoji) - -1 + if (char == ':') { + startAlias = true } else { - sb.append(markdown, potentialAliasStart, index) - index + yield(char) } } - // While not looking for an alias end append all non possible alias chars to the string - } else if (potentialAliasStart == -1) { - sb.append(char) } - } - // Finished iterating markdown while looking for an end, append anything remaining - if (potentialAliasStart != -1) { - sb.append(markdown, potentialAliasStart, markdown.length) + // If we started an alias but ran out of characters, flush it + if (startAlias) { + yield(':') + } else if (aliasBuilder.isNotEmpty()) { + yield(':') + yieldAll(aliasBuilder.asSequence()) + } } - - return sb.toString() } diff --git a/libraries/gemoji/src/test/java/io/sweers/catchup/gemoji/EmojiMarkdownConverterTest.kt b/libraries/gemoji/src/test/java/io/sweers/catchup/gemoji/EmojiMarkdownConverterTest.kt index b4b559791..1fd843aa7 100644 --- a/libraries/gemoji/src/test/java/io/sweers/catchup/gemoji/EmojiMarkdownConverterTest.kt +++ b/libraries/gemoji/src/test/java/io/sweers/catchup/gemoji/EmojiMarkdownConverterTest.kt @@ -20,15 +20,18 @@ import org.junit.Test class EmojiMarkdownConverterTest { - val replaced = "replaced" - val emoji = ":emoji:" - val converter = object : EmojiMarkdownConverter { - override fun convert(alias: String) = if (alias == ":emoji:") replaced else null + companion object { + const val replaced = "replaced" + const val emoji = ":emoji:" + } + + private val converter = object : EmojiMarkdownConverter { + override fun convert(alias: String) = if (alias == "emoji") replaced else null } @Test fun testEmpty() { - val converted = replaceMarkdownEmojis("", converter) + val converted = converter.replaceMarkdownEmojisIn("") assertThat(converted).isEmpty() } @@ -92,5 +95,5 @@ class EmojiMarkdownConverterTest { assertThat(converted).isEqualTo("$replaced:notEmoji:$replaced:") } - private fun convert(markdown: String) = replaceMarkdownEmojis(markdown, converter) + private fun convert(markdown: String) = converter.replaceMarkdownEmojisIn(markdown) } diff --git a/scripts/gemoji/gemoji.py b/scripts/gemoji/gemoji.py index 82d247d4e..23a1fccd3 100644 --- a/scripts/gemoji/gemoji.py +++ b/scripts/gemoji/gemoji.py @@ -43,3 +43,7 @@ connection.commit() connection.close() + +print("Longest alias") +longestAlias = max([alias for alias in [max(gemoji['aliases'], key=len) for gemoji in gemojis]], key=len) +print(longestAlias) diff --git a/services/github/src/main/kotlin/io/sweers/catchup/service/github/GitHubService.kt b/services/github/src/main/kotlin/io/sweers/catchup/service/github/GitHubService.kt index 10746aa74..a0f649bf8 100644 --- a/services/github/src/main/kotlin/io/sweers/catchup/service/github/GitHubService.kt +++ b/services/github/src/main/kotlin/io/sweers/catchup/service/github/GitHubService.kt @@ -30,7 +30,7 @@ import dagger.multibindings.IntoMap import io.reactivex.Observable import io.reactivex.Single import io.sweers.catchup.gemoji.EmojiMarkdownConverter -import io.sweers.catchup.gemoji.replaceMarkdownEmojis +import io.sweers.catchup.gemoji.replaceMarkdownEmojisIn import io.sweers.catchup.libraries.retrofitconverters.DecodingConverter import io.sweers.catchup.libraries.retrofitconverters.delegatingCallFactory import io.sweers.catchup.service.api.CatchUpItem @@ -140,7 +140,7 @@ internal class GitHubService @Inject constructor( .map { with(it) { val description = description - ?.let { " — ${replaceMarkdownEmojis(it, emojiMarkdownConverter.get())}" } + ?.let { " — ${emojiMarkdownConverter.get().replaceMarkdownEmojisIn(it)}" } .orEmpty() CatchUpItem(