From 863fbbf1aff5770ef0f30e5142c2dac849b69df5 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Thu, 3 Mar 2022 23:46:23 +0100
Subject: [PATCH] feat: create module for irc utils, implement irc format
 deserializer

---
 build.gradle.kts                              |    2 +-
 libquassel-irc/build.gradle.kts               |   13 +
 .../de/justjanne/libquassel/irc/IrcFormat.kt  |   67 +
 .../libquassel/irc/IrcFormatDeserializer.kt   |  149 ++
 .../libquassel/irc/backport/StringJoiner.kt   |   39 +
 .../irc/extensions/SequenceExtensions.kt      |   30 +
 .../irc/extensions/StringJoinerExtensions.kt  |   12 +
 .../test/kotlin/IrcFormatDeserializerTest.kt  | 1591 +++++++++++++++++
 settings.gradle.kts                           |    6 +-
 9 files changed, 1906 insertions(+), 3 deletions(-)
 create mode 100644 libquassel-irc/build.gradle.kts
 create mode 100644 libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/IrcFormat.kt
 create mode 100644 libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/IrcFormatDeserializer.kt
 create mode 100644 libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/backport/StringJoiner.kt
 create mode 100644 libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/extensions/SequenceExtensions.kt
 create mode 100644 libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/extensions/StringJoinerExtensions.kt
 create mode 100644 libquassel-irc/src/test/kotlin/IrcFormatDeserializerTest.kt

diff --git a/build.gradle.kts b/build.gradle.kts
index c6142b2..5e37aff 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -15,4 +15,4 @@ plugins {
 }
 
 group = "de.justjanne.libquassel"
