diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-20 21:07:38 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-20 21:07:38 +0900 |
| commit | 491420e94db9b21d350ee49df45eb129dec960d2 (patch) | |
| tree | 7828da3e59c677d7778786586bb9c95061b82df5 | |
| parent | 3b1d35deb3488428dfcff0cd8448c0b0ed620903 (diff) | |
| parent | 061bd455faad138ca1aefeb73be534198a4e0a44 (diff) | |
| download | LunaticChat-491420e94db9b21d350ee49df45eb129dec960d2.tar.gz LunaticChat-491420e94db9b21d350ee49df45eb129dec960d2.tar.bz2 LunaticChat-491420e94db9b21d350ee49df45eb129dec960d2.zip | |
Merge pull request #49 from m1sk9/fix-kana-conversionv0.4.1
fix: Fix Kana conversion with voiced consonants
| -rwxr-xr-x | .github/scripts/post-test-results.sh | 101 | ||||
| -rw-r--r-- | .github/workflows/ci.yaml | 16 | ||||
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | build.gradle.kts | 14 | ||||
| -rw-r--r-- | engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt | 30 | ||||
| -rw-r--r-- | engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverterTest.kt | 293 | ||||
| -rw-r--r-- | platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt | 29 |
7 files changed, 470 insertions, 17 deletions
diff --git a/.github/scripts/post-test-results.sh b/.github/scripts/post-test-results.sh new file mode 100755 index 0000000..f8fbdf2 --- /dev/null +++ b/.github/scripts/post-test-results.sh @@ -0,0 +1,101 @@ +#!/bin/bash +set -e + +MARKER="<!-- test-results-comment -->" + +TOTAL_TESTS=0 +PASSED_TESTS=0 +FAILED_TESTS=0 +SKIPPED_TESTS=0 +FAILED_TEST_NAMES="" + +for xml_file in */build/test-results/test/*.xml; do + if [ -f "$xml_file" ]; then + tests=$(sed -n 's/.*tests="\([0-9]*\)".*/\1/p' "$xml_file" | head -n1) + failures=$(sed -n 's/.*failures="\([0-9]*\)".*/\1/p' "$xml_file" | head -n1) + skipped=$(sed -n 's/.*skipped="\([0-9]*\)".*/\1/p' "$xml_file" | head -n1) + + tests=${tests:-0} + failures=${failures:-0} + skipped=${skipped:-0} + + TOTAL_TESTS=$((TOTAL_TESTS + tests)) + FAILED_TESTS=$((FAILED_TESTS + failures)) + SKIPPED_TESTS=$((SKIPPED_TESTS + skipped)) + + if [ "$failures" -gt 0 ]; then + failed_names=$(sed -n 's/.*<testcase name="\([^"]*\)".*<failure.*/\1/p' "$xml_file" || echo "") + if [ -n "$failed_names" ]; then + FAILED_TEST_NAMES="${FAILED_TEST_NAMES}${failed_names}\n" + fi + fi + fi +done + +PASSED_TESTS=$((TOTAL_TESTS - FAILED_TESTS - SKIPPED_TESTS)) + +if [ "$FAILED_TESTS" -gt 0 ]; then + STATUS="❌ Failed" + STATUS_EMOJI="❌" +else + STATUS="✅ Passed" + STATUS_EMOJI="✅" +fi + +COMMENT_BODY="${MARKER} +## ${STATUS_EMOJI} Test Results + +| Status | Count | +|--------|-------| +| ✅ Passed | ${PASSED_TESTS} | +| ❌ Failed | ${FAILED_TESTS} | +| ⏭️ Skipped | ${SKIPPED_TESTS} | +| **Total** | **${TOTAL_TESTS}** | + +**Overall Status:** ${STATUS} +" + +if [ "$FAILED_TESTS" -gt 0 ] && [ -n "$FAILED_TEST_NAMES" ]; then + COMMENT_BODY="${COMMENT_BODY} + +<details> +<summary>❌ Failed Tests</summary> + +$(echo -e "$FAILED_TEST_NAMES" | sed 's/^/- /') + +</details> +" +fi + +COMMENT_BODY="${COMMENT_BODY} + +--- +*Updated at $(date -u '+%Y-%m-%d %H:%M:%S UTC')*" + +echo "$COMMENT_BODY" > /tmp/test-comment.md + +PR_NUMBER="${GITHUB_REF#refs/pull/}" +PR_NUMBER="${PR_NUMBER%/merge}" + +if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "$GITHUB_REF" ]; then + echo "Not a pull request, skipping comment" + exit 0 +fi + +EXISTING_COMMENT_ID=$(gh api \ + "repos/$GITHUB_REPOSITORY/issues/${PR_NUMBER}/comments" \ + --jq ".[] | select(.body | contains(\"$MARKER\")) | .id" \ + | head -n 1 || echo "") + +if [ -n "$EXISTING_COMMENT_ID" ]; then + echo "Updating existing comment (ID: $EXISTING_COMMENT_ID)" + gh api \ + "repos/$GITHUB_REPOSITORY/issues/comments/${EXISTING_COMMENT_ID}" \ + -X PATCH \ + -f body="$(cat /tmp/test-comment.md)" +else + echo "Creating new comment" + gh pr comment "$PR_NUMBER" --body-file /tmp/test-comment.md +fi + +echo "Test results comment posted successfully" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 987ceb4..6cab034 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,6 +11,10 @@ on: jobs: build_plugin: runs-on: ubuntu-24.04 + permissions: + contents: read + pull-requests: write + issues: write steps: - name: Checkout repository uses: actions/checkout@v6 @@ -24,6 +28,18 @@ jobs: - name: Check ktlint run: ./gradlew ktlintCheck --no-daemon + - name: Run tests + run: ./gradlew test --no-daemon + continue-on-error: true + + - name: Post test results to PR + if: github.event_name == 'pull_request' && always() + env: + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_REF: ${{ github.ref }} + run: .github/scripts/post-test-results.sh + - name: Build with shadowJar run: ./gradlew :platform-paper:shadowJar --parallel --no-daemon diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5ffca..2f692a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # LunaticChat Changelog +### v0.4.1 + +- Fixed Kana conversion with voiced consonants. + ### v0.4.0 - Added an update checker that verifies whether updates are available when the server starts up. diff --git a/build.gradle.kts b/build.gradle.kts index 9430417..3def8c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ plugins { allprojects { group = "dev.m1sk9" - version = "0.4.0" + version = "0.4.1" repositories { mavenCentral() @@ -28,4 +28,16 @@ subprojects { jvmTarget.set(JvmTarget.JVM_21) } } + + tasks.withType<Test> { + useJUnitPlatform() + } + + afterEvaluate { + dependencies { + add("testImplementation", "org.jetbrains.kotlin:kotlin-test-junit5") + add("testImplementation", "org.junit.jupiter:junit-jupiter-api:5.11.4") + add("testRuntimeOnly", "org.junit.jupiter:junit-jupiter-engine:5.11.4") + } + } } diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt index 47ab960..1b3c67d 100644 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt @@ -20,6 +20,20 @@ object KanaConverter { private fun buildTrie(): TrieNode { val mappings = listOf( + // 4文字変換 (x/l prefix small characters) + "xtsu" to "っ", + "ltsu" to "っ", + // 3文字変換 (x/l prefix small characters) + "xtu" to "っ", + "ltu" to "っ", + "xya" to "ゃ", + "lya" to "ゃ", + "xyu" to "ゅ", + "lyu" to "ゅ", + "xyo" to "ょ", + "lyo" to "ょ", + "xwa" to "ゎ", + "lwa" to "ゎ", // 3文字変換 "kya" to "きゃ", "kyi" to "きぃ", @@ -104,6 +118,17 @@ object KanaConverter { "whi" to "うぃ", "whe" to "うぇ", "who" to "うぉ", + // 2文字変換 (x/l prefix small vowels) + "xa" to "ぁ", + "la" to "ぁ", + "xi" to "ぃ", + "li" to "ぃ", + "xu" to "ぅ", + "lu" to "ぅ", + "xe" to "ぇ", + "le" to "ぇ", + "xo" to "ぉ", + "lo" to "ぉ", // 2文字変換 "ka" to "か", "ki" to "き", @@ -181,11 +206,6 @@ object KanaConverter { "wu" to "う", "we" to "ゑ", "wo" to "を", - "la" to "ら", - "li" to "り", - "lu" to "る", - "le" to "れ", - "lo" to "ろ", "nn" to "ん", // 1文字変換 "a" to "あ", diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverterTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverterTest.kt new file mode 100644 index 0000000..a348fa3 --- /dev/null +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverterTest.kt @@ -0,0 +1,293 @@ +package dev.m1sk9.lunaticChat.engine.converter + +import kotlin.test.Test +import kotlin.test.assertEquals + +class KanaConverterTest { + @Test + fun `should convert single vowels to hiragana`() { + assertEquals("あ", KanaConverter.toHiragana("a")) + assertEquals("い", KanaConverter.toHiragana("i")) + assertEquals("う", KanaConverter.toHiragana("u")) + assertEquals("え", KanaConverter.toHiragana("e")) + assertEquals("お", KanaConverter.toHiragana("o")) + } + + @Test + fun `should convert basic ka-row consonants to hiragana`() { + assertEquals("か", KanaConverter.toHiragana("ka")) + assertEquals("き", KanaConverter.toHiragana("ki")) + assertEquals("く", KanaConverter.toHiragana("ku")) + assertEquals("け", KanaConverter.toHiragana("ke")) + assertEquals("こ", KanaConverter.toHiragana("ko")) + } + + @Test + fun `should convert simple word konnichiwa to hiragana`() { + assertEquals("こんいちわ", KanaConverter.toHiragana("konnichiwa")) + } + + @Test + fun `should convert simple word arigatou to hiragana`() { + assertEquals("ありがとう", KanaConverter.toHiragana("arigatou")) + } + + @Test + fun `should convert xtsu and ltsu to small tsu`() { + assertEquals("っ", KanaConverter.toHiragana("xtsu")) + assertEquals("っ", KanaConverter.toHiragana("ltsu")) + } + + @Test + fun `should convert xtu and ltu to small tsu`() { + assertEquals("っ", KanaConverter.toHiragana("xtu")) + assertEquals("っ", KanaConverter.toHiragana("ltu")) + } + + @Test + fun `should convert xa and la to small a`() { + assertEquals("ぁ", KanaConverter.toHiragana("xa")) + assertEquals("ぁ", KanaConverter.toHiragana("la")) + } + + @Test + fun `should convert xi and li to small i`() { + assertEquals("ぃ", KanaConverter.toHiragana("xi")) + assertEquals("ぃ", KanaConverter.toHiragana("li")) + } + + @Test + fun `should convert xu and lu to small u`() { + assertEquals("ぅ", KanaConverter.toHiragana("xu")) + assertEquals("ぅ", KanaConverter.toHiragana("lu")) + } + + @Test + fun `should convert xe and le to small e`() { + assertEquals("ぇ", KanaConverter.toHiragana("xe")) + assertEquals("ぇ", KanaConverter.toHiragana("le")) + } + + @Test + fun `should convert xo and lo to small o`() { + assertEquals("ぉ", KanaConverter.toHiragana("xo")) + assertEquals("ぉ", KanaConverter.toHiragana("lo")) + } + + @Test + fun `should convert xya and lya to small ya`() { + assertEquals("ゃ", KanaConverter.toHiragana("xya")) + assertEquals("ゃ", KanaConverter.toHiragana("lya")) + } + + @Test + fun `should convert xyu and lyu to small yu`() { + assertEquals("ゅ", KanaConverter.toHiragana("xyu")) + assertEquals("ゅ", KanaConverter.toHiragana("lyu")) + } + + @Test + fun `should convert xyo and lyo to small yo`() { + assertEquals("ゃ", KanaConverter.toHiragana("xya")) + assertEquals("ゃ", KanaConverter.toHiragana("lya")) + } + + @Test + fun `should convert xwa and lwa to small wa`() { + assertEquals("ゎ", KanaConverter.toHiragana("xwa")) + assertEquals("ゎ", KanaConverter.toHiragana("lwa")) + } + + @Test + fun `should convert user example - are kore op xtute raxtuka de shinanai question`() { + assertEquals("って", KanaConverter.toHiragana("xtute")) + assertEquals("らっか", KanaConverter.toHiragana("raxtuka")) + + val input = "are kore op xtute raxtuka de shinanai ?" + val expected = "あれ これ おp って らっか で しなない ?" + val actual = KanaConverter.toHiragana(input) + + assertEquals(expected, actual) + } + + @Test + fun `should convert xtute to tte using x prefix`() { + assertEquals("って", KanaConverter.toHiragana("xtute")) + } + + @Test + fun `should convert raxtuka to rakka with small tsu`() { + assertEquals("らっか", KanaConverter.toHiragana("raxtuka")) + } + + @Test + fun `should convert double consonants to small tsu`() { + assertEquals("きって", KanaConverter.toHiragana("kitte")) + assertEquals("がっこう", KanaConverter.toHiragana("gakkou")) + assertEquals("ずっと", KanaConverter.toHiragana("zutto")) + assertEquals("さっぱり", KanaConverter.toHiragana("sappari")) + } + + @Test + fun `should handle double t as small tsu`() { + assertEquals("まった", KanaConverter.toHiragana("matta")) + } + + @Test + fun `should handle double k as small tsu`() { + assertEquals("がっき", KanaConverter.toHiragana("gakki")) + } + + @Test + fun `should handle double p as small tsu`() { + assertEquals("いっぱい", KanaConverter.toHiragana("ippai")) + } + + @Test + fun `should convert kya-row to hiragana`() { + assertEquals("きゃ", KanaConverter.toHiragana("kya")) + assertEquals("きゅ", KanaConverter.toHiragana("kyu")) + assertEquals("きょ", KanaConverter.toHiragana("kyo")) + } + + @Test + fun `should convert sha-row to hiragana`() { + assertEquals("しゃ", KanaConverter.toHiragana("sha")) + assertEquals("しゅ", KanaConverter.toHiragana("shu")) + assertEquals("しょ", KanaConverter.toHiragana("sho")) + } + + @Test + fun `should convert cha-row to hiragana`() { + assertEquals("ちゃ", KanaConverter.toHiragana("cha")) + assertEquals("ちゅ", KanaConverter.toHiragana("chu")) + assertEquals("ちょ", KanaConverter.toHiragana("cho")) + } + + @Test + fun `should convert nya-row to hiragana`() { + assertEquals("にゃ", KanaConverter.toHiragana("nya")) + assertEquals("にゅ", KanaConverter.toHiragana("nyu")) + assertEquals("にょ", KanaConverter.toHiragana("nyo")) + } + + @Test + fun `should convert hya-row to hiragana`() { + assertEquals("ひゃ", KanaConverter.toHiragana("hya")) + assertEquals("ひゅ", KanaConverter.toHiragana("hyu")) + assertEquals("ひょ", KanaConverter.toHiragana("hyo")) + } + + @Test + fun `should convert mya-row to hiragana`() { + assertEquals("みゃ", KanaConverter.toHiragana("mya")) + assertEquals("みゅ", KanaConverter.toHiragana("myu")) + assertEquals("みょ", KanaConverter.toHiragana("myo")) + } + + @Test + fun `should convert rya-row to hiragana`() { + assertEquals("りゃ", KanaConverter.toHiragana("rya")) + assertEquals("りゅ", KanaConverter.toHiragana("ryu")) + assertEquals("りょ", KanaConverter.toHiragana("ryo")) + } + + @Test + fun `should convert gya-row to hiragana`() { + assertEquals("ぎゃ", KanaConverter.toHiragana("gya")) + assertEquals("ぎゅ", KanaConverter.toHiragana("gyu")) + assertEquals("ぎょ", KanaConverter.toHiragana("gyo")) + } + + @Test + fun `should convert ja-row to hiragana`() { + assertEquals("じゃ", KanaConverter.toHiragana("ja")) + assertEquals("じ", KanaConverter.toHiragana("ji")) + assertEquals("じゅ", KanaConverter.toHiragana("ju")) + assertEquals("じょ", KanaConverter.toHiragana("jo")) + } + + @Test + fun `should return empty string for empty input`() { + assertEquals("", KanaConverter.toHiragana("")) + } + + @Test + fun `should pass through non-romanji characters unchanged`() { + assertEquals("123", KanaConverter.toHiragana("123")) + assertEquals("!", KanaConverter.toHiragana("!")) + assertEquals("?", KanaConverter.toHiragana("?")) + } + + @Test + fun `should handle mixed romanji and non-romanji`() { + assertEquals("あ123い", KanaConverter.toHiragana("a123i")) + } + + @Test + fun `should handle uppercase input by converting to lowercase first`() { + assertEquals("か", KanaConverter.toHiragana("KA")) + assertEquals("きゃ", KanaConverter.toHiragana("KYA")) + } + + @Test + fun `should handle mixed case input`() { + assertEquals("こんいちわ", KanaConverter.toHiragana("KoNnIcHiWa")) + } + + @Test + fun `should convert n to n-sound correctly`() { + assertEquals("ん", KanaConverter.toHiragana("n")) + assertEquals("ん", KanaConverter.toHiragana("nn")) + } + + @Test + fun `should handle tsu correctly`() { + assertEquals("つ", KanaConverter.toHiragana("tsu")) + } + + @Test + fun `should handle chi and shi correctly`() { + assertEquals("ち", KanaConverter.toHiragana("chi")) + assertEquals("し", KanaConverter.toHiragana("shi")) + } + + @Test + fun `should handle fu correctly`() { + assertEquals("ふ", KanaConverter.toHiragana("fu")) + assertEquals("ふ", KanaConverter.toHiragana("hu")) + } + + @Test + fun `should handle wo and wa correctly`() { + assertEquals("を", KanaConverter.toHiragana("wo")) + assertEquals("わ", KanaConverter.toHiragana("wa")) + } + + @Test + fun `should convert common greeting ohayou`() { + assertEquals("おはよう", KanaConverter.toHiragana("ohayou")) + } + + @Test + fun `should convert common word sumimasen`() { + assertEquals("すみません", KanaConverter.toHiragana("sumimasen")) + } + + @Test + fun `should convert phrase with double consonants`() { + assertEquals("がんばって", KanaConverter.toHiragana("ganbatte")) + } + + @Test + fun `should convert phrase with small ya-row`() { + assertEquals("きゃべつ", KanaConverter.toHiragana("kyabetsu")) + } + + @Test + fun `should handle long phrase with various patterns`() { + val input = "watashi wa nihongo wo benkyou shiteimasu" + val expected = "わたし わ にほんご を べんきょう しています" + assertEquals(expected, KanaConverter.toHiragana(input)) + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt index 7ee2fbd..7a7365e 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt @@ -49,6 +49,9 @@ class LunaticChat : val httpClient = HttpClient(CIO) + // Initialize player settings manager (always needed for DM notifications) + initializePlayerSettingsManager(configuration) + // Initialize features if (configuration.features.japaneseConversion.enabled) { initializeJapaneseConversionFeature(configuration, httpClient) @@ -92,18 +95,10 @@ class LunaticChat : } /** - * Initializes the Japanese conversion feature including: - * - Player settings management (YAML-based) - * - Conversion cache - * - Google IME API client - * - Romanji converter - * - Periodic cache saving task + * Initializes the player settings manager. + * This is always needed for features like DM notifications. */ - private fun initializeJapaneseConversionFeature( - configuration: LunaticChatConfiguration, - httpClient: HttpClient, - ) { - // Initialize player settings + private fun initializePlayerSettingsManager(configuration: LunaticChatConfiguration) { val settingsFile = dataFolder.resolve(configuration.userSettingsFilePath).toPath() val storage = YamlPlayerSettingsStorage( @@ -118,7 +113,19 @@ class LunaticChat : logger = logger, ) playerSettingsManager!!.initialize() + } + /** + * Initializes the Japanese conversion feature including: + * - Conversion cache + * - Google IME API client + * - Romanji converter + * - Periodic cache saving task + */ + private fun initializeJapaneseConversionFeature( + configuration: LunaticChatConfiguration, + httpClient: HttpClient, + ) { // Initialize conversion cache val cache = ConversionCache( |
