Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions core/toolbox/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library", "kt_jvm_test")

package(default_visibility = ["//visibility:public"])

kt_jvm_library(
name = "toolbox",
srcs = glob(["src/main/kotlin/**/*.kt"]),
visibility = ["//visibility:public"],
deps = [
"//core/data",
"//core/linguistics",
],
)

kt_jvm_test(
name = "trap_detector_engine_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/toolbox/TrapDetectorEngineTest.kt"],
test_class = "com.anysoftkeyboard.calque.toolbox.TrapDetectorEngineTest",
deps = [
":toolbox",
"//core/data",
"@maven//:junit_junit",
],
)

kt_jvm_test(
name = "word_transformer_engine_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/toolbox/WordTransformerEngineTest.kt"],
test_class = "com.anysoftkeyboard.calque.toolbox.WordTransformerEngineTest",
deps = [
":toolbox",
"//core/data",
"@maven//:junit_junit",
],
)

kt_jvm_test(
name = "tense_shifter_engine_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/toolbox/TenseShifterEngineTest.kt"],
test_class = "com.anysoftkeyboard.calque.toolbox.TenseShifterEngineTest",
deps = [
":toolbox",
"@maven//:junit_junit",
],
)

kt_jvm_test(
name = "number_odometer_engine_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/toolbox/NumberOdometerEngineTest.kt"],
test_class = "com.anysoftkeyboard.calque.toolbox.NumberOdometerEngineTest",
deps = [
":toolbox",
"@maven//:junit_junit",
],
)

kt_jvm_test(
name = "clitic_surgery_engine_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/toolbox/CliticSurgeryEngineTest.kt"],
test_class = "com.anysoftkeyboard.calque.toolbox.CliticSurgeryEngineTest",
deps = [
":toolbox",
"//core/linguistics",
"@maven//:junit_junit",
],
)

kt_jvm_test(
name = "omni_bar_engine_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/toolbox/OmniBarEngineTest.kt"],
test_class = "com.anysoftkeyboard.calque.toolbox.OmniBarEngineTest",
deps = [
":toolbox",
"//core/data",
"//core/linguistics",
"@maven//:junit_junit",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.anysoftkeyboard.calque.toolbox

import com.anysoftkeyboard.calque.linguistics.CliticDeconstructor
import com.anysoftkeyboard.calque.linguistics.WordLookupProvider
import java.util.Locale

/** Domain engine for the Clitic Unpacker ("Pronoun Surgery" Sandbox). */
object CliticSurgeryEngine {

/**
* Performs pronoun surgery on [word], breaking down root verb and attached enclitics into
* structured, color-coded visual blocks and generating equivalent proclitic structures.
*/
fun performSurgery(
word: String,
lookupProvider: WordLookupProvider,
): CliticSurgeryResult? {
val breakdown = CliticDeconstructor.deconstruct(word, lookupProvider) ?: return null
val rootVerb = breakdown.rootSurface
val mood = determineVerbMood(rootVerb)
val pronounBlocks =
breakdown.attachedPronouns.map { pronoun -> createPronounBlock(pronoun) }

val rootBlock =
PronounBlock(
type = PronounBlockType.ROOT_VERB,
text = rootVerb,
roleName = "Root Verb (${breakdown.rootLemma})",
meaning = breakdown.rootEntry.englishGloss,
)

val allBlocks = listOf(rootBlock) + pronounBlocks
val procliticEquivalent =
buildProcliticEquivalent(rootVerb, breakdown.attachedPronouns, mood)
val explanation = buildSurgeryExplanation(word, rootVerb, breakdown.attachedPronouns, mood)

return CliticSurgeryResult(
originalWord = word,
rootVerb = rootVerb,
rootMood = mood,
blocks = allBlocks,
separatedPronouns = breakdown.attachedPronouns,
procliticEquivalent = procliticEquivalent,
explanation = explanation,
)
}

private fun determineVerbMood(rootVerb: String): String {
val lower = rootVerb.lowercase(Locale.ROOT)
return when {
lower.endsWith("r") -> "infinitive"
lower.endsWith("ndo") -> "gerund"
else -> "imperative"
}
}

private fun createPronounBlock(pronoun: String): PronounBlock {
val lower = pronoun.lowercase(Locale.ROOT)
val isDirect = lower in setOf("lo", "la", "los", "las")
val isReflexive = lower == "se"

val type =
when {
isDirect -> PronounBlockType.DIRECT_PRONOUN
isReflexive -> PronounBlockType.REFLEXIVE_PRONOUN
else -> PronounBlockType.INDIRECT_PRONOUN
}

val roleName =
when {
isDirect -> "Direct Object"
isReflexive -> "Indirect / Reflexive"
else -> "Indirect Object"
}

val meaning = getPronounMeaning(lower)
return PronounBlock(type = type, text = pronoun, roleName = roleName, meaning = meaning)
}

private fun getPronounMeaning(pronoun: String): String {
return when (pronoun) {
"me" -> "to/for me"
"te" -> "to/for you"
"se" -> "to him/her/them (or reflexive)"
"nos" -> "to/for us"
"os" -> "to/for you all"
"le",
"les" -> "to him/her/them"
"lo" -> "him / it (masc.)"
"la" -> "her / it (fem.)"
"los" -> "them (masc.)"
"las" -> "them (fem.)"
else -> pronoun
}
}

private fun buildProcliticEquivalent(
rootVerb: String,
pronouns: List<String>,
mood: String,
): String {
val joinedPronouns = pronouns.joinToString(" ")
return when (mood) {
"infinitive" -> "$joinedPronouns va a $rootVerb"
"gerund" -> "$joinedPronouns está $rootVerb"
else -> "No $joinedPronouns $rootVerb"
}
}

private fun buildSurgeryExplanation(
originalWord: String,
rootVerb: String,
pronouns: List<String>,
mood: String,
): String {
val pronounCount = if (pronouns.size == 1) "1 pronoun" else "${pronouns.size} pronouns"
val pronounList = pronouns.joinToString(", ") { "'$it'" }
return "'$originalWord' attaches $pronounCount ($pronounList) to the $mood '$rootVerb'."
}
}
Loading
Loading