diff options
| -rw-r--r-- | src/main/java/at/hannibal2/skyhanni/utils/ComponentMatcherUtils.kt | 149 | ||||
| -rw-r--r-- | src/test/java/at/hannibal2/skyhanni/test/ComponentSpanTest.kt | 53 |
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) + } + } + +} |
