From 3c5958b9f5c0a2d3ad2f1b02c4574fab3ebd7eae Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski <janne@kuschku.de> Date: Thu, 3 Mar 2022 19:16:41 +0100 Subject: [PATCH] feat: implement irc format parsing --- .../quasseldroid/util/format/IrcFormat.kt | 59 ++ .../util/format/IrcFormatDeserializer.kt | 156 +++++ .../util/format/IrcFormatRenderer.kt | 62 ++ .../util/irc/IrcFormatDeserializerTest.kt | 632 ++++++++++++++++++ 4 files changed, 909 insertions(+) create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormat.kt create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormatDeserializer.kt create mode 100644 app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormatRenderer.kt create mode 100644 app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/IrcFormatDeserializerTest.kt diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormat.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormat.kt new file mode 100644 index 000000000..063858668 --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormat.kt @@ -0,0 +1,59 @@ +package de.justjanne.quasseldroid.util.format + +import de.justjanne.quasseldroid.util.extensions.joinString +import androidx.compose.ui.graphics.Color as AndroidColor + +object IrcFormat { + data class Span( + val content: String, + val style: Style = Style() + ) { + override fun toString(): String = joinString(", ", "Info(", ")") { + append(content) + if (style != Style()) { + append("style=$style") + } + } + } + + data class Style( + val flags: Set<Flag> = emptySet(), + val foreground: Color? = null, + val background: Color? = null, + ) { + fun flipFlag(flag: Flag) = copy( + flags = if (flags.contains(flag)) flags - flag else flags + flag + ) + + override fun toString(): String = joinString(", ", "Info(", ")") { + if (flags.isNotEmpty()) { + append("flags=$flags") + } + if (foreground != null) { + append("foreground=$foreground") + } + if (background != null) { + append("background=$background") + } + } + } + + sealed class Color { + data class Mirc(val index: Int) : Color() { + override fun toString(): String = "Mirc($index)" + } + + data class Hex(val color: AndroidColor) : Color() { + override fun toString(): String = "Hex(#${color.value.toString(16)})" + } + } + + enum class Flag { + BOLD, + ITALIC, + UNDERLINE, + STRIKETHROUGH, + MONOSPACE, + INVERSE + } +} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormatDeserializer.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormatDeserializer.kt new file mode 100644 index 000000000..bb2a11d7f --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormatDeserializer.kt @@ -0,0 +1,156 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2022 Janne Mareike Koschinski + * Copyright (c) 2022 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.justjanne.quasseldroid.util.format + +import androidx.compose.ui.graphics.Color +import kotlin.math.min + +/** + * A helper class to turn mIRC formatted Strings into Android’s SpannableStrings with the same + * color and format codes + */ +object IrcFormatDeserializer { + + fun parse(content: String) = sequence { + var i = 0 + var lastProcessed = 0 + var current = IrcFormat.Style() + + suspend fun SequenceScope<IrcFormat.Span>.emit() { + if (lastProcessed != i) { + yield(IrcFormat.Span(content.substring(lastProcessed, i), current)) + lastProcessed = i + } + } + + suspend fun SequenceScope<IrcFormat.Span>.processFlag(flag: IrcFormat.Flag) { + emit() + current = current.flipFlag(flag) + lastProcessed = ++i + } + + suspend fun SequenceScope<IrcFormat.Span>.processColor( + length: Int, + radix: Int = 10, + range: IntRange? = null, + matcher: (Char) -> Boolean + ): Pair<Int, Int?>? { + emit() + + // Skip Color Code + lastProcessed = ++i + + val foregroundData = content.substring(i, min(i + length, content.length)) + .takeWhile(matcher) + val foreground = foregroundData.toIntOrNull(radix) + ?.takeIf { range == null || it in range } + ?: return null + + // Skip foreground + i += foregroundData.length + + val backgroundData = + if (content[i] == ',') content.substring(i + 1, min(i + length + 1, content.length)) + .takeWhile(matcher) + else null + val background = backgroundData + ?.toIntOrNull(radix) + ?.takeIf { range == null || it in range } + + if (background != null) { + // Skip background and separator + i += backgroundData.length + 1 + } + + lastProcessed = i + + return Pair(foreground, background) + } + + + while (i < content.length) { + when (content[i]) { + CODE_BOLD -> processFlag(IrcFormat.Flag.BOLD) + CODE_ITALIC -> processFlag(IrcFormat.Flag.ITALIC) + CODE_UNDERLINE -> processFlag(IrcFormat.Flag.UNDERLINE) + CODE_STRIKETHROUGH -> processFlag(IrcFormat.Flag.STRIKETHROUGH) + CODE_MONOSPACE -> processFlag(IrcFormat.Flag.MONOSPACE) + CODE_SWAP, CODE_SWAP_KVIRC -> processFlag(IrcFormat.Flag.INVERSE) + CODE_COLOR -> { + val color = processColor(length = 2, range = 0..99) { + it in '0'..'9' + } + + current = if (color == null) { + current.copy(foreground = null, background = null) + } else { + val (foreground, background) = color + current.copy( + foreground = foreground.takeUnless { it == 99 }?.let { IrcFormat.Color.Mirc(it) }, + background = if (background == null) current.background + else background.takeUnless { it == 99 }?.let { IrcFormat.Color.Mirc(it) } + ) + } + } + CODE_HEXCOLOR -> { + val color = processColor(length = 6, radix = 16) { + it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' + } + + current = if (color == null) { + current.copy(foreground = null, background = null) + } else { + val (foreground, background) = color + current.copy( + foreground = IrcFormat.Color.Hex(Color(foreground).copy(alpha = 1f)), + background = background?.let { + IrcFormat.Color.Hex(Color(it).copy(alpha = 1f)) + } ?: current.background + ) + } + } + CODE_RESET -> { + emit() + current = IrcFormat.Style() + lastProcessed = ++i + } + else -> { + // Regular Character + i++ + } + } + } + + if (lastProcessed != content.length) { + yield(IrcFormat.Span(content.substring(lastProcessed), current)) + } + } + + private const val CODE_BOLD = 0x02.toChar() + private const val CODE_COLOR = 0x03.toChar() + private const val CODE_HEXCOLOR = 0x04.toChar() + private const val CODE_ITALIC = 0x1D.toChar() + private const val CODE_UNDERLINE = 0x1F.toChar() + private const val CODE_STRIKETHROUGH = 0x1E.toChar() + private const val CODE_MONOSPACE = 0x11.toChar() + private const val CODE_SWAP_KVIRC = 0x12.toChar() + private const val CODE_SWAP = 0x16.toChar() + private const val CODE_RESET = 0x0F.toChar() +} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormatRenderer.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormatRenderer.kt new file mode 100644 index 000000000..2cc81b47e --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/IrcFormatRenderer.kt @@ -0,0 +1,62 @@ +package de.justjanne.quasseldroid.util.format + +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import de.justjanne.quasseldroid.ui.theme.QuasselTheme + +object IrcFormatRenderer { + @Composable + fun render( + data: Sequence<IrcFormat.Span>, + // Use default color of Text composable if none set + textColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + // Use default color of surface if none set + backgroundColor: Color = MaterialTheme.colors.surface + ) = buildAnnotatedString { + for (block in data) { + pushStyle(buildSpan(block.style, textColor, backgroundColor)) + append(block.content) + pop() + } + } + + @Composable + private fun toColor(color: IrcFormat.Color?): Color? = when (color) { + null -> null + is IrcFormat.Color.Mirc -> QuasselTheme.mirc.colors[color.index] + is IrcFormat.Color.Hex -> color.color + } + + @Composable + private fun buildSpan( + style: IrcFormat.Style, + textColor: Color, + backgroundColor: Color + ): SpanStyle { + val foreground = toColor(style.foreground) ?: textColor + val background = toColor(style.background) ?: backgroundColor + + return SpanStyle( + fontWeight = if (style.flags.contains(IrcFormat.Flag.BOLD)) FontWeight.Bold else FontWeight.Normal, + fontStyle = if (style.flags.contains(IrcFormat.Flag.ITALIC)) FontStyle.Italic else FontStyle.Normal, + textDecoration = TextDecoration.combine( + listOfNotNull( + if (style.flags.contains(IrcFormat.Flag.STRIKETHROUGH)) TextDecoration.LineThrough else null, + if (style.flags.contains(IrcFormat.Flag.UNDERLINE)) TextDecoration.Underline else null + ) + ), + fontFamily = if (style.flags.contains(IrcFormat.Flag.MONOSPACE)) FontFamily.Monospace else null, + color = if (style.flags.contains(IrcFormat.Flag.INVERSE)) background else foreground, + background = if (style.flags.contains(IrcFormat.Flag.INVERSE)) foreground else background, + ) + } +} diff --git a/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/IrcFormatDeserializerTest.kt b/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/IrcFormatDeserializerTest.kt new file mode 100644 index 000000000..c94ce7bf4 --- /dev/null +++ b/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/IrcFormatDeserializerTest.kt @@ -0,0 +1,632 @@ +package de.kuschku.justjanne.quasseldroid.util.irc + +import androidx.compose.ui.graphics.Color +import de.justjanne.quasseldroid.util.format.IrcFormat +import de.justjanne.quasseldroid.util.format.IrcFormatDeserializer +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class IrcFormatDeserializerTest { + @Test + fun testBroken() { + assertEquals( + emptyList(), + IrcFormatDeserializer.parse( + "\u000f" + ).toList() + ) + + assertEquals( + emptyList(), + IrcFormatDeserializer.parse( + "\u0003\u000f" + ).toList() + ) + assertEquals( + listOf( + IrcFormat.Span( + "[" + ), + IrcFormat.Span( + "hdf-us", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.ITALIC), + foreground = IrcFormat.Color.Mirc(4) + ) + ), + IrcFormat.Span( + "] [" + ), + IrcFormat.Span( + "nd", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(7) + ) + ), + IrcFormat.Span( + "] blah blah blah" + ), + ), + IrcFormatDeserializer.parse( + "[\u001d\u000304hdf-us\u0003\u000f] [\u000307nd\u0003] blah blah blah" + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + "New Break set to: ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(2)) + ), + IrcFormat.Span( + "Target: ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(3)) + ), + IrcFormat.Span("388 "), + IrcFormat.Span( + "| ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(2)) + ), + IrcFormat.Span( + "Type: ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(3)) + ), + IrcFormat.Span("GS | "), + IrcFormat.Span( + "Break: ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(3)) + ), + IrcFormat.Span("58,000 "), + IrcFormat.Span( + "| ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(2)) + ), + IrcFormat.Span( + "120%: ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(3)) + ), + IrcFormat.Span("48,000 | "), + IrcFormat.Span( + "135%: ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(3)) + ), + IrcFormat.Span("43,000 "), + IrcFormat.Span( + "| ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(2)) + ), + IrcFormat.Span( + "145%: ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(3)) + ), + IrcFormat.Span("40,000 "), + IrcFormat.Span( + "| ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(2)) + ), + IrcFormat.Span( + "180%: ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(3)) + ), + IrcFormat.Span("32,000"), + IrcFormat.Span( + " | ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(2)) + ), + IrcFormat.Span( + "Pop: ", + IrcFormat.Style(foreground = IrcFormat.Color.Mirc(3)) + ), + IrcFormat.Span("73819"), + ), + IrcFormatDeserializer.parse( + "\u000302New Break set to: \u000303Target: \u000399388 \u000302| \u000303Type: " + + "\u000399GS | \u000303Break: \u00039958,000 \u000302| \u000303120%: \u00039948,000 | " + + "\u000303135%: \u00039943,000 \u000302| \u000303145%: \u00039940,000 \u000302| " + + "\u000303180%: \u00039932,000\u000302 | \u000303Pop: \u00039973819\u000f" + ).toList() + ) + } + + @Test + fun testStrikethrough() { + assertEquals( + listOf( + IrcFormat.Span("Normal"), + IrcFormat.Span( + "Strikethrough", + IrcFormat.Style(flags = setOf(IrcFormat.Flag.STRIKETHROUGH)) + ), + IrcFormat.Span("Normal") + ), + IrcFormatDeserializer.parse( + "Normal\u001eStrikethrough\u001eNormal" + ).toList() + ) + } + + @Test + fun testInverse() { + assertEquals( + listOf( + IrcFormat.Span("First"), + IrcFormat.Span( + "Second", + IrcFormat.Style(flags = setOf(IrcFormat.Flag.INVERSE)) + ), + IrcFormat.Span( + "Red/Green", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.INVERSE), + foreground = IrcFormat.Color.Mirc(4), + background = IrcFormat.Color.Mirc(3) + ) + ), + IrcFormat.Span( + "Green/Red", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(4), + background = IrcFormat.Color.Mirc(3) + ) + ), + IrcFormat.Span( + "Green/Magenta", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(6), + background = IrcFormat.Color.Mirc(3) + ) + ), + IrcFormat.Span( + "Magenta/Green", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.INVERSE), + foreground = IrcFormat.Color.Mirc(6), + background = IrcFormat.Color.Mirc(3), + ) + ), + ), + IrcFormatDeserializer.parse( + "First\u0016Second\u00034,3Red/Green\u0016Green/Red\u00036Green/Magenta\u0016Magenta/Green" + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span("First"), + IrcFormat.Span( + "Second", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.INVERSE) + ) + ), + IrcFormat.Span( + "Third", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.INVERSE), + foreground = IrcFormat.Color.Mirc(2) + ) + ), + IrcFormat.Span( + "Red/Green", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(4), + background = IrcFormat.Color.Mirc(3) + ) + ), + IrcFormat.Span( + "Green/Red", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.INVERSE), + foreground = IrcFormat.Color.Mirc(4), + background = IrcFormat.Color.Mirc(3) + ) + ), + IrcFormat.Span( + "Green/Magenta", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.INVERSE), + foreground = IrcFormat.Color.Mirc(6), + background = IrcFormat.Color.Mirc(3) + ) + ), + IrcFormat.Span( + "Magenta/Green", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(6), + background = IrcFormat.Color.Mirc(3), + ) + ), + ), + IrcFormatDeserializer.parse( + "First\u0012Second\u00032Third\u0012\u00034,3Red/Green\u0012Green/Red\u00036Green/Magenta\u0016Magenta/Green" + ).toList() + ) + } + + @Test + fun testMonospace() { + assertEquals( + listOf( + IrcFormat.Span( + "test ", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(4) + ) + ), + IrcFormat.Span( + "test", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.MONOSPACE), + foreground = IrcFormat.Color.Mirc(4) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u00034test \u0011test" + ).toList() + ) + assertEquals( + listOf( + IrcFormat.Span( + "test ", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.MONOSPACE) + ) + ), + IrcFormat.Span( + "test", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.MONOSPACE), + foreground = IrcFormat.Color.Mirc(4) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u0011test \u00034test" + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span("`test "), + IrcFormat.Span( + "test`", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(4) + ) + ) + ), + IrcFormatDeserializer.parse( + "`test \u00034test`" + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + "[test ", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(4) + ) + ), + IrcFormat.Span( + "nick`name", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(4), + flags = setOf(IrcFormat.Flag.BOLD) + ) + ), + IrcFormat.Span( + "] [nick`name]", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(4) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u00034[test \u0002nick`name\u0002] [nick`name]" + ).toList() + ) + } + + @Test + fun testColors() { + assertEquals( + listOf( + IrcFormat.Span("Test 1: "), + IrcFormat.Span( + "[", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD), + foreground = IrcFormat.Color.Mirc(12) + ) + ), + IrcFormat.Span( + "6,7,3,9,10,4,8,10,5", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(6) + ) + ), + IrcFormat.Span( + "]", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD), + foreground = IrcFormat.Color.Mirc(12) + ) + ), + IrcFormat.Span(" "), + IrcFormat.Span( + "Test2: ", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(14) + ) + ), + IrcFormat.Span( + " ", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD), + foreground = IrcFormat.Color.Mirc(14) + ) + ), + IrcFormat.Span( + "[", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD), + foreground = IrcFormat.Color.Mirc(12) + ) + ), + IrcFormat.Span("2,9"), + IrcFormat.Span( + "]", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD), + foreground = IrcFormat.Color.Mirc(12) + ) + ), + ), + IrcFormatDeserializer.parse( + "Test 1: \u0002\u000312[\u00036\u00026,7,3,9,10,4,8,10,5\u0002\u000312]"+ + "\u0003\u0002 \u000314Test2: \u0002 \u000312[\u0003\u00022,9\u0002\u000312]\u0003\u0002" + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + "Extended colors", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(55), + background = IrcFormat.Color.Mirc(25) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u000355,25Extended colors\u0003" + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + "Transparent extended colors", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD, IrcFormat.Flag.UNDERLINE), + foreground = IrcFormat.Color.Mirc(55), + background = IrcFormat.Color.Mirc(25) + ) + ), + IrcFormat.Span( + " cleared fg", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD, IrcFormat.Flag.UNDERLINE), + background = IrcFormat.Color.Mirc(25) + ) + ), + IrcFormat.Span( + " cleared bg", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD, IrcFormat.Flag.UNDERLINE), + foreground = IrcFormat.Color.Mirc(55) + ) + ), + IrcFormat.Span( + " cleared both", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD, IrcFormat.Flag.UNDERLINE) + ) + ), + IrcFormat.Span( + " cleared bold", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.UNDERLINE) + ) + ), + IrcFormat.Span(" cleared all") + ), + IrcFormatDeserializer.parse( + "\u001f\u0002\u000355,25Transparent extended colors\u000399,25 cleared fg\u000355,99 cleared bg\u000399,99 cleared both\u0002 cleared bold\u000f cleared all", + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + "Sniper_ShooterCZ", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD), + foreground = IrcFormat.Color.Mirc(0), + background = IrcFormat.Color.Mirc(1) + ) + ), + IrcFormat.Span( + "(1)", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(0), + background = IrcFormat.Color.Mirc(1) + ) + ), + IrcFormat.Span(":"), + IrcFormat.Span( + " kokote", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(2) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u00030,1\u0002Sniper_ShooterCZ\u0002(1)\u000f:\u00032 kokote" + ).toList() + ) + } + + val futureTests = listOf( + // Colors + "\u000309uncurry\u000f \u000312Vect\u000f : \u000312(\u000f\u000312Nat\u000f\u000312,\u000f \u000312Type\u000f\u000312)\u000f -> \u000312Type\u000f", + "*** (\u0002ACTIVITIES\u000f): Mugging: \u000303,03|\u000303,03|\u000303,03|\u000303,03|\u000303,03|\u000303,03|\u000304,04|\u000304,04|\u000300,043\u000300,044\u000300,04%\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000300,04\u000f | [under dev] Piracy: \u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000300,040\u000300,04.\u000300,049\u000300,04%\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000304,04|\u000300,04\u000f (exploring) | At this rate, you will get: Fined", + "\u000308\\u000308 \u000310\\u000310 \u0002\\u0002 \u000304\\u000304 \u0002\\u0002 \u000309\\u000309 \u0002\\u0002 \u0002\\u0002", + "\u000310teal\u0002boldteal\u000304boldred\u0002red", + "\u00033The channel for help with general IRC things such as \u0002\u000313clients\u0002\u00033, \u0002\u00037BNCs\u0002\u00033, \u0002\u00034bots\u0002\u00033, \u0002\u00036scripting\u0002 \u00033etc.", + "\u0002\u000310hi \u0002hola", + "\u000310\u0002hi \u0003hola", + "\u0002\u000310h\u00034i \u0002hola", + "\u00034,4__\u00033,0(\u00038,0✰\u00033,0)\u00032,2__\u00030,1 \u0002Ejercito Paraguayo\u0002 \u00034,4__\u00033,0(\u00038,0✰\u00033,0)\u00032,2__\u00031\u0003***** Lord Commander: mdmg - Sub-Comandantes: Sgto_Galleta ***** \u00030,4 Vencer o Morir!!! Que alguien pase una nueva xd\u0003 http://i.imgur.com/bTWzTuA.jpg", + "\u00034\u000f", + "\u00034\u000fhello", + "\u00031", + "\u000304\u0002>bold\u0002\u0003test", + "\u00037P\u000flayers\u00037(\u0003141/12\u00037)\u000f \u000315Kenzi\u0003 \u00037C\u000furrent votewinner: none | ?? help for all the commands | www.no1gaming.eu | #no1", + "First \u00034Red \u00033Green\u0003\u0002 Bold\u0002\u000f", + "First \u00034Color\u0003\u0002 Bold\u0002 unnecessary:\u0003 \u00034Color\u0003\u0000 plain \u00034Color\u0003\u000f \u0002Bold\u000f \u00034No space color\u0003\u00034 New color\u000f", + "DALnet's recommended mIRC scripting & bot help channel. \u00034Visit us at \u001fwww.dalnethelpdesk.com\u000f for \u0003scripting info, forums, and searchable logs/stats \u000312Looking for a script/bot/addon?\u000f \u0002\u001fmircscripts.org\u000f \u00034or\u000f \u0002\u001fmirc.net\u000f \u000312 Writing your own?\u0003\u00034 Ask \u0002here.\u0002 \u000f - \u000312m\u00034IR\u00038Casdsaa\u0003asdasd\u000f v7.14 has been released", + "\u0002irccloud:\u0002 \u000307master\u0003 \u000303James Wheare\u0003 * \u000287ebfc3\u0002 (1 files in 1 dirs): hidden_host_set formatting - http://example.com/aaaa", + "\u001fStuff.Stuff.123.123.-WOOT\u001f", + "\u00039http://www.google.com/intl/en/about.html\u0003 asdsa http://www.google.com/intl/en/about.html asdasd", + "\u00039http://www.google.com/intl/en/about.html\u0003 asdsa http://www.google.com/intl/en/about.html asdasd", + "\u001fhttp://www.google.com/intl/en/about.html\u001f asda http://www.google.com/intl/en/about.html asdasd", + "\u0002http://www.google.com/intl/en/about.html\u0002", + "\u00036Stuff.Stuff.123.123.Stuff.Stuff.12345.1234.Stuff-TEST\u0003", + " \u0002\u00034,1 [\u000307Test Title\u00034]\u0003 \u0000blah.hah.hah.and.a.bottle.123.of.123456.0.RUM \u00034,1[\u000307hi/there\u00034]\u000f ", + "\u00034,1h\u000f\u00034,2#\u000f\u00034,3l\u000f\u00034,4l\u000f\u00034,5o\u000f", + "\u0002\u000312http://www.site.com/\u000f", + "i was last seen \\ \\\u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00037test^\u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00037._\u00031 \u00031 \u00031 \u000314 '--' \u000312'-.\\__/ \u000314_\u000312l\u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00034\\\u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00034\\\u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u000313||\u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00037/\u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00034\u0002test\u0002\u00031 \u00031 \u00037^\u00031 \u00031 \u00031 \u00031 \u00039)\u000314\\\u000315((((\u00037\\\u00031 \u00031 \u00031 \u00037.\u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00038 :;;,,\u00034'-._\u00031 \u00031 \u00034\\\u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031 \u00031", + ) + + @Test + fun testHexColors() { + assertEquals( + listOf( + IrcFormat.Span( + "some text in 55ee22 rgb", + IrcFormat.Style( + foreground = IrcFormat.Color.Hex(Color(0xff55ee22)) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u000455ee22some text in 55ee22 rgb\u0004" + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + ",some text in 55ee22 rgb", + IrcFormat.Style( + foreground = IrcFormat.Color.Hex(Color(0xff55ee22)) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u000455ee22,some text in 55ee22 rgb\u0004" + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + "some text in 55ee22 rgb on aaaaaa bg", + IrcFormat.Style( + foreground = IrcFormat.Color.Hex(Color(0xff55ee22)), + background = IrcFormat.Color.Hex(Color(0xffaaaaaa)) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u000455ee22,aaaaaasome text in 55ee22 rgb on aaaaaa bg\u0004" + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + ",some text in 55ee22 rgb on aaaaaa bg", + IrcFormat.Style( + foreground = IrcFormat.Color.Hex(Color(0xff55ee22)), + background = IrcFormat.Color.Hex(Color(0xffaaaaaa)) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u000455ee22,aaaaaa,some text in 55ee22 rgb on aaaaaa bg\u0004", + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + ",some text in 55ee22 rgb on aaaaaa bg", + IrcFormat.Style( + foreground = IrcFormat.Color.Hex(Color(0xff55ee22)), + background = IrcFormat.Color.Hex(Color(0xffaaaaaa)) + ) + ), + IrcFormat.Span( + " Bold", + IrcFormat.Style( + flags = setOf(IrcFormat.Flag.BOLD) + ) + ) + ), + IrcFormatDeserializer.parse( + "\u000455ee22,aaaaaa,some text in 55ee22 rgb on aaaaaa bg\u0004\u0002 Bold\u0002", + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + ",some text in 55ee22 rgb on aaaaaa bg", + IrcFormat.Style( + foreground = IrcFormat.Color.Hex(Color(0xff55ee22)), + background = IrcFormat.Color.Hex(Color(0xffaaaaaa)) + ) + ), + IrcFormat.Span("\u0000") + ), + IrcFormatDeserializer.parse( + "\u000455ee22,aaaaaa,some text in 55ee22 rgb on aaaaaa bg\u0004\u0000", + ).toList() + ) + + assertEquals( + listOf( + IrcFormat.Span( + ",some text in 55ee22 rgb on aaaaaa bg", + IrcFormat.Style( + foreground = IrcFormat.Color.Hex(Color(0xff55ee22)), + background = IrcFormat.Color.Hex(Color(0xffaaaaaa)) + ) + ), + IrcFormat.Span( + " Red", + IrcFormat.Style( + foreground = IrcFormat.Color.Mirc(4), + ) + ) + ), + IrcFormatDeserializer.parse( + "\u000455ee22,aaaaaa,some text in 55ee22 rgb on aaaaaa bg\u0004\u00034 Red\u0003", + ).toList() + ) + } +} -- GitLab