From ab83559128fdcf3f9003dbd6a9246c242b99083e Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Mon, 22 Feb 2021 21:56:33 +0100
Subject: [PATCH] Implement code to cleanly parse command expansions

---
 .../protocol/util/ParsingContext.kt           |  66 +++++++++++
 .../protocol/util/expansion/Expansion.kt      |  36 ++++++
 .../util/expansion/ExpansionParsingContext.kt |  67 +++++++++++
 .../protocol/util/expansion/ExpansionTest.kt  | 106 ++++++++++++++++++
 4 files changed, 275 insertions(+)
 create mode 100644 libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/ParsingContext.kt
 create mode 100644 libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expansion/Expansion.kt
 create mode 100644 libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expansion/ExpansionParsingContext.kt
 create mode 100644 libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expansion/ExpansionTest.kt

diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/ParsingContext.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/ParsingContext.kt
new file mode 100644
index 0000000..9531f83
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/ParsingContext.kt
@@ -0,0 +1,66 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * 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.protocol.util
+
+import de.justjanne.libquassel.annotations.Generated
+import de.justjanne.libquassel.protocol.util.expansion.Expansion
+import java.util.function.Supplier
+
+internal abstract class ParsingContext<T>(
+  internal val text: String
+) {
+  protected abstract val matchers: List<Supplier<T?>>
+
+  internal var position = 0
+
+  fun parse(): List<T> {
+    val result = mutableListOf<T>()
+    while (position < text.length) {
+      for (matcher in matchers) {
+        val match = matcher.get()
+        if (match != null) {
+          result.add(match)
+          continue
+        }
+      }
+    }
+    return result
+  }
+
+  @Generated
+  protected inline fun match(
+    vararg patterns: String,
+    crossinline function: () -> Expansion
+  ) = Supplier {
+    for (pattern in patterns) {
+      if (text.startsWith(pattern, startIndex = position)) {
+        position += pattern.length
+        return@Supplier function()
+      }
+    }
+    return@Supplier null
+  }
+
+  @Generated
+  protected inline fun match(
+    vararg patterns: Regex,
+    crossinline function: (List<String>) -> Expansion
+  ) = Supplier {
+    for (pattern in patterns) {
+      val match = pattern.find(text, startIndex = position)
+      if (match != null && match.range.first == position) {
+        position = match.range.last + 1
+        return@Supplier function(match.groupValues)
+      }
+    }
+    return@Supplier null
+  }
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expansion/Expansion.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expansion/Expansion.kt
new file mode 100644
index 0000000..93da3d9
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expansion/Expansion.kt
@@ -0,0 +1,36 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * 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.protocol.util.expansion
+
+sealed class Expansion {
+  data class Text(val value: String) : Expansion()
+  data class ParameterRange(val from: Int, val to: Int?) : Expansion()
+  data class Parameter(val index: Int, val field: ParameterField?) : Expansion()
+  data class Constant(val field: ConstantField) : Expansion()
+
+  enum class ParameterField {
+    HOSTNAME,
+    VERIFIED_IDENT,
+    IDENT,
+    ACCOUNT
+  }
+
+  enum class ConstantField {
+    CHANNEL,
+    NICK,
+    NETWORK
+  }
+
+  companion object {
+    fun parse(text: String): List<Expansion> =
+      ExpansionParsingContext(text).parse()
+  }
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expansion/ExpansionParsingContext.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expansion/ExpansionParsingContext.kt
new file mode 100644
index 0000000..566e2df
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expansion/ExpansionParsingContext.kt
@@ -0,0 +1,67 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * 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.protocol.util.expansion
+
+import de.justjanne.libquassel.protocol.util.ParsingContext
+import java.util.function.Supplier
+
+internal class ExpansionParsingContext(
+  text: String
+) : ParsingContext<Expansion>(text) {
+  override val matchers: List<Supplier<Expansion?>> = listOf(
+    match("\$channelname", "\$channel") {
+      Expansion.Constant(Expansion.ConstantField.CHANNEL)
+    },
+    match("\$currentnick", "\$nick") {
+      Expansion.Constant(Expansion.ConstantField.NICK)
+    },
+    match("\$network") {
+      Expansion.Constant(Expansion.ConstantField.NETWORK)
+    },
+    match("\$0") {
+      Expansion.Parameter(0, null)
+    },
+    match("""\$(\d+)\.\.(\d+)""".toRegex()) { (_, from, to) ->
+      Expansion.ParameterRange(from.toInt(), to.toInt())
+    },
+    match("""\$(\d+)\.\.""".toRegex()) { (_, from) ->
+      Expansion.ParameterRange(from.toInt(), null)
+    },
+    match("""\$(\d+):hostname""".toRegex()) { (_, value) ->
+      Expansion.Parameter(value.toInt(), Expansion.ParameterField.HOSTNAME)
+    },
+    match("""\$(\d+):identd""".toRegex()) { (_, value) ->
+      Expansion.Parameter(value.toInt(), Expansion.ParameterField.VERIFIED_IDENT)
+    },
+    match("""\$(\d+):ident""".toRegex()) { (_, value) ->
+      Expansion.Parameter(value.toInt(), Expansion.ParameterField.IDENT)
+    },
+    match("""\$(\d+):account""".toRegex()) { (_, value) ->
+      Expansion.Parameter(value.toInt(), Expansion.ParameterField.ACCOUNT)
+    },
+    match("""\$(\d+)""".toRegex()) { (_, value) ->
+      Expansion.Parameter(value.toInt(), null)
+    },
+    Supplier {
+      val end = text.indexOf('$', startIndex = position).let {
+        if (it >= 0) it
+        else text.length
+      }
+      if (position < end) {
+        val start = position
+        position = end
+        val value = text.substring(start, end)
+        return@Supplier Expansion.Text(value)
+      }
+      return@Supplier null
+    }
+  )
+}
diff --git a/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expansion/ExpansionTest.kt b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expansion/ExpansionTest.kt
new file mode 100644
index 0000000..0229061
--- /dev/null
+++ b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expansion/ExpansionTest.kt
@@ -0,0 +1,106 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * 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.protocol.util.expansion
+
+import org.junit.jupiter.api.Test
+import kotlin.test.assertEquals
+
+class ExpansionTest {
+  @Test
+  fun testDefaults() {
+    assertEquals(
+      listOf(
+        Expansion.Text("/join "),
+        Expansion.Parameter(0, null)
+      ),
+      Expansion.parse("/join $0")
+    )
+
+    assertEquals(
+      listOf(
+        Expansion.Text("/whois "),
+        Expansion.Parameter(0, null),
+        Expansion.Text(" "),
+        Expansion.Parameter(0, null)
+      ),
+      Expansion.parse("/whois $0 $0")
+    )
+  }
+
+  @Test
+  fun testParameters() {
+    assertEquals(
+      listOf(
+        Expansion.Text("/say Welcome to the support channel for the IRC client Quassel, "),
+        Expansion.Parameter(1, null)
+      ),
+      Expansion.parse("/say Welcome to the support channel for the IRC client Quassel, \$1")
+    )
+    assertEquals(
+      listOf(
+        Expansion.Parameter(1, null),
+        Expansion.Text(" "),
+        Expansion.Parameter(1, Expansion.ParameterField.ACCOUNT),
+        Expansion.Text(" "),
+        Expansion.Parameter(1, Expansion.ParameterField.HOSTNAME),
+        Expansion.Text(" "),
+        Expansion.Parameter(1, Expansion.ParameterField.VERIFIED_IDENT),
+        Expansion.Text(" "),
+        Expansion.Parameter(1, Expansion.ParameterField.IDENT),
+      ),
+      Expansion.parse("\$1 \$1:account \$1:hostname \$1:identd \$1:ident")
+    )
+  }
+
+  @Test
+  fun testConstants() {
+    assertEquals(
+      listOf(
+        Expansion.Text("/say I am "),
+        Expansion.Constant(Expansion.ConstantField.NICK),
+        Expansion.Text(", welcoming you to our channel "),
+        Expansion.Constant(Expansion.ConstantField.CHANNEL),
+        Expansion.Text(" on "),
+        Expansion.Constant(Expansion.ConstantField.NETWORK),
+        Expansion.Text(".")
+      ),
+      Expansion.parse("/say I am \$nick, welcoming you to our channel \$channelname on \$network.")
+    )
+    assertEquals(
+      listOf(
+        Expansion.Text("/say That’s right, I’m /the/ "),
+        Expansion.Constant(Expansion.ConstantField.NICK),
+        Expansion.Text(" from "),
+        Expansion.Constant(Expansion.ConstantField.CHANNEL),
+        Expansion.Text(".")
+      ),
+      Expansion.parse("/say That’s right, I’m /the/ \$nick from \$channel.")
+    )
+  }
+
+  @Test
+  fun testRanges() {
+    assertEquals(
+      listOf(
+        Expansion.Text("1 \""),
+        Expansion.Parameter(1, null),
+        Expansion.Text("\" 2 \""),
+        Expansion.Parameter(2, null),
+        Expansion.Text("\" 3..4 \""),
+        Expansion.ParameterRange(3, 4),
+        Expansion.Text("\" 3.. \""),
+        Expansion.ParameterRange(3, null),
+        Expansion.Text("\""),
+      ),
+      Expansion.parse("1 \"\$1\" 2 \"\$2\" 3..4 \"\$3..4\" 3.. \"\$3..\"")
+    )
+  }
+}
-- 
GitLab