aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/ComponentMatcherUtils.kt149
-rw-r--r--src/test/java/at/hannibal2/skyhanni/test/ComponentSpanTest.kt53
2 files changed, 202 insertions, 0 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/ComponentMatcherUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/ComponentMatcherUtils.kt
new file mode 100644
index 000000000..bd5df2c8d
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/ComponentMatcherUtils.kt
@@ -0,0 +1,149 @@
+package at.hannibal2.skyhanni.utils
+
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.ChatStyle
+import net.minecraft.util.IChatComponent
+import java.util.Stack
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+object ComponentMatcherUtils {
+
+ fun IChatComponent.intoSpan(): ComponentSpan {
+ val text = this.unformattedText
+ return ComponentSpan(
+ this,
+ text,
+ 0,
+ text.length
+ )
+ }
+
+ fun Pattern.styledMatcher(span: ComponentSpan): ComponentMatcher {
+ val matcher = matcher(span.getText())
+ return ComponentMatcher(matcher, span)
+ }
+
+ inline fun <T> Pattern.matchStyledMatcher(chat: IChatComponent, consumer: ComponentMatcher.() -> T) =
+ matchStyledMatcher(chat.intoSpan(), consumer)
+
+ inline fun <T> Pattern.matchStyledMatcher(span: ComponentSpan, consumer: ComponentMatcher.() -> T) =
+ styledMatcher(span).let { if (it.matches()) consumer(it) else null }
+
+ inline fun <T> Pattern.findStyledMatcher(chat: IChatComponent, consumer: ComponentMatcher.() -> T) =
+ findStyledMatcher(chat.intoSpan(), consumer)
+
+ inline fun <T> Pattern.findStyledMatcher(span: ComponentSpan, consumer: ComponentMatcher.() -> T) =
+ styledMatcher(span).let { if (it.find()) consumer(it) else null }
+}
+
+class ComponentMatcher internal constructor(
+ val matcher: Matcher,
+ val span: ComponentSpan,
+) {
+ fun matches(): Boolean {
+ return matcher.matches()
+ }
+
+ fun find(): Boolean {
+ return matcher.find()
+ }
+
+ fun group(index: Int): ComponentSpan? {
+ val start = matcher.start(index)
+ if (start < 0) return null
+ return this.span.slice(start, matcher.end(index))
+ }
+
+ fun group(name: String): ComponentSpan? {
+ val start = matcher.start(name)
+ if (start < 0) return null
+ return this.span.slice(start, matcher.end(name))
+ }
+}
+
+class ComponentSpan internal constructor(
+ val textComponent: IChatComponent,
+ private val cachedText: String,
+ val start: Int, val end: Int
+) {
+ init {
+ require(start <= end)
+ require(0 <= start)
+ require(end <= cachedText.length)
+ }
+
+ val length get() = end - start
+ fun slice(start: Int, end: Int): ComponentSpan {
+ require(0 <= start)
+ require(start <= end)
+ require(end <= length)
+ return ComponentSpan(textComponent, cachedText, this.start + start, this.start + end)
+ }
+
+ fun getText() = cachedText.substring(start, end)
+
+ fun sampleStyleAtStart(): ChatStyle? = sampleAtStart().chatStyle
+
+ fun sampleComponents(): List<IChatComponent> {
+ return sampleSlicedComponents().map { it.first }
+ }
+
+ fun sampleSlicedComponents(): List<Triple<IChatComponent, Int, Int>> {
+ var index = start
+ val workStack = Stack<IChatComponent>()
+ workStack.push(textComponent)
+ var lastComponent = textComponent
+ val listBuilder = mutableListOf<Triple<IChatComponent, Int, Int>>()
+ while (workStack.isNotEmpty()) {
+ val currentComponent = workStack.pop()
+ if (index + length <= 0) {
+ break
+ }
+ for (sibling in currentComponent.siblings.reversed()) {
+ workStack.push(sibling)
+ }
+ val rawText = currentComponent.unformattedTextForChat
+ index -= rawText.length
+ if (index < 0) {
+ listBuilder.add(
+ Triple(
+ currentComponent,
+ (rawText.length + index).coerceAtLeast(0),
+ (rawText.length + index + length).coerceAtMost(rawText.length)
+ )
+ )
+ }
+ lastComponent = currentComponent
+ }
+ if (listBuilder.isEmpty())
+ listBuilder.add(Triple(lastComponent, 0, 0))
+ return listBuilder
+ }
+
+ fun sampleAtStart(): IChatComponent {
+ return sampleComponents().first()
+ }
+
+ /**
+ * Create an [IChatComponent] that looks identical to this slice, including hover events and such.
+ * This new chat component will be structurally different (flat) and not therefore not have the same property
+ * inheritances as the old [textComponent]. Be therefore careful when modifying styles. This new component will also
+ * only use [ChatComponentText], converting any other [IChatComponent] in the process.
+ */
+ fun intoComponent(): IChatComponent {
+ val parent = ChatComponentText("")
+ parent.chatStyle = ChatStyle()
+ sampleSlicedComponents().forEach {
+ val copy = ChatComponentText(it.first.unformattedTextForChat.substring(it.second, it.third))
+ copy.chatStyle = it.first.chatStyle.createDeepCopy()
+ parent.appendSibling(copy)
+ }
+ return parent
+ }
+
+ fun sampleStyles(): List<ChatStyle> {
+ return sampleComponents().map { it.chatStyle }
+ }
+
+}
diff --git a/src/test/java/at/hannibal2/skyhanni/test/ComponentSpanTest.kt b/src/test/java/at/hannibal2/skyhanni/test/ComponentSpanTest.kt
new file mode 100644
index 000000000..9b5f40829
--- /dev/null
+++ b/src/test/java/at/hannibal2/skyhanni/test/ComponentSpanTest.kt
@@ -0,0 +1,53 @@
+package at.hannibal2.skyhanni.test
+
+import at.hannibal2.skyhanni.utils.ComponentMatcherUtils.findStyledMatcher
+import at.hannibal2.skyhanni.utils.ComponentMatcherUtils.intoSpan
+import at.hannibal2.skyhanni.utils.ComponentMatcherUtils.matchStyledMatcher
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.ChatStyle
+import net.minecraft.util.EnumChatFormatting
+import org.junit.jupiter.api.Test
+import java.util.regex.Pattern
+
+class ComponentSpanTest {
+ private fun text(string: String, init: ChatComponentText.() -> Unit = {}) = ChatComponentText(string).also(init)
+
+ @Test
+ fun testComponent() {
+ val component = text("12345") {
+ appendSibling(text("12345") {
+ chatStyle = ChatStyle().setColor(EnumChatFormatting.RED)
+ })
+ appendSibling(text("12345"))
+ }
+ val span = component.intoSpan()
+ require(span.sampleStyleAtStart()?.isEmpty == true)
+ require(span.slice(5, 8).sampleStyleAtStart()?.color == EnumChatFormatting.RED)
+ require(span.slice(8, 12).sampleStyleAtStart()?.color == EnumChatFormatting.RED)
+ require(span.slice(10, 12).sampleStyleAtStart()?.isEmpty == true)
+ require(span.slice(4, 11).intoComponent().formattedText == "§r5§r§c12345§r1§r")
+ }
+
+ @Test
+ fun testRegex() {
+ val component = text("12345") {
+ appendSibling(text("abcdef") {
+ chatStyle = ChatStyle().setColor(EnumChatFormatting.RED)
+ })
+ appendSibling(text("12345"))
+ }
+ Pattern.compile("[0-9]*(?<middle>[a-z]+)[0-9]*").matchStyledMatcher(component) {
+ require(group("middle")?.sampleStyleAtStart()?.color == EnumChatFormatting.RED)
+ }
+ val middlePartExtracted =
+ Pattern.compile("[0-9]*(?<middle>[0-9][a-z]+[0-9])[0-9]*").matchStyledMatcher(component) {
+ require(group("middle")?.sampleComponents()?.size == 3)
+ require(group("middle")?.sampleStyles()?.find { it.color != null }?.color == EnumChatFormatting.RED)
+ group("middle")
+ }!!
+ Pattern.compile("(?<whole>c)").findStyledMatcher(middlePartExtracted) {
+ require(group("whole")?.sampleStyleAtStart()?.color == EnumChatFormatting.RED)
+ }
+ }
+
+}