-version = "0.9.2"
+version = "0.10.0"
diff --git a/libquassel-irc/build.gradle.kts b/libquassel-irc/build.gradle.kts
new file mode 100644
index 0000000..a268c30
--- /dev/null
+++ b/libquassel-irc/build.gradle.kts
@@ -0,0 +1,13 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+plugins {
+  id("justjanne.kotlin")
+  id("justjanne.publication")
+}
diff --git a/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/IrcFormat.kt b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/IrcFormat.kt
new file mode 100644
index 0000000..d0ab78c
--- /dev/null
+++ b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/IrcFormat.kt
@@ -0,0 +1,67 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.irc
+
+import de.justjanne.libquassel.irc.extensions.joinString
+
+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: Int) : Color() {
+      override fun toString(): String = "Hex(#${color.toString(16)})"
+    }
+  }
+
+  enum class Flag {
+    BOLD,
+    ITALIC,
+    UNDERLINE,
+    STRIKETHROUGH,
+    MONOSPACE,
+    INVERSE
+  }
+}
diff --git a/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/IrcFormatDeserializer.kt b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/IrcFormatDeserializer.kt
new file mode 100644
index 0000000..8350bbc
--- /dev/null
+++ b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/IrcFormatDeserializer.kt
@@ -0,0 +1,149 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.irc
+
+import de.justjanne.libquassel.irc.extensions.collapse
+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 (i < content.length && 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(foreground),
+              background = background?.let {
+                IrcFormat.Color.Hex(it)
+              } ?: 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))
+    }
+  }.collapse { prev, current ->
+    if (prev.style == current.style) prev.copy(content = prev.content + current.content)
+    else null
+  }
+
+  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/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/backport/StringJoiner.kt b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/backport/StringJoiner.kt
new file mode 100644
index 0000000..0a0d4eb
--- /dev/null
+++ b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/backport/StringJoiner.kt
@@ -0,0 +1,39 @@
+package de.justjanne.libquassel.irc.backport
+
+import java.io.Serializable
+
+internal class StringJoiner(
+  private val delimiter: String,
+  private val prefix: String = "",
+  private val suffix: String = ""
+) : Serializable, Appendable {
+  private val builder = StringBuilder()
+
+  override fun append(data: CharSequence?, start: Int, end: Int): StringJoiner =
+    this.apply { prepareBuilder().append(data, start, end) }
+
+  override fun append(data: CharSequence?): StringJoiner =
+    this.apply { prepareBuilder().append(data) }
+
+  override fun append(data: Char): StringJoiner =
+    this.apply { prepareBuilder().append(data) }
+
+  private fun prepareBuilder(): StringBuilder = builder.apply {
+    append(if (isEmpty()) prefix else delimiter)
+  }
+
+  override fun toString(): String =
+    if (builder.isEmpty()) {
+      prefix + suffix
+    } else {
+      val length = builder.length
+      builder.append(suffix)
+      val result = builder.toString()
+      builder.setLength(length)
+      result
+    }
+
+  fun length(): Int =
+    if (builder.isEmpty()) prefix.length + suffix.length
+    else builder.length + suffix.length
+}
diff --git a/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/extensions/SequenceExtensions.kt b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/extensions/SequenceExtensions.kt
new file mode 100644
index 0000000..c761234
--- /dev/null
+++ b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/extensions/SequenceExtensions.kt
@@ -0,0 +1,30 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.irc.extensions
+
+internal fun <T> Sequence<T>.collapse(callback: (T, T) -> T?) = sequence<T> {
+  var prev: T? = null
+  for (item in iterator()) {
+    if (prev != null) {
+      val collapsed = callback(prev, item)
+      if (collapsed == null) {
+        yield(prev)
+        prev = item
+      } else {
+        prev = collapsed
+      }
+    } else {
+      prev = item
+    }
+  }
+  if (prev != null) {
+    yield(prev)
+  }
+}
diff --git a/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/extensions/StringJoinerExtensions.kt b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/extensions/StringJoinerExtensions.kt
new file mode 100644
index 0000000..315e7dd
--- /dev/null
+++ b/libquassel-irc/src/main/kotlin/de/justjanne/libquassel/irc/extensions/StringJoinerExtensions.kt
@@ -0,0 +1,12 @@
+package de.justjanne.libquassel.irc.extensions
+
+import de.justjanne.libquassel.irc.backport.StringJoiner
+
+internal inline fun joinString(
+  delimiter: String = "",
+  prefix: String = "",
+  suffix: String = "",
+  builderAction: StringJoiner.() -> Unit
+): String {
+  return StringJoiner(delimiter, prefix, suffix).apply(builderAction).toString()
+}
diff --git a/libquassel-irc/src/test/kotlin/IrcFormatDeserializerTest.kt b/libquassel-irc/src/test/kotlin/IrcFormatDeserializerTest.kt
new file mode 100644
index 0000000..d5c4803
--- /dev/null
+++ b/libquassel-irc/src/test/kotlin/IrcFormatDeserializerTest.kt
@@ -0,0 +1,1591 @@
+package de.justjanne.libquassel.irc
+
+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()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "uncurry",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(9)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          "Vect",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(12)
+          )
+        ),
+        IrcFormat.Span(" : "),
+        IrcFormat.Span(
+          "(Nat,",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(12)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          "Type)",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(12)
+          )
+        ),
+        IrcFormat.Span(" -> "),
+        IrcFormat.Span(
+          "Type",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(12)
+          )
+        )
+      ),
+      IrcFormatDeserializer.parse(
+        "\u000309uncurry\u000f \u000312Vect\u000f : \u000312(\u000f\u000312Nat\u000f\u000312," +
+          "\u000f \u000312Type\u000f\u000312)\u000f -> \u000312Type\u000f"
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span("*** ("),
+        IrcFormat.Span(
+          "ACTIVITIES",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD)
+          )
+        ),
+        IrcFormat.Span("): Mugging: "),
+        IrcFormat.Span(
+          "||||||",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3),
+            background = IrcFormat.Color.Mirc(3),
+          )
+        ),
+        IrcFormat.Span(
+          "||",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4),
+            background = IrcFormat.Color.Mirc(4),
+          )
+        ),
+        IrcFormat.Span(
+          "34%",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(0),
+            background = IrcFormat.Color.Mirc(4),
+          )
+        ),
+        IrcFormat.Span(
+          "||||||||",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4),
+            background = IrcFormat.Color.Mirc(4),
+          )
+        ),
+        IrcFormat.Span(" | [under dev] Piracy: "),
+        IrcFormat.Span(
+          "|||||||",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4),
+            background = IrcFormat.Color.Mirc(4),
+          )
+        ),
+        IrcFormat.Span(
+          "0.9%",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(0),
+            background = IrcFormat.Color.Mirc(4),
+          )
+        ),
+        IrcFormat.Span(
+          "||||||||",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4),
+            background = IrcFormat.Color.Mirc(4),
+          )
+        ),
+        IrcFormat.Span(" (exploring) | At this rate, you will get: Fined")
+      ),
+      IrcFormatDeserializer.parse(
+        "*** (\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",
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "\\u000308 ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(8)
+          )
+        ),
+        IrcFormat.Span(
+          "\\u000310 ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(10)
+          )
+        ),
+        IrcFormat.Span(
+          "\\u0002 ",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(10)
+          )
+        ),
+        IrcFormat.Span(
+          "\\u000304 ",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "\\u0002 ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "\\u000309 ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(9)
+          )
+        ),
+        IrcFormat.Span(
+          "\\u0002 ",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(9)
+          )
+        ),
+        IrcFormat.Span(
+          "\\u0002",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(9)
+          )
+        ),
+      ),
+      IrcFormatDeserializer.parse(
+        "\u000308\\u000308 \u000310\\u000310 \u0002\\u0002 \u000304\\u000304 \u0002\\u0002 " +
+          "\u000309\\u000309 \u0002\\u0002 \u0002\\u0002"
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "teal",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(10)
+          )
+        ),
+        IrcFormat.Span(
+          "boldteal",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(10)
+          )
+        ),
+        IrcFormat.Span(
+          "boldred",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "red",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        )
+      ),
+      IrcFormatDeserializer.parse(
+        "\u000310teal\u0002boldteal\u000304boldred\u0002red",
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "The channel for help with general IRC things such as ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3)
+          )
+        ),
+        IrcFormat.Span(
+          "clients",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(13)
+          )
+        ),
+        IrcFormat.Span(
+          ", ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3)
+          )
+        ),
+        IrcFormat.Span(
+          "BNCs",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span(
+          ", ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3)
+          )
+        ),
+        IrcFormat.Span(
+          "bots",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          ", ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3)
+          )
+        ),
+        IrcFormat.Span(
+          "scripting",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(6)
+          )
+        ),
+        IrcFormat.Span(
+          " ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(6)
+          )
+        ),
+        IrcFormat.Span(
+          "etc.",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3)
+          )
+        )
+      ),
+      IrcFormatDeserializer.parse(
+        "\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.",
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "hi ",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(10)
+          )
+        ),
+        IrcFormat.Span(
+          "hola",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(10)
+          )
+        )
+      ),
+      IrcFormatDeserializer.parse(
+        "\u0002\u000310hi \u0002hola"
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "hi ",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(10)
+          )
+        ),
+        IrcFormat.Span(
+          "hola",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD)
+          )
+        )
+      ),
+      IrcFormatDeserializer.parse(
+        "\u000310\u0002hi \u0003hola"
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "h",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(10)
+          )
+        ),
+        IrcFormat.Span(
+          "i ",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "hola",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        )
+      ),
+      IrcFormatDeserializer.parse(
+        "\u0002\u000310h\u00034i \u0002hola"
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "__",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4),
+            background = IrcFormat.Color.Mirc(4),
+          )
+        ),
+        IrcFormat.Span(
+          "(",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3),
+            background = IrcFormat.Color.Mirc(0),
+          )
+        ),
+        IrcFormat.Span(
+          "✰",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(8),
+            background = IrcFormat.Color.Mirc(0),
+          )
+        ),
+        IrcFormat.Span(
+          ")",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3),
+            background = IrcFormat.Color.Mirc(0),
+          )
+        ),
+        IrcFormat.Span(
+          "__",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(2),
+            background = IrcFormat.Color.Mirc(2),
+          )
+        ),
+        IrcFormat.Span(
+          " ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(0),
+            background = IrcFormat.Color.Mirc(1),
+          )
+        ),
+        IrcFormat.Span(
+          "Ejercito Paraguayo",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(0),
+            background = IrcFormat.Color.Mirc(1),
+          )
+        ),
+        IrcFormat.Span(
+          " ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(0),
+            background = IrcFormat.Color.Mirc(1),
+          )
+        ),
+        IrcFormat.Span(
+          "__",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4),
+            background = IrcFormat.Color.Mirc(4),
+          )
+        ),
+        IrcFormat.Span(
+          "(",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3),
+            background = IrcFormat.Color.Mirc(0),
+          )
+        ),
+        IrcFormat.Span(
+          "✰",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(8),
+            background = IrcFormat.Color.Mirc(0),
+          )
+        ),
+        IrcFormat.Span(
+          ")",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3),
+            background = IrcFormat.Color.Mirc(0),
+          )
+        ),
+        IrcFormat.Span(
+          "__",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(2),
+            background = IrcFormat.Color.Mirc(2),
+          )
+        ),
+        IrcFormat.Span("***** Lord Commander: mdmg - Sub-Comandantes: Sgto_Galleta ***** "),
+        IrcFormat.Span(
+          " Vencer o Morir!!!  Que alguien pase una nueva xd",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(0),
+            background = IrcFormat.Color.Mirc(4),
+          )
+        ),
+        IrcFormat.Span(" https://i.imgur.com/bTWzTuA.jpg"),
+      ),
+      IrcFormatDeserializer.parse(
+        "\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 https://i.imgur.com/bTWzTuA.jpg"
+      ).toList()
+    )
+
+    assertEquals(
+      emptyList(),
+      IrcFormatDeserializer.parse(
+        "\u00034\u000f"
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span("hello")
+      ),
+      IrcFormatDeserializer.parse(
+        "\u00034\u000fhello"
+      ).toList()
+    )
+
+    assertEquals(
+      emptyList(),
+      IrcFormatDeserializer.parse(
+        "\u00031"
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          ">bold",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span("test")
+      ),
+      IrcFormatDeserializer.parse(
+        "\u000304\u0002>bold\u0002\u0003test"
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "P",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span("layers"),
+        IrcFormat.Span(
+          "(",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span(
+          "1/12",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(14)
+          )
+        ),
+        IrcFormat.Span(
+          ")",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          "Kenzi",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(15)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          "C",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span("urrent votewinner: none | ?? help for all the commands | www.no1gaming.eu | #no1"),
+      ),
+      IrcFormatDeserializer.parse(
+        "\u00037P\u000flayers\u00037(\u0003141/12\u00037)\u000f \u000315Kenzi\u0003 \u00037C" +
+          "\u000furrent votewinner: none | ?? help for all the commands | www.no1gaming.eu | #no1",
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span("First "),
+        IrcFormat.Span(
+          "Red ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "Green",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(3)
+          )
+        ),
+        IrcFormat.Span(
+          " Bold",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD)
+          )
+        )
+      ),
+      IrcFormatDeserializer.parse(
+        "First \u00034Red \u00033Green\u0003\u0002 Bold\u0002\u000f",
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span("First "),
+        IrcFormat.Span(
+          "Color",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          " Bold",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD)
+          )
+        ),
+        IrcFormat.Span(" unnecessary: "),
+        IrcFormat.Span(
+          "Color",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span("\u0000 plain "),
+        IrcFormat.Span(
+          "Color",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          "Bold",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          "No space color New color",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+      ),
+      IrcFormatDeserializer.parse(
+        "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",
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span("DALnet's recommended mIRC scripting & bot help channel. "),
+        IrcFormat.Span(
+          "Visit us at ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "www.dalnethelpdesk.com",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.UNDERLINE),
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(" for scripting info, forums, and searchable logs/stats "),
+        IrcFormat.Span(
+          "Looking for a script/bot/addon?",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(12)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          "mircscripts.org",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD, IrcFormat.Flag.UNDERLINE)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          "or",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          "mirc.net",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD, IrcFormat.Flag.UNDERLINE)
+          )
+        ),
+        IrcFormat.Span(" "),
+        IrcFormat.Span(
+          " Writing your own?",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(12)
+          )
+        ),
+        IrcFormat.Span(
+          " Ask ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "here.",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD),
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          " ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(" - "),
+        IrcFormat.Span(
+          "m",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(12)
+          )
+        ),
+        IrcFormat.Span(
+          "IR",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "Casdsaa",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(8)
+          )
+        ),
+        IrcFormat.Span("asdasd v7.14 has been released")
+      ),
+      IrcFormatDeserializer.parse(
+        "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",
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span("i was last seen \\ \\"),
+        IrcFormat.Span(
+          "             ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          "test^",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span(
+          "      ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          "._",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span(
+          "   ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          " '--' ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(14)
+          )
+        ),
+        IrcFormat.Span(
+          "'-.\\__/ ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(12)
+          )
+        ),
+        IrcFormat.Span(
+          "_",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(14)
+          )
+        ),
+        IrcFormat.Span(
+          "l",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(12)
+          )
+        ),
+        IrcFormat.Span(
+          "       ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          "\\",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "        ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          "\\",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "      ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          "||",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(13)
+          )
+        ),
+        IrcFormat.Span(
+          "       ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          "/",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span(
+          "       ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          "test",
+          IrcFormat.Style(
+            flags = setOf(IrcFormat.Flag.BOLD), foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "  ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          "^",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span(
+          "    ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          ")",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(9)
+          )
+        ),
+        IrcFormat.Span(
+          "\\",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(14)
+          )
+        ),
+        IrcFormat.Span(
+          "((((",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(15)
+          )
+        ),
+        IrcFormat.Span(
+          "\\",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span(
+          "   ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          ".",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(7)
+          )
+        ),
+        IrcFormat.Span(
+          "            ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          " :;;,,",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(8)
+          )
+        ),
+        IrcFormat.Span(
+          "'-._",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "  ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+        IrcFormat.Span(
+          "\\",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(4)
+          )
+        ),
+        IrcFormat.Span(
+          "                        ",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Mirc(1)
+          )
+        ),
+      ),
+      IrcFormatDeserializer.parse(
+        "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"
+      ).toList()
+    )
+  }
+
+  @Test
+  fun testHexColors() {
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          "some text in 55ee22 rgb",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Hex(0x55ee22)
+          )
+        )
+      ),
+      IrcFormatDeserializer.parse(
+        "\u000455ee22some text in 55ee22 rgb\u0004"
+      ).toList()
+    )
+
+    assertEquals(
+      listOf(
+        IrcFormat.Span(
+          ",some text in 55ee22 rgb",
+          IrcFormat.Style(
+            foreground = IrcFormat.Color.Hex(0x55ee22)
+          )
+        )
+      ),
+      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(0x55ee22),
+            background = IrcFormat.Color.Hex(0xaaaaaa)
+          )
+        )
+      ),
+      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(0x55ee22),
+            background = IrcFormat.Color.Hex(0xaaaaaa)
+          )
+        )
+      ),
+      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(0x55ee22),
+            background = IrcFormat.Color.Hex(0xaaaaaa)
+          )
+        ),
+        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(0x55ee22),
+            background = IrcFormat.Color.Hex(0xaaaaaa)
+          )
+        ),
+        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(0x55ee22),
+            background = IrcFormat.Color.Hex(0xaaaaaa)
+          )
+        ),
+        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()
+    )
+  }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 45551fe..c1a227a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -6,6 +6,7 @@
  * v. 2.0. If a copy of the MPL was not distributed with this file, You can
  * obtain one at https://mozilla.org/MPL/2.0/.
  */
+
 enableFeaturePreview("VERSION_CATALOGS")
 
 rootProject.name = "libquassel"
@@ -14,9 +15,10 @@ includeBuild("gradle/convention")
 
 include(
   ":libquassel-annotations",
-  ":libquassel-protocol",
+  ":libquassel-client",
   ":libquassel-generator",
-  ":libquassel-client"
+  ":libquassel-irc",
+  ":libquassel-protocol"
 )
 
 pluginManagement {
-- 
GitLab