Skip to content
Snippets Groups Projects
Verified Commit 863fbbf1 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

feat: create module for irc utils, implement irc format deserializer

parent 852b9180
No related branches found
No related tags found
No related merge requests found
Pipeline #2970 failed
......@@ -15,4 +15,4 @@ plugins {
}
group = "de.justjanne.libquassel"
version = "0.9.2"
version = "0.10.0"
/*
* 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")
}
/*
* 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
}
}
/*
* 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()
}
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
}
/*
* 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)
}
}
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()
}
This diff is collapsed.
......@@ -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 {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment