├── .editorconfig ├── .gitignore ├── Dockerfile ├── logback ├── logback-dev.xml └── logback.xml ├── .github └── workflows │ └── ci.yml ├── resources ├── longtest.txt └── help.md ├── src └── ithkuil │ └── iv │ └── gloss │ ├── interfaces │ ├── Terminal.kt │ └── Discord.kt │ ├── ExternalJuncture.kt │ ├── dispatch │ ├── Date.kt │ └── Dispatch.kt │ ├── Context.kt │ ├── Constants.kt │ ├── WordTypes.kt │ ├── Structural.kt │ ├── Affixes.kt │ ├── Formatting.kt │ ├── Categories.kt │ ├── Slots.kt │ └── Words.kt ├── test ├── DiscordTests.kt ├── ContextTests.kt ├── FormattingTests.kt ├── SlotTests.kt └── WordTests.kt ├── .drone.yml ├── README.md ├── architecture.md └── pom.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.kt] 2 | indent_style = space 3 | indent_size = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | resources/token.txt 2 | resources/roots.tsv 3 | resources/affixes.tsv 4 | target/ 5 | .idea 6 | tnilgloss.iml 7 | ithkuil.log -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | RUN apk add --no-cache openjdk9-jre-headless git 3 | ENV VERSION 1.1.0-1.0.0 4 | ENTRYPOINT java -jar ithkuilgloss-$VERSION-jar-with-dependencies.jar 5 | COPY target/ithkuilgloss-$VERSION-jar-with-dependencies.jar / 6 | COPY .git .git 7 | COPY resources resources -------------------------------------------------------------------------------- /logback/logback-dev.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ithkuil.log 5 | 6 | %d{HH:mm:ss.SSS} %-5level | %msg%xException%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /logback/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.err 5 | 6 | %d{HH:mm:ss.SSS} %-5level | %msg%xException%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: "17" 21 | distribution: "temurin" 22 | - name: Build and test with Maven 23 | run: mvn --batch-mode --update-snapshots verify 24 | -------------------------------------------------------------------------------- /resources/longtest.txt: -------------------------------------------------------------------------------- 1 | Ufhulâ eatreuhlï wuksmenţi'ë 2 | Mâlu'u hma: Hmaggwí-ašnexürrtřa yoskwätļu'u 3 | Azhesâ. Tartïnhâ, antfäsi'a, 4 | Aţsägissa'hňu, yuilžařča. Aççpeřinï theuxač, 5 | Wamfpuňï avcasu'u, umweřuňï umskäzomļï'ï 6 | Avllevâ evẓâlüduna wulyezwi açmaňotanï. 7 | Emzäsouyâ tha, áẓčelä ešwarïntena'a, 8 | Wanluẓda ařžřusö'athu, aḑlialuň açnüsö'athu: 9 | Wužtļi'a mangut atväsâ: Ha 10 | "Wiadná la hnï *Ozimändiës*, hweltivî-eltíl: 11 | Ẓosêi ümbrira lëi, nu'i aňčäzwarru'u, yumřřuňêi!" hai 12 | Aiňļalïrxouyâ. Arčüsüanhi'a 13 | Esčäswallüxeu, erkefïrahma'u yabgaguňahma'u 14 | Iazhasahra eňtila'u alřähma'u efklaswünhâ. -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/interfaces/Terminal.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss.interfaces 2 | 3 | import ithkuil.iv.gloss.dispatch.* 4 | import java.io.FileNotFoundException 5 | 6 | fun main() { 7 | 8 | try { 9 | loadResourcesLocal() 10 | } catch (e: FileNotFoundException) { 11 | println("Loading resources...") 12 | loadResourcesOnline() 13 | } 14 | 15 | do { 16 | print(">>> ") 17 | val msg = readLine() ?: "" 18 | val response = respond(msg) 19 | if (response != null) { 20 | println(response) 21 | } 22 | } while (msg.isNotEmpty()) 23 | } 24 | -------------------------------------------------------------------------------- /test/DiscordTests.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss.test 2 | 3 | import ithkuil.iv.gloss.dispatch.respond 4 | import ithkuil.iv.gloss.interfaces.splitMessages 5 | import java.io.File 6 | import kotlin.test.Test 7 | import kotlin.test.assertTrue 8 | 9 | class DiscordTests { 10 | @Test 11 | fun longMessageTest() { 12 | val longText = File("./resources/longtest.txt").readText() 13 | val messages = respond("?gloss $longText")!!.splitMessages().toList() 14 | assertTrue("Is empty!") { messages.isNotEmpty() } 15 | assertTrue("Wrong size: ${messages.size}") { messages.size == 2 } 16 | assertTrue("Are longer than 2000 chars ${messages.map { it.length }}") { messages.all { it.length <= 2000 } } 17 | } 18 | } -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: docker 3 | name: default 4 | 5 | steps: 6 | - name: build 7 | image: maven:3-openjdk-15 8 | commands: 9 | - mvn --batch-mode package test 10 | - name: docker 11 | image: plugins/docker 12 | settings: 13 | registry: ghcr.io 14 | username: ngoriyasjil 15 | password: 16 | from_secret: registry_pass 17 | repo: ghcr.io/ngoriyasjil/ithkuilgloss 18 | branch: master 19 | tags: latest 20 | when: 21 | repo: 22 | - ngoriyasjil/IthkuilGloss 23 | branch: 24 | - master 25 | - name: webhook 26 | image: plugins/webhook 27 | settings: 28 | urls: 29 | from_secret: webhook_url 30 | when: 31 | repo: 32 | - ngoriyasjil/IthkuilGloss 33 | branch: 34 | - master 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IthkuilGloss 2 | 3 | A Discord parser bot for the Ithkuil IV language. 4 | 5 | Currently up to date to morphophonology v0.19.0. 6 | 7 | See [the help file](https://github.com/ngoriyasjil/IthkuilGloss/blob/master/resources/help.md) for the list of commands! 8 | 9 | ## How to run 10 | 11 | 1. If you’re running the Discord bot (rather than the CLI), place the bot token in the file `resources/token.txt`. 12 | 2. Compile with Maven. 13 | 14 | ## How to run the Command Line Interface 15 | 16 | 1. Set `main.class` to `ithkuil.iv.gloss.interfaces.TerminalKt` in `pom.xml` 17 | 2. Compile with Maven and run 18 | 19 | The CLI will by default download the lexicon locally, and then use that. To update the local lexicon, use the `?!reload` command. 20 | 21 | Ithkuil IV language is created by John Quijada. 22 | 23 | Based on TNILGloss by Syst3ms -------------------------------------------------------------------------------- /test/ContextTests.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss.test 2 | 3 | import ithkuil.iv.gloss.Valid 4 | import ithkuil.iv.gloss.formatWord 5 | import ithkuil.iv.gloss.isCarrier 6 | import kotlin.test.Test 7 | import kotlin.test.assertFalse 8 | import kotlin.test.assertTrue 9 | 10 | class ContextTests { 11 | 12 | @Test 13 | fun `Carrier identification examples`() { 14 | assertCarrier("sala") 15 | assertCarrier("husana-mala") 16 | assertCarrier("hamala-sala") 17 | assertCarrier("hla") 18 | assertCarrier("hňayazë") 19 | assertCarrier("ahnaxena") 20 | assertNotCarrier("hma") 21 | assertNotCarrier("ëisala") 22 | } 23 | } 24 | 25 | fun assertCarrier(word: String) { 26 | assertTrue(word) { isCarrier(formatWord(word) as Valid) } 27 | } 28 | 29 | fun assertNotCarrier(word: String) { 30 | assertFalse(word) { isCarrier(formatWord(word) as Valid) } 31 | } -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/ExternalJuncture.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | 4 | fun externalJuncture(formatted: List): String { 5 | 6 | val (valids, invalids) = formatted.partition { it is Valid } 7 | .let { (valids, invalids) -> 8 | Pair( 9 | valids.map { it as Valid }, 10 | invalids.map { it as Invalid } 11 | ) 12 | } 13 | 14 | if (invalids.isNotEmpty()) { 15 | return invalids.joinToString("\n") { "**$it**: *${it.message}*" } 16 | } 17 | 18 | 19 | val words: List = valids.flatMap { 20 | when (it) { 21 | is Word -> listOf(it) 22 | is ConcatenatedWords -> it.words 23 | } 24 | } 25 | 26 | val violations = mutableListOf() 27 | 28 | for ((word1, word2) in words.zipWithNext()) { 29 | if (word1.last().isConsonant() && word2.first().isConsonant()) { 30 | if (word1.postfixPunctuation.isEmpty() && word2.prefixPunctuation.isEmpty()) { 31 | violations.add("*EJ violation: $word1 $word2*") 32 | } 33 | } 34 | } 35 | 36 | if (violations.isEmpty()) return "*No EJ violations found*" 37 | 38 | return violations.joinToString("\n") 39 | } -------------------------------------------------------------------------------- /test/FormattingTests.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss.test 2 | 3 | import ithkuil.iv.gloss.Invalid 4 | import ithkuil.iv.gloss.Stress 5 | import ithkuil.iv.gloss.Word 6 | import ithkuil.iv.gloss.formatWord 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | infix fun String.hasStress(stress: Stress) = assertEquals(stress, (formatWord(this) as Word).stress, this) 11 | 12 | infix fun String.givesInvalid(error: String) = assertEquals(error, (formatWord(this) as Invalid).message) 13 | 14 | class FormattingTests { 15 | 16 | @Test 17 | fun `Stress examples`() { 18 | "a" hasStress Stress.MONOSYLLABIC 19 | "ala" hasStress Stress.PENULTIMATE 20 | "alá" hasStress Stress.ULTIMATE 21 | "lìala" hasStress Stress.PENULTIMATE 22 | "ua" hasStress Stress.PENULTIMATE 23 | "ëu" hasStress Stress.MONOSYLLABIC 24 | "alái" hasStress Stress.ULTIMATE 25 | "ái'la'sa" hasStress Stress.ANTEPENULTIMATE 26 | "ála'a" hasStress Stress.ANTEPENULTIMATE 27 | } 28 | 29 | @Test 30 | fun `Stress error examples`() { 31 | "á" givesInvalid "Marked default stress" 32 | "ála" givesInvalid "Marked default stress" 33 | "álá" givesInvalid "Double-marked stress" 34 | "álalala" givesInvalid "Unrecognized stress placement" 35 | "aí" givesInvalid "Unrecognized stress placement" 36 | } 37 | 38 | @Test 39 | fun `Sentence start prefix formatting errors`() { 40 | "çêlala" givesInvalid "Stress on sentence prefix" 41 | "ç" givesInvalid "Lone sentence prefix" 42 | "çw" givesInvalid "Lone sentence prefix" 43 | "çç" givesInvalid "Lone sentence prefix" 44 | } 45 | } -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/dispatch/Date.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss.dispatch 2 | 3 | import ithkuil.iv.gloss.Stem 4 | import ithkuil.iv.gloss.VOWEL_FORMS 5 | import kotlinx.datetime.Clock 6 | import kotlinx.datetime.LocalDateTime 7 | import kotlinx.datetime.TimeZone 8 | import kotlinx.datetime.toLocalDateTime 9 | 10 | private val NUMBER_ROOTS = listOf( 11 | "vr", 12 | "ll", 13 | "ks", 14 | "z", 15 | "pš", 16 | "st", 17 | "cp", 18 | "ns", 19 | "čk", 20 | "lẓ", 21 | "j", 22 | ) 23 | 24 | private val INVALID_INITIAL_NUMBER_ROOTS = setOf("ns", "lẓ", "čk") 25 | 26 | private val NUMBER_AFFIXES = listOf( 27 | "vc", 28 | "zc", 29 | "ks", 30 | "z", 31 | "pš", 32 | "st", 33 | "cp", 34 | "ns", 35 | "čk", 36 | "lẓ", 37 | "j", 38 | "cg", 39 | "jd" 40 | ) 41 | 42 | private fun numeralBody(n: Int, shortcut: Boolean = false, stem: Stem = Stem.ONE): String { 43 | val units = n % 10 44 | val tens = n / 10 45 | 46 | val unitsRoot = NUMBER_ROOTS[units] 47 | val tensCs = if (tens > 0) "${VOWEL_FORMS[tens - 1]}rs" else "" 48 | 49 | val cc = if (shortcut) "w" else "" 50 | 51 | val vvElides = !shortcut && stem == Stem.ONE && unitsRoot !in INVALID_INITIAL_NUMBER_ROOTS 52 | val vv = if (vvElides) "" else when (stem) { 53 | Stem.ONE -> "a" 54 | Stem.TWO -> "e" 55 | Stem.THREE -> "u" 56 | Stem.ZERO -> "o" 57 | } 58 | 59 | val vrCa = if (!shortcut) "al" else "" 60 | 61 | 62 | return "$cc$vv$unitsRoot$vrCa$tensCs" 63 | } 64 | 65 | fun datetimeInIthkuil( 66 | datetime: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.UTC) 67 | ): String { 68 | 69 | val day = datetime.dayOfMonth 70 | val month = datetime.monthNumber 71 | val year = datetime.year 72 | 73 | val ardhal = "${numeralBody(day, shortcut = true, Stem.THREE)}ëirwia${NUMBER_AFFIXES[month]}iktó" 74 | 75 | val ernal = "${numeralBody(year / 100)}a'o" 76 | val arnal = "${numeralBody(year % 100)}ürwi'i" 77 | 78 | val urwal = "${numeralBody(datetime.hour, shortcut = true, Stem.THREE)}erwa'o" 79 | val erwal = "${numeralBody(datetime.minute)}oň" 80 | 81 | 82 | return "$ardhal $ernal $arnal ($urwal $erwal)".replaceFirstChar { it.uppercase() } 83 | 84 | } -------------------------------------------------------------------------------- /architecture.md: -------------------------------------------------------------------------------- 1 | Hi. If you are reading this, you probably want to develop the bot, or just figure out how it works. The code should be readable if you want to dive right in, but I hope an overview will help. 2 | 3 | The bot has a layered structure, with IO on the top and the raw language categories on the bottom. The code is mostly functional in style, apart from a few mutating indices and such here and there. The layers are as follows: 4 | 5 | ### Layer 6: Interfaces 6 | 7 | There are currently three implemented interfaces to the bot: the command line interface, responding to Discord messages, and Discord slash commands. Each produces command strings for the Dispatch layer, and displays whatever it gets back. 8 | 9 | ### Layer 5: Dispatch 10 | 11 | A fully text-based interface that contains the actual commands. Basically just a big "when" block. If the command is to gloss, it sends the strings of words downwards, gets back the Gloss objects and asks them nicely to become gloss strings. All logic relating to the dictionaries is on this layer too. 12 | 13 | ### Layer 4: Formatting 14 | 15 | This layer clears the raw words of any punctuation, alternate versions of letters and stress markings, producing nice, clean lists of consonant clusters and vowelforms. The type of a given word is also figured out on this level. 16 | 17 | ### Layer 3: Context 18 | 19 | In a few cases, words affect the interpretation of nearby words, i.e. modular adjuncts, suppletive adjuncts and the carrier root. This layer handles that before calling the word-by-word glossing level. 20 | 21 | ### Layer 2: Words 22 | 23 | Each word is parsed slot by slot by calling the matching function for its word type. The main difficulty on this level is figuring out the various messy rules as to what slot goes where. Each slot or affix is then parsed in its own function 24 | 25 | ### Layer 1: Slots (and Affixes) 26 | 27 | This layer works out the mapping of phonemic forms to the underlying grammatical categories. 28 | 29 | ### Layer 0: Categories 30 | 31 | Here are finally the actual categories, and the actual logic for glossing them! The Dispatch level calls a Gloss to be glossed, which calls its Slots, which ultimately call the .gloss methods of the categories, passing the settings (precision and whether defaults are shown) down each layer. 32 | 33 | That might be a lot to keep in one's head, but you generally don't have to worry about the other layers when working on one. I wish you luck. 34 | 35 | -- Umḑuaňvilppöirksüxaiẓda -------------------------------------------------------------------------------- /resources/help.md: -------------------------------------------------------------------------------- 1 | **IthkuilGloss Help** 2 | 3 | The IthkuilGloss bot gives morpheme-by-morpheme glosses of Ithkuil IV text. A gloss exposes the underlying grammatical values, which helps in both understanding and composing Ithkuil text. 4 | 5 | Main commands: 6 | - Glosses the following words one at a time: 7 | - **?gloss**: regular precision 8 | - **?full**: full precision 9 | - **?short**: short precision 10 | 11 | - Glosses the text sentences linearly (sometimes useful for glossing entire sentences): 12 | - **?s** or **?sgloss**: regular precision 13 | - **?sfull**: full precision 14 | - **?sshort**: short precision 15 | 16 | > Command: ?gloss lalo 17 | > 18 | > Reply: **lalo:** __S1__-“adult human“-ERG 19 | 20 | By default, the commands don't show default values. To show all values, use the prefix "??" (e.g. "??gloss") instead of "?". 21 | 22 | For glossing short examples amidst text, any text in between ":?" and "?:" is glossed linearly with regular precision (equivalent to **?s**). Multiple such glosses can be used in one message. 23 | 24 | Precision: 25 | - __Regular precision__: all morphological components except affixes are abbreviated 26 | - __Full precision__: all morphological components are completely written out 27 | - __Short precision__: all morphological components including affixes are abbreviated, roots will only display their generic title 28 | 29 | Other commands: 30 | - **?root**, **?affix**: look up the definition of a root or affix by its consonantal value 31 | - **?!reload**: updates the root and affix dictionaries from the spreadsheet 32 | - **?date**: gives the current UTC time and date in Ithkuil IV 33 | - **?ej**: checks a text for violations of External Juncture (Sec. 1.5) 34 | - **?whosacutebot**: tells the bot that it is such a cute bot 35 | 36 | If you edit or delete your message within a minute of posting it, the bot's reply will also be edited or deleted. You can also delete a message by the bot that is a reply to you (or where the original message is deleted) by reacting to it with an ``:x:`` emoji. 37 | 38 | Formatting details: 39 | - Bold text in place of a root/affix means that it was not found in the current database 40 | - Underlined text means that the category was taken into account when looking for a description of the root. 41 | For example, "S2-'description' " indicates that S2 contributed nothing to the final result of 'description'; However, "__S2__-'description'" indicates that 'description' was specifically picked because S2 was specified. 42 | 43 | For glossing Ithkuil III, there is the command "!gloss". This is a separate system which is not maintained by us (or anyone). 44 | 45 | The bot is an amateur coding project, and may suffer from severe bugs at any time. If you spot one, please contact me (@IshtarAletheia#0347), so I can hopefully go about fixing it. 46 | 47 | GitHub repo at https://github.com/ngoriyasjil/IthkuilGloss 48 | 49 | Crowd-sourced dictionary used by the bot at https://docs.google.com/spreadsheets/d/1JdaG1PaSQJRE2LpILvdzthbzz1k_a0VT86XSXouwGy8/edit?usp=sharing 50 | -------------------------------------------------------------------------------- /test/SlotTests.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss.test 2 | 3 | import ithkuil.iv.gloss.* 4 | import kotlin.test.* 5 | 6 | class SlotTests { 7 | 8 | @Test 9 | fun `Cc parses only for values in CC_CONSONANTS`() { 10 | CC_CONSONANTS.forEach { it isNotCcOf (null to null) } 11 | (CN_CONSONANTS - CC_CONSONANTS).forEach { it isCcOf (null to null) } 12 | } 13 | 14 | @Test 15 | fun `Cc examples`() { 16 | "hl" isCcOf (Concatenation.TYPE_ONE to Shortcut.W) 17 | "y" isCcOf (null to Shortcut.Y) 18 | "hw" isCcOf (Concatenation.TYPE_TWO to null) 19 | } 20 | 21 | @Test 22 | fun `Special Vv parses for all values in SPECIAL_VV_VOWELS`() { 23 | SPECIAL_VV_VOWELS.forEach { 24 | assertNotNull(parseSpecialVv(it, null), it) 25 | } 26 | } 27 | 28 | @Test 29 | fun `Vv examples with no shortcut`() { 30 | parseVv("a", null) hasGlossOf "S1" 31 | parseVv("ö", null) hasGlossOf "S0.CPT" 32 | parseVv("ua", null) hasGlossOf "S3.**t**/4" 33 | parseVv("oë", null) hasGlossOf "CPT.DYN" 34 | parseVv("ae", null) hasGlossOf "" 35 | } 36 | 37 | @Test 38 | fun `Vv examples with shortcuts`() { 39 | parseVv("io", Shortcut.W) hasGlossOf "S2.N" 40 | parseVv("io", Shortcut.Y) hasGlossOf "S2.A" 41 | parseVv("ae", Shortcut.Y) hasGlossOf "PRX" 42 | } 43 | 44 | @Test 45 | fun `Normal Vr examples`() { 46 | parseVr("ä") hasGlossOf "CTE" 47 | parseVr("o") hasGlossOf "DYN.CSV" 48 | parseVr("öe") hasGlossOf "DYN.OBJ.AMG" 49 | } 50 | 51 | @Test 52 | fun `Affix Vr examples`() { 53 | parseAffixVr("a") hasGlossOf "D1" 54 | parseAffixVr("ai") hasGlossOf "D1.FNC" 55 | parseAffixVr("ae") hasGlossOf "D0" 56 | parseAffixVr("ea") hasGlossOf "D0.FNC" 57 | parseAffixVr("üo") hasGlossOf "D0.RPS" 58 | parseAffixVr("üö") hasGlossOf "D0.AMG" 59 | } 60 | 61 | @Test 62 | fun `Ca examples`() { 63 | parseCa("l") hasGlossOf "" 64 | parseCa("s") hasGlossOf "DPX" 65 | parseCa("nļ") hasGlossOf "ASO" 66 | parseCa("tļ") hasGlossOf "RPV" 67 | parseCa("řktç") hasGlossOf "VAR.MSC.PRX.A.RPV" 68 | parseCa("nš") hasGlossOf "COA.G.RPV" 69 | parseCa("zḑ") hasGlossOf "MFS.DPL.A.RPV" 70 | parseCa("nx") hasGlossOf "MSC.GRA.N.RPV" 71 | } 72 | 73 | @Test 74 | fun `VnCn parses for all values in CN_CONSONANTS`() { 75 | CN_CONSONANTS.forEach { cn -> 76 | assertNotNull(parseVnCn("a", cn)) 77 | } 78 | } 79 | 80 | @Test 81 | fun `VnCn examples`() { 82 | parseVnCn("i", "h") hasGlossOf "RCP" 83 | parseVnCn("i", "w") hasGlossOf "PRG" 84 | parseVnCn("ai", "hl") hasGlossOf "PCT.SUB" 85 | parseVnCn("ia", "hl", marksMood = false) hasGlossOf "1:BEN.CCA" 86 | parseVnCn("ao", "h") hasGlossOf "MIN" 87 | parseVnCn("ao", "hňw") hasGlossOf "DCL.HYP" 88 | parseVnCn("ao", "h", absoluteLevel = true) hasGlossOf "MINa" 89 | } 90 | 91 | @Test 92 | fun `Absolute Level fails with other vowel series`() { 93 | assertNull(parseVnCn("a", "h", absoluteLevel = true)) 94 | assertNull(parseVnCn("ai", "h", absoluteLevel = true)) 95 | assertNull(parseVnCn("ia", "h", absoluteLevel = true)) 96 | } 97 | } 98 | 99 | infix fun String.isCcOf(pair: Pair) { 100 | val gloss = parseCc(this) 101 | assertEquals(pair, gloss, this) 102 | } 103 | 104 | infix fun String.isNotCcOf(illegal: Pair) { 105 | val gloss = parseCc(this) 106 | assertNotEquals(illegal, gloss, this) 107 | } 108 | 109 | infix fun Glossable?.hasGlossOf(expected: String) { 110 | val gloss = this?.gloss(GlossOptions()) 111 | assertEquals(expected, gloss) 112 | } -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/Context.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | import ithkuil.iv.gloss.dispatch.logger 4 | 5 | fun glossInContext(words: List): List> { 6 | val glossPairs = mutableListOf>() 7 | 8 | var withinQuotes = false 9 | var followsCarrier = false 10 | var terminatedPhrase = false 11 | 12 | for ((index, maybeWord) in words.withIndex()) { 13 | 14 | val gloss: ContextOutcome 15 | 16 | val word: Valid = when (maybeWord) { 17 | is Invalid -> { 18 | glossPairs.add(maybeWord.toString() to Error(maybeWord.message)) 19 | continue 20 | } 21 | 22 | is Valid -> maybeWord 23 | } 24 | 25 | if (followsCarrier && word.prefixPunctuation matches "[:⫶]".toRegex()) withinQuotes = true 26 | if (terminatedPhrase && word as? Word == listOf("h", "ü")) terminatedPhrase = false 27 | 28 | 29 | if (!followsCarrier && !withinQuotes && !terminatedPhrase) { 30 | if (isCarrier(word)) { 31 | followsCarrier = true 32 | 33 | val hasTerminator = words.subList(index + 1, words.size) 34 | .mapNotNull { it as? Valid } 35 | .takeWhile { !(it.postfixPunctuation matches "[.?!]+".toRegex()) } 36 | .any { isTerminator(it) } 37 | 38 | if (hasTerminator) terminatedPhrase = true 39 | 40 | } 41 | 42 | val nextFormativeIsVerbal: Boolean? by lazy { 43 | words.subList(index + 1, words.size) 44 | .mapNotNull { it as? Valid } 45 | .takeWhile { !(it.postfixPunctuation matches "[.?!]+".toRegex()) } 46 | .find { 47 | when (it) { 48 | is ConcatenatedWords -> true 49 | is Word -> it.wordType == WordType.FORMATIVE 50 | } 51 | } 52 | ?.let { isVerbal(it) } 53 | } 54 | 55 | gloss = try { 56 | 57 | when (word) { 58 | is Word -> parseWord(word, marksMood = nextFormativeIsVerbal) 59 | is ConcatenatedWords -> parseConcatenationChain(word) 60 | } 61 | 62 | } catch (ex: Exception) { 63 | logger.error("", ex) 64 | Error("Something went wrong in glossing this word. Please contact the maintainers.") 65 | } 66 | 67 | } else { 68 | 69 | gloss = Foreign(word.toString()) 70 | 71 | if (followsCarrier) followsCarrier = false 72 | if (isTerminator(word)) terminatedPhrase = false 73 | if (withinQuotes && word.postfixPunctuation matches "[:⫶]".toRegex()) withinQuotes = false 74 | } 75 | 76 | glossPairs.add(word.toString() to gloss) 77 | 78 | } 79 | 80 | return glossPairs 81 | 82 | } 83 | 84 | fun isVerbal(word: Valid): Boolean = when (word) { 85 | is ConcatenatedWords -> isVerbal(word.words.last()) 86 | is Word -> { 87 | (word.wordType == WordType.FORMATIVE) 88 | && (word.stress in setOf(Stress.ULTIMATE, Stress.MONOSYLLABIC)) 89 | } 90 | } 91 | 92 | private fun isTerminator(word: Valid) = word == listOf("h", "ü") || word.prefixPunctuation == LOW_TONE_MARKER 93 | 94 | fun isCarrier(word: Valid): Boolean { 95 | 96 | return when (word) { 97 | is ConcatenatedWords -> word.words.any { isCarrier(it) } 98 | 99 | is Word -> when (word.wordType) { 100 | WordType.FORMATIVE -> { 101 | val rootIndex = when { 102 | word.getOrNull(0) in CC_CONSONANTS -> 2 103 | word.getOrNull(0)?.isVowel() == true -> 1 104 | else -> 0 105 | } 106 | 107 | if (word.getOrNull(rootIndex - 1) in SPECIAL_VV_VOWELS) return false 108 | 109 | word.getOrNull(rootIndex) == CARRIER_ROOT_CR 110 | } 111 | 112 | WordType.REFERENTIAL, WordType.COMBINATION_REFERENTIAL -> 113 | setOf("hl", "hn", "hň").any { it in word } //Quotative adjunct "hm" is not included 114 | 115 | else -> false 116 | } 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /test/WordTests.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss.test 2 | 3 | import ithkuil.iv.gloss.* 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class WordTests { 8 | 9 | @Test 10 | fun `Poem test`() { 11 | "yužgrá" glossesTo "S3.PRX-**žgr**-OBS" 12 | "eolaleici" glossesTo "S2.**t**/5-**l**-**c**/3₂-AFF" 13 | "hlamröé-úçtļořëi" glossesTo "T1-S1-**mr**-PCR—S3-**çtļ**-DYN.CSV-G.RPV-STM\\FRA" 14 | "khe" glossesTo "Rdp.DET-ABS" 15 | "adni'lö" glossesTo "S1-**dn**-OBJ-UTL" 16 | "yeilaiceu" glossesTo "S2.RPV-**l**-**c**/1₂-ATT" 17 | "aiňļa'vu" glossesTo "S1.**r**/4-**ňļ**-N-RLT" 18 | } 19 | 20 | @Test 21 | fun `Slot V marking matches actual number of slot V affixes`() { 22 | "alarfull" glossesTo "S1-**l**-**rf**/9₁-{Ca}" 23 | "a'larfunall" glossesTo "S1-**l**-**rf**/9₁-**n**/1₁-{Ca}" 24 | "wa'lena" givesError "Unexpectedly few slot V affixes" 25 | "waršana'anera" givesError "Unexpectedly many slot V affixes" 26 | } 27 | 28 | @Test 29 | fun `The Vc glottal stop can be moved`() { 30 | "lala'a" glossesTo "S1-**l**-PRN" 31 | "la'la" glossesTo "S1-**l**-PRN" 32 | "wala'ana" glossesTo "S1-**l**-**n**/1₁-{Ca}" 33 | "halala'a-alal" givesError "Unexpected glottal stop in concatenated formative" 34 | "a'lananalla'a" glossesTo "S1-**l**-**n**/1₁-**n**/1₁-{Ca}-PRN" 35 | "a'la'nanalla" glossesTo "S1-**l**-**n**/1₁-**n**/1₁-{Ca}-PRN" 36 | "a'la'nanalla'a" givesError "Too many glottal stops" 37 | } 38 | 39 | @Test 40 | fun `Cs root formative examples`() { 41 | "ëilal" glossesTo "**l**/1-D1" 42 | "oërmölá" glossesTo "CPT.DYN-**rm**/6-D6-OBS" 43 | "oërmoulá" glossesTo "CPT.DYN-**rm**/6-D6.FNC-OBS" 44 | } 45 | 46 | @Test 47 | fun `Cs root Vvs return an error with shortcuts`() { 48 | "wëil" givesError "Shortcuts can't be used with a Cs-root" 49 | } 50 | 51 | @Test 52 | fun `One example of each word type parses correctly`() { 53 | "lalu" glossesTo "S1-**l**-IND" 54 | "ihnú" glossesTo "RCP.COU-{under adj.}" 55 | "äst" glossesTo "**st**/2₁" 56 | "miyüs" glossesTo "ma-AFF-DAT-2m" 57 | "mixenüa" glossesTo "ma-AFF-**n**/3₁-THM" 58 | "ha" glossesTo "DSV" 59 | "pļļ" glossesTo "“Funny!“" 60 | "hrei" glossesTo "CCA" 61 | } 62 | 63 | 64 | @Test 65 | fun `Stress-marked categories are glossed with a slash`() { 66 | "lála'a" glossesTo "S1-**l**-PRN\\FRA" 67 | "layá" glossesTo "1m-THM-THM\\RPV" 68 | } 69 | 70 | @Test 71 | fun `Sentence prefix examples`() { 72 | "çëlal" glossesTo "[sentence:]-S1-**l**" 73 | "çalal" glossesTo "[sentence:]-S1-**l**" 74 | "çwala" glossesTo "[sentence:]-S1-**l**" 75 | "ççala" glossesTo "[sentence:]-S1.PRX-**l**" 76 | "çëhamala-lala" glossesTo "[sentence:]-T1-S1-**m**—S1-**l**" 77 | "hamala-çëlala" givesError "Sentence prefix inside concatenation chain" 78 | } 79 | 80 | @Test 81 | fun `Combination referentials are distinguished from similar-looking formatives`() { 82 | "ţnaxekka" glossesTo "S1-**ţn**-**x**/3₁-MSC" 83 | "ţnaxeka" glossesTo "[mi.BEN+2p]-**k**/3₁" 84 | } 85 | } 86 | 87 | infix fun String.glossesTo(gloss: String) { 88 | 89 | val parse = when (val word = formatWord(this)) { 90 | is Word -> parseWord(word) 91 | is ConcatenatedWords -> parseConcatenationChain(word) 92 | is Invalid -> throw AssertionError(word.message) 93 | } 94 | 95 | val (result, message) = when (parse) { 96 | is Error -> null to "Error: ${parse.message}" 97 | is Parsed -> parse.gloss(GlossOptions()) to this 98 | } 99 | 100 | assertEquals(gloss, result, message) 101 | } 102 | 103 | infix fun String.givesError(error: String) { 104 | 105 | val parse = when (val word = formatWord(this)) { 106 | is Word -> parseWord(word) 107 | is ConcatenatedWords -> parseConcatenationChain(word) 108 | is Invalid -> throw AssertionError(word.message) 109 | } 110 | 111 | val (result, message) = when (parse) { 112 | is Error -> parse.message to this 113 | is Parsed -> null to parse.gloss(GlossOptions()) 114 | } 115 | 116 | assertEquals(error, result, message) 117 | } -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/Constants.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | const val SLOT_SEPARATOR = "-" 4 | const val CATEGORY_SEPARATOR = "." 5 | const val AFFIX_DEGREE_SEPARATOR = "/" 6 | const val STRESS_SLOT_SEPARATOR = "\\" 7 | const val REFERENT_SEPARATOR = "+" 8 | const val REFERENT_START = "[" 9 | const val REFERENT_END = "]" 10 | const val CONCATENATION_SEPARATOR = "—" 11 | const val LOW_TONE_MARKER = "_" 12 | 13 | const val CA_STACKING_VOWEL = "üö" 14 | const val IVL_CS = "nļ" 15 | const val CARRIER_ROOT_CR = "s" 16 | 17 | val ITHKUIL_CHARS = setOf( 18 | "p", "b", "t", "d", "k", "g", "f", "v", "ţ", "ḑ", "s", "z", "š", "ž", "ç", "x", "h", "ļ", 19 | "c", "ẓ", "č", "j", "m", "n", "ň", "r", "l", "w", "y", "ř", 20 | "a", "ä", "e", "ë", "i", "u", "ü", "o", "ö", 21 | "á", "â", "é", "ê", "í", "ú", "û", "ó", "ô", 22 | "'", "-" 23 | ) 24 | 25 | //Substitutions: 26 | 27 | val ALLOGRAPHS = listOf( 28 | "\u200B" to "", 29 | "’" to "'", // U+2019 RIGHT SINGLE QUOTATION MARK (used by JQ and others and cross-linguistically) 30 | "ʼ" to "'", // U+02BC MODIFIER LETTER APOSTROPHE (used by uagle and cross-linguistically) 31 | "á" to "á", 32 | "ä" to "ä", "â" to "â", 33 | "é" to "é", 34 | "ë" to "ë", "ê" to "ê", 35 | "[ìı]|ì" to "i", "í" to "í", 36 | "ó" to "ó", "ö" to "ö", "ô" to "ô", 37 | "ù|ù" to "u", "ú" to "ú", "ü" to "ü", "û" to "û", 38 | "č" to "č", 39 | "ç" to "ç", "[ṭŧț]|ţ|ṭ" to "ţ", 40 | "[ḍđ]|ḍ|ḑ" to "ḑ", 41 | "[łḷ]|ḷ|ļ" to "ļ", 42 | "š" to "š", 43 | "ž" to "ž", 44 | "ż|ẓ" to "ẓ", 45 | "ṇ|ň|ņ|ṇ" to "ň", 46 | "ṛ|ř|ŗ|r͕|ŗ|ṛ" to "ř", 47 | ) 48 | 49 | private const val unvoiced = "stckpţfçšč" 50 | 51 | val CA_DESUBSTITUTIONS = listOf( 52 | "ḑy" to "ţţ", "vw" to "ff", 53 | "(?<=[$unvoiced])ţ|(?<=[^$unvoiced])ḑ" to "bn", 54 | "(?<=[$unvoiced])f|(?<=[^$unvoiced])v" to "bm", 55 | "\\Bxw" to "çx", "ňn" to "ngn", "\\Bň" to "gn", "\\Bx" to "gm", 56 | "ňš" to "řř", "ňs" to "řr", "nš" to "rř", "ns" to "rr", 57 | "nd" to "çy", "ng" to "kg", "mb" to "pb", 58 | "pļ" to "ll", "nk" to "kk", "nt" to "tt", "mp" to "pp" 59 | ) 60 | 61 | val CA_DEGEMINATIONS = mapOf( 62 | "jjn" to "dn", "jjm" to "dm", "gžžn" to "gn", "gžžm" to "gm", "bžžn" to "bn", "bžžm" to "bm", 63 | "ḑḑn" to "tn", "ḑḑm" to "tm", "xxn" to "kn", "xxm" to "km", "vvn" to "pn", "vmm" to "pm", 64 | "ddv" to "tp", "ḑvv" to "tk", "ggv" to "kp", "ggḑ" to "kt", "bbv" to "pk", "bbḑ" to "pt", 65 | ) 66 | 67 | val UNSTRESSED_FORMS = listOf( 68 | "á" to "a", "â" to "ä", 69 | "é" to "e", "ê" to "ë", 70 | "í" to "i", 71 | "ô" to "ö", "ó" to "o", 72 | "û" to "ü", "ú" to "u" 73 | ) 74 | 75 | //Vowels 76 | 77 | val VOWEL_FORMS = listOf( 78 | "a", "ä", "e", "i", "ëi", "ö", "o", "ü", "u", 79 | "ai", "au", "ei", "eu", "ëu", "ou", "oi", "iu", "ui", 80 | "ia/uä", "ie/uë", "io/üä", "iö/üë", "eë", "uö/öë", "uo/öä", "ue/ië", "ua/iä", 81 | "ao", "aö", "eo", "eö", "oë", "öe", "oe", "öa", "oa", 82 | ) 83 | 84 | val VOWELS = setOf( 85 | 'a', 'ä', 'e', 'ë', 'i', 'ö', 'o', 'ü', 'u', 86 | 'á', 'â', 'é', 'ê', 'í', 'ú', 'û', 'ó', 'ô', 87 | ) 88 | 89 | val VOWELS_AND_GLOTTAL_STOP = VOWELS + '\'' 90 | 91 | val DIPHTHONGS = setOf("ai", "äi", "ei", "ëi", "oi", "öi", "ui", "au", "eu", "ëu", "ou", "iu") 92 | 93 | val DEGREE_ZERO_CS_ROOT_FORMS = setOf("ae", "ea", "äi", "öi") 94 | 95 | val SPECIAL_VV_VOWELS = setOf( 96 | "ëi", "eë", "ëu", "oë", 97 | "ae", "ea", 98 | ) 99 | 100 | //Consonants 101 | 102 | val CONSONANTS = listOf( 103 | 'p', 'b', 't', 'd', 'k', 'g', 'f', 'v', 'ţ', 'ḑ', 's', 'z', 'š', 'ž', 'ç', 'x', 'h', 'ļ', 104 | 'c', 'ẓ', 'č', 'j', 'm', 'n', 'ň', 'r', 'l', 'w', 'y', 'ř' 105 | ) 106 | 107 | val CC_CONSONANTS = setOf("w", "y", "h", "hl", "hm", "hw", "hr", "hn") 108 | 109 | val CP_CONSONANTS = setOf("hl", "hm", "hn", "hň") 110 | 111 | val COMBINATION_REFERENTIAL_SPECIFICATION = listOf("x", "xt", "xp", "xx") 112 | 113 | val BICONSONANTAL_REFERENTIALS = setOf( 114 | "tļ", 115 | "th", "ph", "kh", 116 | "ll", "rr", "řř", 117 | "mm", "nn", "ňň", 118 | "hl", "hm", "hn", "hň" 119 | ) 120 | 121 | val CASE_AFFIXES = setOf( 122 | "sw", "zw", "čw", "šw", "žw", "jw", "lw", 123 | "sy", "zy", "čy", "šy", "žy", "jy", "ly" 124 | ) 125 | 126 | val CN_CONSONANTS = setOf( 127 | "h", "hl", "hr", "hm", "hn", "hň", 128 | "w", "y", "hw", "hrw", "hmw", "hnw", "hňw" 129 | ) 130 | 131 | val CN_PATTERN_ONE = setOf( 132 | "h", "hl", "hr", "hm", "hn", "hň" 133 | ) 134 | 135 | val CZ_CONSONANTS = setOf("h", "'h", "'hl", "'hr", "hw", "'hw") 136 | 137 | val INVALID_ROOT_FORMS = setOf("ļ", "ç", "çç", "çw", "w", "y") 138 | 139 | //Other 140 | 141 | val SENTENCE_START_GLOSS = GlossString("[sentence start]", "[sentence:]", "[S]") 142 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | ithkuil.iv.gloss 6 | ithkuilgloss 7 | 1.1.0-1.0.0 8 | 9 | ithkuilgloss 10 | https://github.com/ngoriyasjil/IthkuilGloss 11 | 12 | 13 | UTF-8 14 | 1.7.20 15 | true 16 | 1.8 17 | ithkuil.iv.gloss.interfaces.DiscordKt 18 | 19 | 20 | 21 | 22 | jcenter 23 | jcenter-bintray 24 | https://jcenter.bintray.com 25 | 26 | 27 | 28 | bintray-kordlib-Kord 29 | https://dl.bintray.com/kordlib/Kord 30 | 31 | 32 | 33 | snapshots-repo 34 | https://oss.sonatype.org/content/repositories/snapshots 35 | false 36 | true 37 | 38 | 39 | 40 | 41 | 42 | 43 | org.jetbrains.kotlin 44 | kotlin-test-junit 45 | ${kotlin.version} 46 | test 47 | 48 | 49 | 50 | ch.qos.logback 51 | logback-classic 52 | 1.3.4 53 | 54 | 55 | 56 | org.slf4j 57 | slf4j-api 58 | 2.0.3 59 | 60 | 61 | 62 | io.github.microutils 63 | kotlin-logging-jvm 64 | 3.0.0 65 | 66 | 67 | 68 | org.jetbrains.kotlin 69 | kotlin-stdlib-jdk8 70 | ${kotlin.version} 71 | 72 | 73 | 74 | dev.kord 75 | kord-core 76 | 0.8.0-M15 77 | 78 | 79 | 80 | org.jetbrains.kotlin 81 | kotlin-maven-plugin 82 | ${kotlin.version} 83 | provided 84 | 85 | 86 | 87 | org.jetbrains.kotlinx 88 | kotlinx-datetime-jvm 89 | 0.4.0 90 | 91 | 92 | 93 | 94 | 95 | ${project.basedir}/src 96 | ${project.basedir}/test 97 | 98 | 99 | 100 | ${project.basedir}/logback 101 | 102 | 103 | 104 | 105 | 106 | maven-clean-plugin 107 | 3.1.0 108 | 109 | 110 | org.jetbrains.kotlin 111 | kotlin-maven-plugin 112 | ${kotlin.version} 113 | 114 | 115 | 116 | compile 117 | 118 | compile 119 | 120 | 121 | 122 | 123 | test-compile 124 | 125 | test-compile 126 | 127 | 128 | 129 | 130 | 131 | 132 | -opt-in=kotlin.RequiresOptIn 133 | -Xuse-ir 134 | 135 | 136 | 137 | 138 | 139 | org.apache.maven.plugins 140 | maven-assembly-plugin 141 | 2.6 142 | 143 | 144 | make-assembly 145 | package 146 | 147 | single 148 | 149 | 150 | 151 | 152 | ${main.class} 153 | 154 | 155 | 156 | jar-with-dependencies 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/WordTypes.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | fun wordTypeOf(word: Word): WordType { 4 | 5 | return when { 6 | pattern(word) { 7 | consonant() 8 | } -> WordType.BIAS_ADJUNCT 9 | 10 | pattern(word) { 11 | +"hr" 12 | vowel() 13 | } -> WordType.MOOD_CASESCOPE_ADJUNCT 14 | 15 | pattern(word) { 16 | +"h" 17 | vowel() 18 | } -> WordType.REGISTER_ADJUNCT 19 | 20 | pattern(word) { 21 | maybe("w", "y") 22 | confirm { it !in setOf("ë", "äi") } 23 | repeat(3) { 24 | maybe { 25 | vowel() 26 | oneOf(CN_CONSONANTS) 27 | } 28 | } 29 | vowel() 30 | } -> WordType.MODULAR_ADJUNCT 31 | 32 | pattern(word) { 33 | either( 34 | { 35 | maybe("ë") 36 | consonant() 37 | }, 38 | { 39 | +"a" 40 | oneOf(CP_CONSONANTS) 41 | } 42 | ) 43 | vowel() 44 | oneOf(COMBINATION_REFERENTIAL_SPECIFICATION) 45 | test(slots.none { it.isConsonant() && it.isGeminateCa() }) 46 | tail() 47 | } -> WordType.COMBINATION_REFERENTIAL 48 | 49 | pattern(word) { 50 | confirm { it != "ë" } 51 | vowel() 52 | consonant() 53 | maybe { vowel() } 54 | } -> WordType.AFFIXUAL_ADJUNCT 55 | 56 | pattern(word) { 57 | maybe("ë") 58 | consonant() 59 | val czGlottal = current()?.endsWith("'") ?: false 60 | if (czGlottal) modify { it.removeSuffix("'") } 61 | vowel() 62 | if (czGlottal) modify { "'$it" } 63 | oneOf(CZ_CONSONANTS) 64 | vowel() 65 | consonant() 66 | tail() 67 | } -> WordType.MULTIPLE_AFFIX_ADJUNCT 68 | 69 | pattern(word) { 70 | maybe("ë", "äi") 71 | referentialConsonant() 72 | while (current() == "ë") { 73 | +"ë" 74 | referentialConsonant() 75 | } 76 | vowel() 77 | maybe { 78 | oneOf("w", "y") 79 | vowel() 80 | maybe { 81 | referentialConsonant() 82 | maybe("ë") 83 | } 84 | } 85 | } -> WordType.REFERENTIAL 86 | 87 | else -> WordType.FORMATIVE 88 | } 89 | } 90 | 91 | 92 | fun pattern(word: Word, pattern: Matcher.() -> Unit): Boolean { 93 | val matcher = Matcher(word) 94 | matcher.pattern() 95 | if (matcher.slots.isNotEmpty()) { 96 | matcher.matching = false 97 | } 98 | return matcher.matching 99 | } 100 | 101 | class Matcher(var slots: List, var matching: Boolean = true) { 102 | 103 | fun current() = slots.getOrNull(0) 104 | 105 | private fun fulfills(c: (String) -> Boolean) { 106 | if (!matching) return 107 | 108 | if (current()?.let { c(it) } == true) { 109 | slots = slots.drop(1) 110 | } else matching = false 111 | } 112 | 113 | operator fun String.unaryPlus() = fulfills { it == this } 114 | 115 | fun vowel() = fulfills { it.isVowel() } 116 | 117 | fun consonant() = fulfills { it !in CN_CONSONANTS && it.isConsonant() } 118 | 119 | fun referentialConsonant() = fulfills { it in CP_CONSONANTS || (it.isConsonant() && it !in CN_CONSONANTS) } 120 | 121 | fun oneOf(set: Collection) = fulfills { it in set } 122 | 123 | fun oneOf(vararg options: String) = fulfills { it in options } 124 | 125 | fun maybe(pattern: Matcher.() -> Unit) { 126 | if (!matching) return 127 | 128 | val fork = Matcher(slots, true) 129 | 130 | fork.pattern() 131 | 132 | if (fork.matching) this.slots = fork.slots 133 | } 134 | 135 | fun maybe(vararg options: String) = maybe { oneOf(*options) } 136 | 137 | fun either(first: Matcher.() -> Unit, second: Matcher.() -> Unit) { 138 | if (!matching) return 139 | 140 | val fork1 = Matcher(slots, true) 141 | val fork2 = Matcher(slots, true) 142 | 143 | fork1.first() 144 | fork2.second() 145 | 146 | when { 147 | fork1.matching -> this.slots = fork1.slots 148 | fork2.matching -> this.slots = fork2.slots 149 | else -> matching = false 150 | } 151 | } 152 | 153 | fun tail() { 154 | if (!matching) return 155 | 156 | slots = emptyList() 157 | } 158 | 159 | fun confirm(predicate: (String) -> Boolean) { 160 | if (!matching) return 161 | 162 | if (current()?.let { predicate(it) } != true) matching = false 163 | } 164 | 165 | fun test(claim: Boolean) { 166 | if (!matching) return 167 | 168 | if (!claim) matching = false 169 | } 170 | 171 | fun modify(transform: (String) -> String) { 172 | if (!matching) return 173 | 174 | val cur = current() 175 | 176 | if (cur != null) { 177 | slots = listOf(transform(cur)) + slots.drop(1) 178 | } else { 179 | matching = false 180 | } 181 | 182 | } 183 | } 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/interfaces/Discord.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss.interfaces 2 | 3 | import dev.kord.common.annotation.KordPreview 4 | import dev.kord.core.Kord 5 | import dev.kord.core.behavior.channel.MessageChannelBehavior 6 | import dev.kord.core.behavior.edit 7 | import dev.kord.core.behavior.reply 8 | import dev.kord.core.entity.Message 9 | import dev.kord.core.entity.ReactionEmoji 10 | import dev.kord.core.entity.User 11 | import dev.kord.core.event.message.MessageCreateEvent 12 | import dev.kord.core.event.message.MessageUpdateEvent 13 | import dev.kord.core.event.message.ReactionAddEvent 14 | import dev.kord.core.live.live 15 | import dev.kord.core.live.on 16 | import dev.kord.core.on 17 | import dev.kord.gateway.Intent 18 | import dev.kord.gateway.PrivilegedIntent 19 | import dev.kord.rest.request.RestRequestException 20 | import ithkuil.iv.gloss.dispatch.loadResourcesOnline 21 | import ithkuil.iv.gloss.dispatch.logger 22 | import kotlinx.coroutines.delay 23 | import kotlinx.coroutines.job 24 | import kotlinx.coroutines.runBlocking 25 | import java.io.File 26 | import kotlin.coroutines.cancellation.CancellationException 27 | import ithkuil.iv.gloss.dispatch.respond as terminalRespond 28 | 29 | @OptIn(PrivilegedIntent::class) 30 | @KordPreview 31 | suspend fun main() { 32 | val token = File("./resources/token.txt").readLines().first() 33 | val kord = Kord(token) 34 | kord.on { 35 | logger.debug { "Saw a message: \"${message.content}\"" } 36 | replyAndTrackChanges() 37 | } 38 | 39 | kord.on { 40 | 41 | val messag = message.asMessageOrNull() ?: return@on 42 | if (messag.author != kord.getSelf()) return@on 43 | if (emoji != ReactionEmoji.Unicode("\u274C")) return@on 44 | if (messag.referencedMessage != null && user != messag.referencedMessage?.author) return@on 45 | 46 | messag.delete() 47 | } 48 | 49 | loadResourcesOnline() 50 | kord.login { 51 | intents { 52 | +Intent.MessageContent 53 | +Intent.GuildMessages 54 | +Intent.GuildMessageReactions 55 | +Intent.DirectMessages 56 | +Intent.DirectMessagesReactions 57 | } 58 | presence { playing("?help for info") } 59 | logger.info { "Logged in!" } 60 | } 61 | } 62 | 63 | @KordPreview 64 | private suspend fun MessageCreateEvent.replyAndTrackChanges() { 65 | val response = message.respondTo() ?: return 66 | 67 | val liveMessage = message.live() 68 | 69 | 70 | liveMessage.on { 71 | with(liveMessage.message) { 72 | val replyTo = message.referencedMessage?.content 73 | 74 | logger.debug { "replyTo: $replyTo" } 75 | 76 | val contentWithReply = if (replyTo != null && content matches "^\\S*$".toRegex()) { 77 | "$content $replyTo" 78 | } else { 79 | content 80 | } 81 | 82 | val editTo = terminalRespond(contentWithReply)?.splitMessages()?.first() ?: "*Unknown invocation*" 83 | 84 | logger.info { "Edited a message to $editTo responding to $contentWithReply" } 85 | 86 | response.edit { 87 | content = editTo 88 | } 89 | } 90 | } 91 | 92 | liveMessage.coroutineContext.job.invokeOnCompletion { 93 | if ((it as? CancellationException)?.message == "Minute passed") return@invokeOnCompletion 94 | logger.debug { "Original message deleted" } 95 | runBlocking { response.delete("Original message deleted") } 96 | } 97 | 98 | delay(60000) 99 | 100 | liveMessage.shutDown(CancellationException("Minute passed")) 101 | } 102 | 103 | suspend fun Message.respondTo(): Message? { 104 | val user = author ?: return null 105 | if (user.isBot || !(content.startsWith("?") || content.contains(":?"))) return null 106 | if (content == "?help") { 107 | sendHelp(user, channel) 108 | return null 109 | } 110 | 111 | val replyTo = referencedMessage?.content 112 | 113 | val contentWithReply = if (replyTo != null && content matches "^\\S*$".toRegex()) { 114 | logger.info { "-> respond($content) replying to $replyTo" } 115 | "$content $replyTo" 116 | } else { 117 | logger.info { "-> respond($content)" } 118 | content 119 | } 120 | 121 | val replies = mutableListOf() 122 | 123 | terminalRespond(contentWithReply) 124 | .also { logger.info { " respond($content) ->\n$it" } } 125 | ?.splitMessages() 126 | ?.forEach { 127 | val r = reply { content = it } 128 | replies.add(r) 129 | } 130 | 131 | return replies[0] 132 | } 133 | 134 | suspend fun sendHelp(helpee: User, channel: MessageChannelBehavior) { 135 | logger.info { "-> sendHelp(${helpee.tag})" } 136 | 137 | val helpMessages = File("./resources/help.md") 138 | .readText() 139 | .splitMessages() 140 | 141 | try { 142 | val dmChannel = helpee.getDmChannel() 143 | helpMessages.forEach { dmChannel.createMessage(it) } 144 | } catch (e: RestRequestException) { 145 | channel.createMessage( 146 | "Couldn't send help your way, ${helpee.mention}! " + 147 | "Your DMs might be disabled for this server." 148 | ) 149 | return 150 | } 151 | 152 | 153 | channel.createMessage("Help sent your way, ${helpee.mention}!") 154 | } 155 | 156 | fun String.splitMessages(): Sequence = sequence { 157 | val remainder = lineSequence().fold(StringBuilder()) { current, line -> 158 | if (current.length + line.length + 1 > 2000) { 159 | yield(current.toString()) 160 | StringBuilder().appendLine(line) 161 | } else current.appendLine(line) 162 | } 163 | yieldAll(remainder.chunkedSequence(2000)) 164 | } 165 | -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/Structural.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | interface Resources { 4 | fun getAffix(cs: String): AffixData? 5 | fun getRoot(cr: String): RootData? 6 | } 7 | 8 | data class AffixData(val abbreviation: String, val descriptions: List) { 9 | operator fun get(degree: Degree) = descriptions[degree.ordinal] 10 | } 11 | 12 | data class RootData(val descriptions: List) { 13 | operator fun get(stem: Stem) = descriptions[stem.ordinal] 14 | } 15 | 16 | 17 | interface Glossable { 18 | fun gloss(o: GlossOptions): String 19 | fun checkDictionary(r: Resources): Glossable = this 20 | } 21 | 22 | interface Category : Glossable { 23 | val ordinal: Int 24 | val name: String 25 | val short: String 26 | 27 | override fun gloss(o: GlossOptions) = when { 28 | !o.includeDefaults && ordinal == 0 -> "" 29 | o.verbose -> name.lowercase() 30 | else -> short 31 | } 32 | } 33 | 34 | interface NoDefault : Category { 35 | override fun gloss(o: GlossOptions): String = 36 | super.gloss(o.showDefaults()) 37 | } 38 | 39 | sealed class ContextOutcome 40 | class Foreign(val word: String) : ContextOutcome() 41 | 42 | sealed class ParseOutcome : ContextOutcome() 43 | class Error(val message: String) : ParseOutcome() 44 | 45 | open class Parsed( 46 | private val slots: List, 47 | private val stressMarked: Glossable? = null, 48 | ) : ParseOutcome(), Glossable { 49 | 50 | constructor(vararg slots: Glossable?, stressMarked: Glossable? = null) : 51 | this(slots.filterNotNull(), stressMarked = stressMarked) 52 | 53 | override fun gloss(o: GlossOptions): String { 54 | val mainWord = slots 55 | .map { it.gloss(o) } 56 | .filter(String::isNotEmpty) 57 | .joinToString(SLOT_SEPARATOR) 58 | val stressCategory = stressMarked?.gloss(o) 59 | .let { if (!it.isNullOrEmpty()) "$STRESS_SLOT_SEPARATOR$it" else "" } 60 | 61 | return mainWord + stressCategory 62 | } 63 | 64 | override fun checkDictionary(r: Resources): Parsed { 65 | val newSlots = slots.map { it.checkDictionary(r) } 66 | 67 | return Parsed(newSlots, stressMarked = stressMarked) 68 | } 69 | 70 | 71 | fun addPrefix(prefix: Glossable): Parsed { 72 | val newSlots = buildList { 73 | add(prefix) 74 | addAll(slots) 75 | } 76 | 77 | return Parsed(newSlots, stressMarked = stressMarked) 78 | } 79 | } 80 | 81 | enum class Precision { 82 | REGULAR, 83 | SHORT, 84 | FULL, 85 | } 86 | 87 | class GlossOptions( 88 | private val precision: Precision = Precision.REGULAR, 89 | val includeDefaults: Boolean = false, 90 | ) { 91 | 92 | fun showDefaults(condition: Boolean = true) = 93 | GlossOptions(precision, includeDefaults || condition) 94 | 95 | override fun toString(): String = "${precision.name} form ${if (includeDefaults) "with defaults" else ""}" 96 | 97 | val concise: Boolean 98 | get() = (precision == Precision.SHORT) 99 | val verbose: Boolean 100 | get() = (precision == Precision.FULL) 101 | } 102 | 103 | 104 | class Slot(private val values: List) : Glossable, List by values { 105 | 106 | constructor(vararg values: Glossable?) : this(values.filterNotNull()) 107 | 108 | override fun gloss(o: GlossOptions): String { 109 | return values 110 | .map { it.gloss(o) } 111 | .filter(String::isNotEmpty) 112 | .joinToString(CATEGORY_SEPARATOR) 113 | } 114 | 115 | override fun toString(): String { 116 | val slotValues = values 117 | .joinToString { it.gloss(GlossOptions(Precision.FULL, includeDefaults = true)) } 118 | return "Slot($slotValues)" 119 | } 120 | 121 | override fun checkDictionary(r: Resources): Slot = Slot(values.map { it.checkDictionary(r) }) 122 | } 123 | 124 | class ConcatenationChain(private val formatives: List) : Parsed() { 125 | 126 | override fun gloss(o: GlossOptions): String { 127 | return formatives 128 | .map { it.gloss(o) } 129 | .filter(String::isNotEmpty) 130 | .joinToString(CONCATENATION_SEPARATOR) 131 | } 132 | 133 | override fun checkDictionary(r: Resources): ConcatenationChain { 134 | return ConcatenationChain(formatives.map { it.checkDictionary(r) }) 135 | } 136 | } 137 | 138 | class GlossString( 139 | private val full: String, 140 | private val normal: String = full, 141 | private val short: String = normal, 142 | private val ignorable: Boolean = false 143 | ) : Glossable { 144 | 145 | override fun gloss(o: GlossOptions): String { 146 | return when { 147 | ignorable && !o.includeDefaults -> "" 148 | o.concise -> short 149 | o.verbose -> full 150 | else -> normal 151 | } 152 | } 153 | } 154 | 155 | class Shown(private val value: Glossable, private val condition: Boolean = true) : Glossable { 156 | 157 | override fun gloss(o: GlossOptions): String = value.gloss(o.showDefaults(condition)) 158 | 159 | } 160 | 161 | class Underlineable(val value: T, var used: Boolean = false) : Glossable { 162 | 163 | override fun gloss(o: GlossOptions): String = value.gloss(o).let { if (used) "__${it}__" else it } 164 | 165 | } 166 | 167 | class ForcedDefault( 168 | private val value: Glossable, 169 | private val default: String, 170 | private val condition: Boolean = true 171 | ) : Glossable { 172 | 173 | override fun gloss(o: GlossOptions): String = 174 | value.gloss(o).let { if (it.isEmpty() && condition) default else it } 175 | 176 | } 177 | 178 | class Root(private val cr: String, private val stem: Underlineable) : Glossable { 179 | 180 | private var description: String = "**$cr**" 181 | 182 | override fun checkDictionary(r: Resources): Root { 183 | 184 | val rootEntry = r.getRoot(cr) 185 | 186 | if (rootEntry != null) { 187 | 188 | val stemDesc = rootEntry[stem.value] 189 | 190 | description = if (stemDesc.isNotEmpty()) { 191 | stem.used = true 192 | "“$stemDesc”" 193 | } else { 194 | "“${rootEntry[Stem.ZERO]}”" 195 | } 196 | } 197 | return this 198 | } 199 | 200 | override fun gloss(o: GlossOptions): String = description 201 | } -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/Affixes.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | enum class Degree(val numeral: Int) : Glossable { 4 | ONE(1), 5 | TWO(2), 6 | THREE(3), 7 | FOUR(4), 8 | FIVE(5), 9 | SIX(6), 10 | SEVEN(7), 11 | EIGHT(8), 12 | NINE(9), 13 | ZERO(0); 14 | 15 | override fun gloss(o: GlossOptions): String = 16 | if (o.verbose) "degree_${name.lowercase()}" else "D${numeral}" 17 | 18 | companion object { 19 | fun byForm(n: Int): Degree? = values().find { it.numeral == n } 20 | } 21 | } 22 | 23 | enum class AffixType(val subscript: String) { 24 | ONE("\u2081"), 25 | TWO("\u2082"), 26 | THREE("\u2083"); 27 | 28 | } 29 | 30 | enum class CaseAffixKind(override val short: String) : NoDefault { 31 | CASE_ACCESSOR("acc"), 32 | INVERSE_ACCESSOR("ia"), 33 | CASE_STACKING("case"); 34 | } 35 | 36 | sealed class AffixOutcome 37 | 38 | class AffixError(val message: String) : AffixOutcome() 39 | 40 | sealed class ValidAffix : AffixOutcome(), Glossable 41 | 42 | class CaStacker(private val ca: Slot) : ValidAffix() { 43 | override fun gloss(o: GlossOptions): String = "(${ForcedDefault(ca, "default_ca").gloss(o)})" 44 | } 45 | 46 | class CaseAffix(private val kind: CaseAffixKind, private val case: Case, private val type: AffixType?) : ValidAffix() { 47 | override fun gloss(o: GlossOptions): String = 48 | "(${kind.gloss(o)}:${case.gloss(o.showDefaults())})${type?.subscript ?: ""}" 49 | } 50 | 51 | class ReferentialShortcut(private val referents: Referential, private val case: Case) : ValidAffix() { 52 | override fun gloss(o: GlossOptions): String = "(${referents.gloss(o)}-${case.gloss(o.showDefaults())})" 53 | } 54 | 55 | class CsAffix(private val cs: String, private val degree: Degree, private val type: AffixType? = null) : ValidAffix() { 56 | 57 | private var description: String? = null 58 | private var abbreviation: String = "**$cs**" 59 | 60 | override fun checkDictionary(r: Resources): Glossable { 61 | 62 | val affixEntry = r.getAffix(cs) ?: return this 63 | 64 | abbreviation = affixEntry.abbreviation 65 | 66 | if (degree != Degree.ZERO) { 67 | description = affixEntry[degree] 68 | } 69 | 70 | return this 71 | 72 | } 73 | 74 | override fun gloss(o: GlossOptions): String { 75 | return if (o.concise || degree == Degree.ZERO || description == null) { 76 | "$abbreviation$AFFIX_DEGREE_SEPARATOR${degree.numeral}${type?.subscript ?: ""}" 77 | } else { 78 | "‘$description’${type?.subscript ?: ""}" 79 | } 80 | } 81 | } 82 | 83 | class IvlAffix private constructor(private val values: Slot) : ValidAffix() { 84 | 85 | override fun gloss(o: GlossOptions): String = "(${values.gloss(o)})" 86 | 87 | companion object { 88 | operator fun invoke(vx: String): IvlAffix? = 89 | parseIvlAffixVowel(vx)?.let { values -> IvlAffix(values) } 90 | } 91 | } 92 | 93 | 94 | class Affix(private val vx: String, private val cs: String) { 95 | fun parse(canBeReferentialShortcut: Boolean = false): AffixOutcome { 96 | if (vx == CA_STACKING_VOWEL) { 97 | val ca = parseCa(cs) ?: return AffixError("Unknown stacked Ca: $cs") 98 | return CaStacker(ca) 99 | } 100 | 101 | if (cs in CASE_AFFIXES) { 102 | val vc = when (cs) { 103 | "sw", "zw", "čw", "šw", "žw", "jw", "lw" -> vx 104 | "sy", "zy", "čy", "šy", "žy", "jy", "ly" -> glottalizeVowel(vx) 105 | else -> return AffixError("Case affix form not handled: $cs. Please report the error") 106 | } 107 | 108 | val case = Case.byVowel(vc) ?: return AffixError("Unknown case vowel: $vx") 109 | 110 | val kind = when (cs) { 111 | "sw", "sy", "zw", "zy", "čw", "čy" -> CaseAffixKind.CASE_ACCESSOR 112 | "šw", "šy", "žw", "žy", "jw", "jy" -> CaseAffixKind.INVERSE_ACCESSOR 113 | "lw", "ly" -> CaseAffixKind.CASE_STACKING 114 | else -> return AffixError("Case affix form not handled: $cs. Please report the error") 115 | } 116 | 117 | val type = when (cs) { 118 | "sw", "sy", "šw", "šy" -> AffixType.ONE 119 | "zw", "zy", "žw", "žy" -> AffixType.TWO 120 | "čw", "čy", "jw", "jy" -> AffixType.THREE 121 | "lw", "ly" -> null 122 | else -> return AffixError("Case affix form not handled: $cs. Please report the error") 123 | } 124 | 125 | return CaseAffix(kind, case, type) 126 | } 127 | 128 | if (cs == IVL_CS) { 129 | return IvlAffix(vx) ?: AffixError("Unknown IVL affix vowel: $vx") 130 | } 131 | 132 | val (series, form) = seriesAndForm(vx) 133 | 134 | if (canBeReferentialShortcut && series == 3 || series == 4) { 135 | val case = when (series) { 136 | 3 -> when (form) { 137 | 1 -> Case.POSSESSIVE 138 | 2 -> Case.PROPRIETIVE 139 | 3 -> Case.GENITIVE 140 | 4 -> Case.ATTRIBUTIVE 141 | 5 -> Case.PRODUCTIVE 142 | 6 -> Case.INTERPRETATIVE 143 | 7 -> Case.ORIGINATIVE 144 | 8 -> Case.INTERDEPENDENT 145 | 9 -> Case.PARTITIVE 146 | else -> return AffixError("Unknown vowel form ($form): $vx") 147 | } 148 | 149 | 4 -> when (form) { 150 | 1 -> Case.THEMATIC 151 | 2 -> Case.INSTRUMENTAL 152 | 3 -> Case.ABSOLUTIVE 153 | 4 -> Case.AFFECTIVE 154 | 5 -> Case.STIMULATIVE 155 | 6 -> Case.EFFECTUATIVE 156 | 7 -> Case.ERGATIVE 157 | 8 -> Case.DATIVE 158 | 9 -> Case.INDUCIVE 159 | else -> return AffixError("Unknown vowel form ($form): $vx") 160 | } 161 | 162 | else -> return AffixError("Unknown referential shortcut series ($series): $vx") 163 | } 164 | 165 | val referential = parseFullReferent(listOf(cs)) ?: return AffixError("Unknown referential: $cs") 166 | 167 | return ReferentialShortcut(referential, case) 168 | } 169 | 170 | val degree = if (vx in setOf("ae", "ea", "üo")) { 171 | Degree.ZERO 172 | } else Degree.byForm(form) ?: return AffixError("Unknown affix vowel form: $vx") 173 | 174 | val type: AffixType = if (degree == Degree.ZERO) when (vx) { 175 | "ae" -> AffixType.ONE 176 | "ea" -> AffixType.TWO 177 | "üo" -> AffixType.THREE 178 | else -> return AffixError("Unknown degree zero vowel: $vx") 179 | } else when (series) { 180 | 1 -> AffixType.ONE 181 | 2 -> AffixType.TWO 182 | 3 -> AffixType.THREE 183 | else -> return AffixError("Unknown vowel series ($series): $vx") 184 | } 185 | 186 | return CsAffix(cs, degree, type) 187 | 188 | } 189 | } 190 | 191 | inline fun AffixOutcome.validate(dealWithError: (AffixError) -> Nothing): ValidAffix = when (this) { 192 | is ValidAffix -> this 193 | is AffixError -> dealWithError(this) 194 | } 195 | 196 | fun List.parseAll(): List = 197 | map { it.parse(canBeReferentialShortcut = (size == 1)) } 198 | 199 | inline fun List.validateAll(dealWithError: (AffixError) -> Nothing): List = 200 | map { it.validate(dealWithError) } -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/dispatch/Dispatch.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss.dispatch 2 | 3 | import ithkuil.iv.gloss.* 4 | import kotlinx.datetime.Clock 5 | import mu.KotlinLogging 6 | import java.io.File 7 | import java.net.URL 8 | import kotlin.system.exitProcess 9 | 10 | val startTime = Clock.System.now() 11 | 12 | val logger = KotlinLogging.logger { } 13 | 14 | fun parseAffixes(data: String): Map = data 15 | .lineSequence() 16 | .drop(1) 17 | .map { it.split("\t") } 18 | .filter { it.size >= 11 } 19 | .associate { it[0] to AffixData(it[1], it.subList(2, 11)) } 20 | 21 | fun parseRoots(data: String): Map = data 22 | .lineSequence() 23 | .drop(1) 24 | .map { it.split("\t") } 25 | .filter { it.size >= 5 } 26 | .associate { it[0] to RootData(it.subList(1, 5)) } 27 | 28 | object LocalDictionary : Resources { 29 | 30 | var affixes: Map = emptyMap() 31 | var roots: Map = emptyMap() 32 | 33 | override fun getAffix(cs: String): AffixData? = affixes[cs] 34 | 35 | override fun getRoot(cr: String): RootData? = roots[cr] 36 | } 37 | 38 | const val MORPHOPHONOLOGY_VERSION = "1.0.0" 39 | 40 | const val AFFIXES_URL = 41 | "https://docs.google.com/spreadsheets/d/1JdaG1PaSQJRE2LpILvdzthbzz1k_a0VT86XSXouwGy8/export?format=tsv&gid=499365516" 42 | const val ROOTS_URL = 43 | "https://docs.google.com/spreadsheets/d/1JdaG1PaSQJRE2LpILvdzthbzz1k_a0VT86XSXouwGy8/export?format=tsv&gid=1534088303" 44 | 45 | const val AFFIXES_PATH = "./resources/affixes.tsv" 46 | const val ROOTS_PATH = "./resources/roots.tsv" 47 | 48 | fun loadResourcesOnline() = with(LocalDictionary) { 49 | logger.info { "-> loadResourcesOnline() (${affixes.size} affixes, ${roots.size} roots)" } 50 | val loadedAffixes = URL(AFFIXES_URL).readText() 51 | val loadedRoots = URL(ROOTS_URL).readText() 52 | 53 | File(AFFIXES_PATH).writeText(loadedAffixes) 54 | File(ROOTS_PATH).writeText(loadedRoots) 55 | 56 | affixes = parseAffixes(loadedAffixes) 57 | roots = parseRoots(loadedRoots) 58 | logger.info { " loadResourcesOnline() -> (${affixes.size} affixes, ${roots.size} roots)" } 59 | } 60 | 61 | fun loadResourcesLocal() = with(LocalDictionary) { 62 | logger.info { "-> loadResourcesLocal() (${affixes.size} affixes, ${roots.size} roots)" } 63 | val loadedAffixes = File(AFFIXES_PATH).readText() 64 | val loadedRoots = File(ROOTS_PATH).readText() 65 | 66 | affixes = parseAffixes(loadedAffixes) 67 | roots = parseRoots(loadedRoots) 68 | logger.info { " loadResourcesLocal() -> (${affixes.size} affixes, ${roots.size} roots)" } 69 | } 70 | 71 | fun requestPrecision(request: String) = when { 72 | "short" in request -> Precision.SHORT 73 | "full" in request -> Precision.FULL 74 | else -> Precision.REGULAR 75 | } 76 | 77 | private fun glossInline(content: String): String? = 78 | ":\\?(.*?)\\?:".toRegex(RegexOption.DOT_MATCHES_ALL) 79 | .findAll(content) 80 | .map { match -> respond("?s ${match.groupValues[1].trimWhitespace()}") } 81 | .joinToString("\n\n") 82 | .takeIf { it.isNotBlank() } 83 | 84 | fun respond(content: String): String? { 85 | if (!content.startsWith("?")) { 86 | return glossInline(content) 87 | } 88 | 89 | val (fullRequest, arguments) = content.splitOnWhitespace().let { it[0] to it.drop(1) } 90 | val request = fullRequest.removePrefix("??").removePrefix("?") 91 | val o = GlossOptions(requestPrecision(request), fullRequest.startsWith("??")) 92 | logger.info { " respond($content) received options: $o" } 93 | logger.info { 94 | " respond($content) received arguments: ${ 95 | arguments.mapIndexed { index, it -> "$index: $it" } 96 | }" 97 | } 98 | 99 | return when (request) { 100 | "gloss", "short", "full" -> wordByWord(arguments, o) 101 | 102 | "s", "sgloss", "sshort", "sfull" -> sentenceGloss(arguments, o) 103 | 104 | "root" -> lookupRoot(arguments) 105 | 106 | "affix" -> lookupAffix(arguments) 107 | 108 | "!stop" -> exitProcess(0) 109 | 110 | "!reload" -> try { 111 | loadResourcesOnline() 112 | "External resources successfully reloaded!" 113 | } catch (e: Exception) { 114 | logger.error { e.toString() } 115 | "Error while reloading external resources. Please contact the maintainers" 116 | } 117 | 118 | "status" -> """ 119 | __Status report:__ 120 | **Ithkuil Version:** $MORPHOPHONOLOGY_VERSION 121 | **Roots:** ${LocalDictionary.roots.size} 122 | **Affixes:** ${LocalDictionary.affixes.size} 123 | **Help file exists:** ${File("./resources/help.md").exists()} 124 | **Uptime:** ${Clock.System.now() - startTime} 125 | **Last commit:** $lastCommit 126 | """.trimIndent() 127 | 128 | "ej" -> externalJuncture(arguments.formatAll()) 129 | 130 | "whosagoodbot", "whosacutebot" -> "(=^ェ^=✿)" 131 | 132 | "date" -> datetimeInIthkuil() 133 | 134 | else -> null 135 | } 136 | } 137 | 138 | val lastCommit: String by lazy { 139 | ProcessBuilder("git", "log", "-1", "--oneline") 140 | .start() 141 | .inputStream 142 | .bufferedReader() 143 | .readText() 144 | } 145 | 146 | fun lookupRoot(crs: List): String { 147 | val lookups = crs.map { it.removeSuffix(",").trim('-').defaultForm() } 148 | 149 | val entries = mutableListOf() 150 | 151 | for (cr in lookups) { 152 | val root = LocalDictionary.roots[cr] 153 | if (root != null) { 154 | val generalDescription = root[Stem.ZERO] 155 | val stemDescriptions = root.descriptions.drop(1) 156 | val titleLine = "**-${cr.uppercase()}-**: $generalDescription" 157 | 158 | val descLines = stemDescriptions.mapIndexedNotNull { index, description -> 159 | if (description.isNotEmpty()) "${index + 1}. $description" 160 | else null 161 | }.joinToString("\n") 162 | 163 | entries.add( 164 | "$titleLine\n$descLines" 165 | ) 166 | 167 | } else { 168 | entries.add("*-${cr.uppercase()}- not found*") 169 | } 170 | 171 | } 172 | 173 | return entries.joinToString("\n\n") 174 | 175 | } 176 | 177 | fun lookupAffix(cxs: List): String { 178 | val lookups = cxs.map { it.removeSuffix(",").trim('-').defaultForm() } 179 | 180 | val entries = mutableListOf() 181 | 182 | for (cx in lookups) { 183 | val affix = LocalDictionary.affixes[cx] 184 | if (affix != null) { 185 | val abbreviation = affix.abbreviation 186 | val degreeDescriptions = affix.descriptions 187 | val titleLine = "**-$cx**: $abbreviation" 188 | 189 | val descLines = degreeDescriptions.mapIndexedNotNull { index, description -> 190 | if (description.isNotEmpty()) "${index + 1}. $description" 191 | else null 192 | }.joinToString("\n") 193 | 194 | entries.add( 195 | "$titleLine\n$descLines" 196 | ) 197 | 198 | } else { 199 | entries.add("*-$cx not found*") 200 | } 201 | 202 | } 203 | 204 | return entries.joinToString("\n\n") 205 | } 206 | 207 | 208 | fun sentenceGloss(words: List, o: GlossOptions): String { 209 | val glosses = glossInContext(words.formatAll()) 210 | .map { (word, parsed) -> 211 | when (parsed) { 212 | is Foreign -> "*$word*" 213 | is Error -> "**$word**" 214 | is Parsed -> { 215 | parsed.checkDictionary(LocalDictionary) 216 | parsed.gloss(o).withZeroWidthSpaces() 217 | } 218 | } 219 | } 220 | 221 | return glosses.joinToString(" ") 222 | } 223 | 224 | fun wordByWord(words: List, o: GlossOptions): String { 225 | val glossPairs = glossInContext(words.formatAll()) 226 | .map { (word, parsed) -> 227 | when (parsed) { 228 | is Foreign -> "**$word**" 229 | is Error -> "**$word:** *${parsed.message}*" 230 | is Parsed -> { 231 | parsed.checkDictionary(LocalDictionary) 232 | "**$word:** ${parsed.gloss(o)}" 233 | } 234 | } 235 | } 236 | 237 | return glossPairs.joinToString("\n") 238 | 239 | } 240 | 241 | fun String.withZeroWidthSpaces() = replace("[/—-]".toRegex(), "\u200b$0") 242 | 243 | fun String.splitOnWhitespace() = split(Regex("\\p{javaWhitespace}")).filter { it.isNotEmpty() } 244 | 245 | fun String.trimWhitespace() = splitOnWhitespace().joinToString(" ") -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/Formatting.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | sealed class FormattingOutcome 4 | 5 | class Invalid(private val word: String, val message: String) : FormattingOutcome() { 6 | override fun toString(): String = word 7 | } 8 | 9 | sealed class Valid : FormattingOutcome() { 10 | abstract val prefixPunctuation: String 11 | abstract val postfixPunctuation: String 12 | abstract val hasSentencePrefix: Boolean 13 | } 14 | 15 | class ConcatenatedWords( 16 | val words: List, 17 | override val prefixPunctuation: String = "", 18 | override val postfixPunctuation: String = "", 19 | ) : Valid() { 20 | 21 | override fun toString(): String = words 22 | .joinToString( 23 | separator = "-", 24 | prefix = prefixPunctuation, 25 | postfix = postfixPunctuation 26 | ) { it.toString() } 27 | 28 | override val hasSentencePrefix: Boolean 29 | get() = words.first().hasSentencePrefix 30 | } 31 | 32 | class Word( 33 | private val word: String, 34 | private val groups: List, 35 | val stress: Stress, 36 | override val prefixPunctuation: String, 37 | override val postfixPunctuation: String, 38 | override val hasSentencePrefix: Boolean, 39 | ) : List by groups, Valid() { 40 | 41 | override fun toString(): String = "$prefixPunctuation$word$postfixPunctuation" 42 | 43 | val wordType by lazy { wordTypeOf(this) } 44 | } 45 | 46 | fun formatWord(fullWord: String): FormattingOutcome { 47 | 48 | if (fullWord.isEmpty()) return Invalid(fullWord, "Empty word") 49 | 50 | val punct = ".,?!:;⫶`\"*_" 51 | val punctuationRegex = "([$punct]*)([^$punct]+)([$punct]*)".toRegex() 52 | 53 | val (prefix, word, postfix) = punctuationRegex.matchEntire(fullWord)?.destructured 54 | ?: return Invalid(fullWord, "Unexpected punctuation") 55 | 56 | val escapedPrefix = prefix.escapeMarkdown() 57 | val escapedPostfix = postfix.escapeMarkdown() 58 | 59 | if ("-" in word) { 60 | return formatConcatenatedWords(word, escapedPrefix, escapedPostfix) 61 | } 62 | 63 | val clean = word.defaultForm() 64 | 65 | if (clean.last() == '\'') return Invalid(clean, "Word ends in glottal stop") 66 | 67 | fun codepointString(c: Char): String { 68 | val codepoint = c.code 69 | .toString(16) 70 | .uppercase() 71 | .padStart(4, '0') 72 | return "\"$c\" (U+$codepoint)" 73 | } 74 | 75 | val nonIthkuil = clean.filter { it.toString() !in ITHKUIL_CHARS } 76 | if (nonIthkuil.isNotEmpty()) { 77 | var message = nonIthkuil.map { codepointString(it) }.joinToString() 78 | 79 | if ("[qˇ^ʰ]".toRegex() in nonIthkuil) { 80 | message += " You might be writing in Ithkuil III. Try \"!gloss\" instead." 81 | } 82 | return Invalid(clean, "Non-ithkuil characters detected: $message") 83 | } 84 | 85 | val stressedGroups = clean.splitGroups() 86 | 87 | for (group in stressedGroups) { 88 | if (group.isConsonant() xor group.isVowel()) continue 89 | return Invalid(clean, "Unknown group: $group") 90 | } 91 | 92 | val stress = findStress(stressedGroups) 93 | 94 | if (stressedGroups.take(2) == listOf("ç", "ê")) return Invalid(clean, "Stress on sentence prefix") 95 | 96 | when (stress) { 97 | Stress.INVALID_PLACE -> return Invalid(clean, "Unrecognized stress placement") 98 | Stress.MARKED_DEFAULT -> return Invalid(clean, "Marked default stress") 99 | Stress.DOUBLE_MARKED -> return Invalid(clean, "Double-marked stress") 100 | else -> { 101 | } 102 | } 103 | 104 | val hasSentencePrefix = stressedGroups[0] in setOf("ç", "çw", "çç") 105 | 106 | val strippedGroups = when (stressedGroups[0]) { 107 | "ç" -> if (stressedGroups.getOrNull(1) == "ë") stressedGroups.drop(2) else stressedGroups.drop(1) 108 | "çw" -> listOf("w") + stressedGroups.drop(1) 109 | "çç" -> listOf("y") + stressedGroups.drop(1) 110 | else -> stressedGroups 111 | } 112 | 113 | if (strippedGroups.isEmpty() || strippedGroups in setOf(listOf("w"), listOf("y"))) { 114 | return Invalid(clean, "Lone sentence prefix") 115 | } 116 | 117 | val groups = strippedGroups.map { it.clearStress() } 118 | 119 | return Word(clean, groups, stress, escapedPrefix, escapedPostfix, hasSentencePrefix) 120 | } 121 | 122 | private fun String.escapeMarkdown(): String = replace("[\\\\`*_{}\\[\\]<>()#+\\-.!|]".toRegex(), "\\\\$0") 123 | 124 | private fun formatConcatenatedWords( 125 | word: String, 126 | prefix: String, 127 | postfix: String 128 | ): FormattingOutcome { 129 | val words = word 130 | .split("-") 131 | .formatAll() 132 | .map { 133 | when (it) { 134 | is Word -> { 135 | if (it.wordType != WordType.FORMATIVE) { 136 | return Invalid(word, "Non-formative concatenated: ($it)") 137 | } 138 | it 139 | } 140 | 141 | is Invalid -> return Invalid(word, "${it.message} ($it)") 142 | is ConcatenatedWords -> return Invalid(word, "Nested concatenation! ($it)") 143 | } 144 | } 145 | return ConcatenatedWords(words, prefixPunctuation = prefix, postfixPunctuation = postfix) 146 | } 147 | 148 | fun List.formatAll(): List = map { formatWord(it) } 149 | 150 | enum class GroupingState { 151 | VOWEL, 152 | CONSONANT; 153 | 154 | fun switch(): GroupingState = when (this) { 155 | CONSONANT -> VOWEL 156 | VOWEL -> CONSONANT 157 | } 158 | 159 | companion object { 160 | fun start(first: Char): GroupingState = if (first in VOWELS) VOWEL else CONSONANT 161 | } 162 | 163 | } 164 | 165 | fun String.splitGroups(): List { 166 | 167 | var index = 0 168 | var state = GroupingState.start(first()) 169 | 170 | val groups = mutableListOf() 171 | 172 | while (index <= lastIndex) { 173 | val group = when (state) { 174 | GroupingState.CONSONANT -> substring(index) 175 | .takeWhile { it in CONSONANTS } 176 | 177 | GroupingState.VOWEL -> substring(index) 178 | .takeWhile { it in VOWELS_AND_GLOTTAL_STOP } 179 | } 180 | 181 | state = state.switch() 182 | index += group.length 183 | groups += group 184 | } 185 | 186 | return groups 187 | } 188 | 189 | // Matches strings of the form "a", "ai", "a'" "a'i" and "ai'". Doesn't guarantee a valid vowelform. 190 | fun String.isVowel() = when (length) { 191 | 1 -> this[0] in VOWELS 192 | 2 -> this[0] in VOWELS && this[1] in VOWELS_AND_GLOTTAL_STOP 193 | 3 -> all { it in VOWELS_AND_GLOTTAL_STOP } && this[0] != '\'' && this.count { it == '\'' } == 1 194 | else -> false 195 | } 196 | 197 | fun String.isConsonant() = this.all { it in CONSONANTS } 198 | 199 | val STRESSED_VOWELS = setOf('á', 'â', 'é', 'ê', 'í', 'î', 'ô', 'ó', 'û', 'ú') 200 | 201 | fun String.hasStress(): Boolean? = when { 202 | this.getOrNull(1) in STRESSED_VOWELS -> null 203 | this[0] in STRESSED_VOWELS -> true 204 | else -> false 205 | } 206 | 207 | fun seriesAndForm(v: String): Pair { 208 | return when (val index = VOWEL_FORMS.indexOfFirst { v isSameVowelAs it }) { 209 | -1 -> Pair(-1, -1) 210 | else -> Pair((index / 9) + 1, (index % 9) + 1) 211 | } 212 | } 213 | 214 | fun unglottalizeVowel(v: String): String { 215 | return v.filter { it != '\'' } 216 | .let { 217 | if (it.length == 2 && it[0] == it[1]) it.take(1) else it 218 | } 219 | } 220 | 221 | fun glottalizeVowel(v: String): String { 222 | return when (v.length) { 223 | 1 -> "$v'$v" 224 | 2 -> "${v[0]}'${v[1]}" 225 | else -> v 226 | } 227 | } 228 | 229 | //Deals with series three vowels 230 | infix fun String.isSameVowelAs(s: String): Boolean = if ("/" in s) { 231 | s.split("/").any { it == this } 232 | } else { 233 | s == this 234 | } 235 | 236 | fun String.substituteAll(substitutions: List>) = 237 | substitutions.fold(this) { current, (allo, sub) -> 238 | current.replace(allo.toRegex(), sub) 239 | } 240 | 241 | fun String.clearStress() = substituteAll(UNSTRESSED_FORMS) 242 | 243 | fun String.defaultForm() = lowercase().substituteAll(ALLOGRAPHS) 244 | 245 | enum class Stress { 246 | ULTIMATE, 247 | PENULTIMATE, 248 | ANTEPENULTIMATE, 249 | MONOSYLLABIC, 250 | 251 | MARKED_DEFAULT, 252 | DOUBLE_MARKED, 253 | INVALID_PLACE; 254 | } 255 | 256 | fun findStress(groups: List): Stress { 257 | val nuclei = groups.filter(String::isVowel) 258 | .map { it.removeSuffix("'") } 259 | .flatMap { 260 | if (it.length == 1 || it.clearStress() in DIPHTHONGS) { 261 | listOf(it) 262 | } else { 263 | it.toCharArray() 264 | .map(Char::toString) 265 | .filter { ch -> ch != "'" } 266 | } 267 | } 268 | 269 | val stresses = nuclei 270 | .reversed() 271 | .map { it.hasStress() ?: return Stress.INVALID_PLACE } 272 | 273 | val count = stresses.count { it } 274 | 275 | if (count > 1) return Stress.DOUBLE_MARKED 276 | if (nuclei.size == 1) { 277 | return if (count == 0) Stress.MONOSYLLABIC else Stress.MARKED_DEFAULT 278 | } 279 | 280 | return when (stresses.indexOfFirst { it }) { 281 | -1 -> Stress.PENULTIMATE 282 | 0 -> Stress.ULTIMATE 283 | 1 -> Stress.MARKED_DEFAULT 284 | 2 -> Stress.ANTEPENULTIMATE 285 | else -> Stress.INVALID_PLACE 286 | } 287 | } 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/Categories.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | enum class WordType { 4 | FORMATIVE, 5 | MODULAR_ADJUNCT, 6 | AFFIXUAL_ADJUNCT, 7 | MULTIPLE_AFFIX_ADJUNCT, 8 | REFERENTIAL, 9 | COMBINATION_REFERENTIAL, 10 | REGISTER_ADJUNCT, 11 | BIAS_ADJUNCT, 12 | MOOD_CASESCOPE_ADJUNCT; 13 | } 14 | 15 | enum class RootMode { 16 | ROOT, 17 | AFFIX, 18 | REFERENCE; 19 | } 20 | 21 | enum class Shortcut { 22 | Y, 23 | W; 24 | } 25 | 26 | enum class Concatenation(override val short: String) : NoDefault { 27 | TYPE_ONE("T1"), 28 | TYPE_TWO("T2"); 29 | } 30 | 31 | enum class Version(override val short: String) : Category { 32 | PROCESSUAL("PRC"), 33 | COMPLETIVE("CPT"); 34 | } 35 | 36 | enum class Relation(override val short: String) : Category { 37 | UNFRAMED("UNF"), 38 | FRAMED("FRA"); 39 | } 40 | 41 | enum class Stem(override val short: String) : NoDefault { 42 | ZERO("S0"), 43 | ONE("S1"), 44 | TWO("S2"), 45 | THREE("S3"); 46 | 47 | override fun gloss(o: GlossOptions) = when { 48 | o.verbose -> "stem_${name.lowercase()}" 49 | else -> short 50 | } 51 | } 52 | 53 | enum class Specification(override val short: String) : Category { 54 | BASIC("BSC"), 55 | CONTENTIAL("CTE"), 56 | CONSTITUTIVE("CSV"), 57 | OBJECTIVE("OBJ"); 58 | } 59 | 60 | enum class Function(override val short: String) : Category { 61 | STATIVE("STA"), 62 | DYNAMIC("DYN"); 63 | } 64 | 65 | interface CaCategory : Category // Should ideally be sealed, but that feature isn't yet implemented 66 | 67 | @Suppress("unused") 68 | enum class Configuration(override val short: String) : CaCategory { 69 | UNIPLEX("UPX"), 70 | DUPLEX("DPX"), 71 | DUPLEX_SIMILAR_SEPARATE("DSS"), 72 | DUPLEX_SIMILAR_CONNECTED("DSC"), 73 | DUPLEX_SIMILAR_FUSED("DSF"), 74 | DUPLEX_DISSIMILAR_SEPARATE("DDS"), 75 | DUPLEX_DISSIMILAR_CONNECTED("DDC"), 76 | DUPLEX_DISSIMILAR_FUSED("DDF"), 77 | DUPLEX_FUZZY_SEPARATE("DFS"), 78 | DUPLEX_FUZZY_CONNECTED("DFC"), 79 | DUPLEX_FUZZY_FUSED("DFF"), 80 | MULTIPLEX_SIMILAR_SEPARATE("MSS"), 81 | MULTIPLEX_SIMILAR_CONNECTED("MSC"), 82 | MULTIPLEX_SIMILAR_FUSED("MSF"), 83 | MULTIPLEX_DISSIMILAR_SEPARATE("MDS"), 84 | MULTIPLEX_DISSIMILAR_CONNECTED("MDC"), 85 | MULTIPLEX_DISSIMILAR_FUSED("MDF"), 86 | MULTIPLEX_FUZZY_SEPARATE("MFS"), 87 | MULTIPLEX_FUZZY_CONNECTED("MFC"), 88 | MULTIPLEX_FUZZY_FUSED("MFF"); 89 | 90 | companion object { 91 | fun byAbbreviation(s: String): Configuration? { 92 | return values().find { it.short == s } 93 | } 94 | } 95 | 96 | } 97 | 98 | enum class Affiliation(override val short: String) : CaCategory { 99 | CONSOLIDATIVE("CSL"), 100 | ASSOCIATIVE("ASO"), 101 | VARIATIVE("VAR"), 102 | COALESCENT("COA"); 103 | } 104 | 105 | enum class Extension(override val short: String) : CaCategory { 106 | DELIMITIVE("DEL"), 107 | PROXIMAL("PRX"), 108 | INCEPTIVE("ICP"), 109 | ATTENUATIVE("ATV"), 110 | GRADUATIVE("GRA"), 111 | DEPLETIVE("DPL"); 112 | } 113 | 114 | enum class Perspective(override val short: String) : CaCategory { 115 | MONADIC("M"), 116 | AGGLOMERATIVE("G"), 117 | NOMIC("N"), 118 | ABSTRACT("A"); 119 | } 120 | 121 | enum class Essence(override val short: String) : CaCategory { 122 | NORMAL("NRM"), 123 | REPRESENTATIVE("RPV"); 124 | } 125 | 126 | enum class Context(override val short: String) : Category { 127 | EXISTENTIAL("EXS"), 128 | FUNCTIONAL("FNC"), 129 | REPRESENTATIONAL("RPS"), 130 | AMALGAMATIVE("AMG"); 131 | } 132 | 133 | enum class Valence(override val short: String) : Category { 134 | MONOACTIVE("MNO"), 135 | PARALLEL("PRL"), 136 | COROLLARY("CRO"), 137 | RECIPROCAL("RCP"), 138 | COMPLEMENTARY("CPL"), 139 | DUPLICATIVE("DUP"), 140 | DEMONSTRATIVE("DEM"), 141 | CONTINGENT("CNG"), 142 | PARTICIPATIVE("PTI"); 143 | 144 | companion object { 145 | fun byForm(form: Int) = values()[form - 1] 146 | } 147 | } 148 | 149 | enum class Phase(override val short: String) : NoDefault { 150 | PUNCTUAL("PCT"), 151 | ITERATIVE("ITR"), 152 | REPETITIVE("REP"), 153 | INTERMITTENT("ITM"), 154 | RECURRENT("RCT"), 155 | FREQUENTATIVE("FRE"), 156 | FRAGMENTATIVE("FRG"), 157 | VACILLATIVE("VAC"), 158 | FLUCTUATIVE("FLC"); 159 | 160 | companion object { 161 | fun byForm(form: Int) = values()[form - 1] 162 | } 163 | 164 | } 165 | 166 | 167 | class EffectAndPerson(private val person: String?, private val effect: Effect) : Glossable { 168 | 169 | override fun gloss(o: GlossOptions): String { 170 | return if (person != null) { 171 | "$person:${effect.gloss(o.showDefaults())}" 172 | } else effect.gloss(o.showDefaults()) 173 | } 174 | 175 | companion object { 176 | fun byForm(form: Int) = when (form) { 177 | 1 -> EffectAndPerson("1", Effect.BENEFICIAL) 178 | 2 -> EffectAndPerson("2", Effect.BENEFICIAL) 179 | 3 -> EffectAndPerson("3", Effect.BENEFICIAL) 180 | 4 -> EffectAndPerson("SLF", Effect.BENEFICIAL) 181 | 5 -> EffectAndPerson(null, Effect.UNKNOWN) 182 | 6 -> EffectAndPerson("SLF", Effect.DETRIMENTAL) 183 | 7 -> EffectAndPerson("3", Effect.DETRIMENTAL) 184 | 8 -> EffectAndPerson("2", Effect.DETRIMENTAL) 185 | 9 -> EffectAndPerson("1", Effect.DETRIMENTAL) 186 | else -> throw IndexOutOfBoundsException("Invalid vowelform: $form") //Unreachable 187 | } 188 | } 189 | 190 | } 191 | 192 | enum class Effect(override val short: String) : Category { 193 | NEUTRAL("NEU"), 194 | BENEFICIAL("BEN"), 195 | UNKNOWN("UNK"), 196 | DETRIMENTAL("DET"); 197 | } 198 | 199 | enum class Level(override val short: String) : NoDefault { 200 | MINIMAL("MIN"), 201 | SUBEQUATIVE("SBE"), 202 | INFERIOR("IFR"), 203 | DEFICIENT("DFT"), 204 | EQUATIVE("EQU"), 205 | SURPASSIVE("SUR"), 206 | SUPERLATIVE("SPL"), 207 | SUPEREQUATIVE("SPQ"), 208 | MAXIMAL("MAX"); 209 | 210 | companion object { 211 | fun byForm(form: Int) = values()[form - 1] 212 | } 213 | } 214 | 215 | enum class LevelRelativity(override val short: String) : Category { 216 | RELATIVE("r"), 217 | ABSOLUTE("a"); 218 | } 219 | 220 | class LevelAndRelativity( 221 | private val level: Level, 222 | private val relativity: LevelRelativity 223 | ) : Glossable { 224 | 225 | constructor(form: Int, absoluteLevel: Boolean) : this( 226 | Level.byForm(form), 227 | if (absoluteLevel) LevelRelativity.ABSOLUTE else LevelRelativity.RELATIVE 228 | ) 229 | 230 | override fun gloss(o: GlossOptions): String { 231 | return level.gloss(o) + 232 | (if (!o.verbose) "" else CATEGORY_SEPARATOR) + 233 | relativity.gloss(o) 234 | } 235 | } 236 | 237 | enum class Aspect(override val short: String, val series: Int, val form: Int) : NoDefault { 238 | RETROSPECTIVE("RTR", 1, 1), 239 | PROSPECTIVE("PRS", 1, 2), 240 | HABITUAL("HAB", 1, 3), 241 | PROGRESSIVE("PRG", 1, 4), 242 | IMMINENT("IMM", 1, 5), 243 | PRECESSIVE("PCS", 1, 6), 244 | REGULATIVE("REG", 1, 7), 245 | SUMMATIVE("SMM", 1, 8), 246 | ANTICIPATORY("ATP", 1, 9), 247 | 248 | RESUMPTIVE("RSM", 2, 1), 249 | CESSATIVE("CSS", 2, 2), 250 | PAUSAL("PAU", 2, 3), 251 | REGRESSIVE("RGR", 2, 4), 252 | PRECLUSIVE("PCL", 2, 5), 253 | CONTINUATIVE("CNT", 2, 6), 254 | INCESSATIVE("ICS", 2, 7), 255 | EXPERIENTIAL("EXP", 2, 8), 256 | INTERRUPTIVE("IRP", 2, 9), 257 | 258 | PREEMPTIVE("PMP", 3, 1), 259 | CLIMACTIC("CLM", 3, 2), 260 | DILATORY("DLT", 3, 3), 261 | TEMPORARY("TMP", 3, 4), 262 | EXPENDITIVE("XPD", 3, 5), 263 | LIMITATIVE("LIM", 3, 6), 264 | EXPEDITIVE("EPD", 3, 7), 265 | PROTRACTIVE("PTC", 3, 8), 266 | PREPARATORY("PPR", 3, 9), 267 | 268 | DISCLUSIVE("DCL", 4, 1), 269 | CONCLUSIVE("CCL", 4, 2), 270 | CULMINATIVE("CUL", 4, 3), 271 | INTERMEDIATIVE("IMD", 4, 4), 272 | TARDATIVE("TRD", 4, 5), 273 | TRANSITIONAL("TNS", 4, 6), 274 | INTERCOMMUTATIVE("ITC", 4, 7), 275 | MOTIVE("MTV", 4, 8), 276 | SEQUENTIAL("SQN", 4, 9); 277 | 278 | companion object { 279 | fun byVowel(vn: String) = values().find { 280 | val (series, form) = seriesAndForm(vn) 281 | it.series == series && it.form == form 282 | } 283 | } 284 | } 285 | 286 | enum class Mood(override val short: String) : Category { 287 | FACTUAL("FAC"), 288 | SUBJUNCTIVE("SUB"), 289 | ASSUMPTIVE("ASM"), 290 | SPECULATIVE("SPC"), 291 | COUNTERFACTIVE("COU"), 292 | HYPOTHETICAL("HYP"); 293 | } 294 | 295 | enum class CaseScope(override val short: String) : Category { 296 | NATURAL("CCN"), 297 | ANTECEDENT("CCA"), 298 | SUBALTERN("CCS"), 299 | QUALIFIER("CCQ"), 300 | PRECEDENT("CCP"), 301 | SUCCESSIVE("CCV"); 302 | } 303 | 304 | enum class Case(override val short: String, val series: Int, val form: Int, val glottal: Boolean = false) : Category { 305 | // Transrelative 306 | THEMATIC("THM", 1, 1), 307 | INSTRUMENTAL("INS", 1, 2), 308 | ABSOLUTIVE("ABS", 1, 3), 309 | AFFECTIVE("AFF", 1, 4), 310 | STIMULATIVE("STM", 1, 5), 311 | EFFECTUATIVE("EFF", 1, 6), 312 | ERGATIVE("ERG", 1, 7), 313 | DATIVE("DAT", 1, 8), 314 | INDUCIVE("IND", 1, 9), 315 | 316 | // Appositive 317 | POSSESSIVE("POS", 2, 1), 318 | PROPRIETIVE("PRP", 2, 2), 319 | GENITIVE("GEN", 2, 3), 320 | ATTRIBUTIVE("ATT", 2, 4), 321 | PRODUCTIVE("PDC", 2, 5), 322 | INTERPRETATIVE("ITP", 2, 6), 323 | ORIGINATIVE("OGN", 2, 7), 324 | INTERDEPENDENT("IDP", 2, 8), 325 | PARTITIVE("PAR", 2, 9), 326 | 327 | // Associative 328 | APPLICATIVE("APL", 3, 1), 329 | PURPOSIVE("PUR", 3, 2), 330 | TRANSMISSIVE("TRA", 3, 3), 331 | DEFERENTIAL("DFR", 3, 4), 332 | CONTRASTIVE("CRS", 3, 5), 333 | TRANSPOSITIVE("TSP", 3, 6), 334 | COMMUTATIVE("CMM", 3, 7), 335 | COMPARATIVE("CMP", 3, 8), 336 | CONSIDERATIVE("CSD", 3, 9), 337 | 338 | // Adverbial 339 | FUNCTIVE("FUN", 4, 1), 340 | TRANSFORMATIVE("TFM", 4, 2), 341 | CLASSIFICATIVE("CLA", 4, 3), 342 | RESULTATIVE("RSL", 4, 4), 343 | CONSUMPTIVE("CSM", 4, 5), 344 | CONCESSIVE("CON", 4, 6), 345 | AVERSIVE("AVS", 4, 7), 346 | CONVERSIVE("CVS", 4, 8), 347 | SITUATIVE("SIT", 4, 9), 348 | 349 | // Relational 350 | PERTINENTIAL("PRN", 1, 1, true), 351 | DESCRIPTIVE("DSP", 1, 2, true), 352 | CORRELATIVE("COR", 1, 3, true), 353 | COMPOSITIVE("CPS", 1, 4, true), 354 | COMITATIVE("COM", 1, 5, true), 355 | UTILITATIVE("UTL", 1, 6, true), 356 | PREDICATIVE("PRD", 1, 7, true), 357 | RELATIVE("RLT", 1, 9, true), 358 | 359 | // Affinitive 360 | ACTIVATIVE("ACT", 2, 1, true), 361 | ASSIMILATIVE("ASI", 2, 2, true), 362 | ESSIVE("ESS", 2, 3, true), 363 | TERMINATIVE("TRM", 2, 4, true), 364 | SELECTIVE("SEL", 2, 5, true), 365 | CONFORMATIVE("CFM", 2, 6, true), 366 | DEPENDENT("DEP", 2, 7, true), 367 | VOCATIVE("VOC", 2, 9, true), 368 | 369 | // Spatio-Temporal I 370 | LOCATIVE("LOC", 3, 1, true), 371 | ATTENDANT("ATD", 3, 2, true), 372 | ALLATIVE("ALL", 3, 3, true), 373 | ABLATIVE("ABL", 3, 4, true), 374 | ORIENTATIVE("ORI", 3, 5, true), 375 | INTERRELATIVE("IRL", 3, 6, true), 376 | INTRATIVE("INV", 3, 7, true), 377 | NAVIGATIVE("NAV", 3, 9, true), 378 | 379 | // Spatio-Temporal II 380 | CONCURSIVE("CNR", 4, 1, true), 381 | ASSESSIVE("ASS", 4, 2, true), 382 | PERIODIC("PER", 4, 3, true), 383 | PROLAPSIVE("PRO", 4, 4, true), 384 | PRECURSIVE("PCV", 4, 5, true), 385 | POSTCURSIVE("PCR", 4, 6, true), 386 | ELAPSIVE("ELP", 4, 7, true), 387 | PROLIMITIVE("PLM", 4, 9, true); 388 | 389 | companion object { 390 | fun byVowel(vc: String): Case? { 391 | val glottal = '\'' in vc 392 | val (series, form) = seriesAndForm(unglottalizeVowel(vc)) 393 | return values().find { 394 | it.form == form 395 | && it.series == series 396 | && it.glottal == glottal 397 | } 398 | } 399 | } 400 | } 401 | 402 | enum class Illocution(override val short: String) : Category { 403 | ASSERTIVE("ASR"), 404 | DIRECTIVE("DIR"), 405 | DECLARATIVE("DEC"), 406 | INTERROGATIVE("IRG"), 407 | VERIFICATIVE("VRF"), 408 | ADMONITIVE("ADM"), 409 | POTENTIATIVE("POT"), 410 | HORTATIVE("HOR"), 411 | CONJECTURAL("CNJ"); 412 | } 413 | 414 | enum class Validation(override val short: String) : NoDefault { 415 | OBSERVATIONAL("OBS"), 416 | RECOLLECTIVE("REC"), 417 | PURPORTIVE("PUP"), 418 | REPORTIVE("RPR"), 419 | UNSPECIFIED("USP"), 420 | IMAGINARY("IMA"), 421 | CONVENTIONAL("CVN"), 422 | INTUITIVE("ITU"), 423 | INFERENTIAL("INF"); 424 | } 425 | 426 | enum class Bias(override val short: String, val cb: String, private val representative: String) : NoDefault { 427 | ACCIDENTAL("ACC", "lf", "As luck would would have it..."), 428 | ADMISSIVE("ADM", "lļ", "Mm-hm"), 429 | ANNUNCIATIVE("ANN", "drr", "Wait till you hear this!"), 430 | ANTICIPATIVE("ANP", "lst", ""), 431 | APPREHENSIVE("APH", "vvz", "I'm worried..."), 432 | APPROBATIVE("APB", "řs", "OK"), 433 | ARBITRARY("ARB", "xtļ", "Yeah, whatever..."), 434 | ARCHETYPAL("ACH", "mçt", "Such a...!"), 435 | ATTENTIVE("ATE", "ňj", "Who would have thought?"), 436 | COINCIDENTAL("COI", "ššč", "What a coincidence!"), 437 | COMEDIC("CMD", "pļļ", "Funny!"), 438 | CONTEMPLATIVE("CTV", "gvv", "Hmmmm..."), 439 | CONTEMPTIVE("CTP", "kšš", "What nonsense!"), 440 | CONTENSIVE("CNV", "rrj", "I told you so!"), 441 | CORRECTIVE("CRR", "ňţ", "What I meant to say is..."), 442 | CORRUPTIVE("CRP", "gžž", "What corruption!"), 443 | DEJECTIVE("DEJ", "žžg", "[dejected sigh]"), 444 | DELECTATIVE("DLC", "ẓmm", "Whee!"), 445 | DERISIVE("DRS", "pfc", "How foolish!"), 446 | DESPERATIVE("DES", "mřř", "I'm sorry to have to tell you..."), 447 | DIFFIDENT("DFD", "cč", "It's nothing, just..."), 448 | DISAPPROBATIVE("DPB", "ffx", "I don't like that..."), 449 | DISCONCERTIVE("DCC", "gzj", "I don't feel confortable about this..."), 450 | DISMISSIVE("DIS", "kff", "So what!"), 451 | DOLOROUS("DOL", "řřx", "Ow! Ouch!"), 452 | DUBITATIVE("DUB", "mmf", "I doubt it"), 453 | EUPHEMISTIC("EUP", "vvt", "Let me put it this way..."), 454 | EUPHORIC("EUH", "gzz", "What bliss!"), 455 | EXASPERATIVE("EXA", "kçç", "Don't you get it?"), 456 | EXIGENT("EXG", "rrs", "It's now or never!"), 457 | EXPERIENTIAL("EXP", "pss", "Well, now!"), 458 | FASCINATIVE("FSC", "žžj", "Cool! Wow!"), 459 | FORTUITOUS("FOR", "lzp", "All is well that ends well"), 460 | GRATIFICATIVE("GRT", "mmh", "Ahhhh! [physical pleasure]"), 461 | IMPATIENT("IPT", "žžv", "C'mon!"), 462 | IMPLICATIVE("IPL", "vll", "Of course,..."), 463 | INDIGNATIVE("IDG", "pšš", "How dare...!?"), 464 | INFATUATIVE("IFT", "vvr", "Praise be to...!"), 465 | INSIPID("ISP", "lçp", "How boring!"), 466 | INVIDIOUS("IVD", "řřn", "How unfair!"), 467 | IRONIC("IRO", "mmž", "Just great!"), 468 | MANDATORY("MND", "msk", "Take it or leave it"), 469 | OPTIMAL("OPT", "ççk", "So!/Totally!"), 470 | PERPLEXIVE("PPX", "llh", "Huh?"), 471 | PESSIMISTIC("PES", "ksp", "Pfft!"), 472 | PRESUMPTIVE("PSM", "nnţ", "It can only mean one thing..."), 473 | PROPITIOUS("PPT", "mll", "It's a wonder that..."), 474 | PROPOSITIVE("PPV", "sl", "Consider:"), 475 | PROSAIC("PSC", "žžt", "Meh."), 476 | REACTIVE("RAC", "kll", "My goodness!"), 477 | REFLECTIVE("RFL", "llm", "Look at it this way..."), 478 | RENUNCIATIVE("RNC", "mst", "So much for...!"), 479 | REPULSIVE("RPU", "šštļ", "Ew! Gross!"), 480 | REVELATIVE("RVL", "mmļ", "A-ha!"), 481 | SATIATIVE("SAT", "ļţ", "How satisfying!"), 482 | SKEPTICAL("SKP", "rnž", "Yeah, right!"), 483 | SOLICITATIVE("SOL", "ňňs", "Please"), 484 | STUPEFACTIVE("STU", "ļļč", "What the...?"), 485 | SUGGESTIVE("SGS", "ltç", "How about..."), 486 | TREPIDATIVE("TRP", "llč", "Oh, no!"), 487 | VEXATIVE("VEX", "ksk", "How annoying!"); 488 | 489 | override fun gloss(o: GlossOptions): String = when { 490 | o.concise -> short 491 | o.verbose -> "(${name.lowercase()}: “$representative“)" 492 | else -> "“${representative}“" 493 | } 494 | 495 | companion object { 496 | fun byCb(cb: String) = values().find { it.cb == cb } 497 | } 498 | } 499 | 500 | class RegisterAdjunct(private val register: Register, private val final: Boolean) : Glossable { 501 | override fun gloss(o: GlossOptions): String { 502 | return when (final) { 503 | false -> register.gloss(o) 504 | true -> "${register.gloss(o)}_END" 505 | } 506 | } 507 | 508 | } 509 | 510 | enum class Register(override val short: String, val initial: String, val final: String) : NoDefault { 511 | DISCURSIVE("DSV", "a", "ai"), 512 | PARENTHETICAL("PNT", "e", "ei"), 513 | SPECIFICATIVE("SPF", "i", "iu"), 514 | EXAMPLIFICATIVE("EXM", "o", "oi"), 515 | COGITANT("CGT", "ö", "üo"), 516 | MATHEMATICAL("MTH", "u", "ui"), 517 | CARRIER_END("CAR", "", "ü"); 518 | 519 | companion object { 520 | fun adjunctByVowel(v: String): RegisterAdjunct? { 521 | 522 | val matchesInitial = values().find { it.initial == v } 523 | if (matchesInitial != null) return RegisterAdjunct(matchesInitial, false) 524 | 525 | val matchesFinal = values().find { it.final == v } 526 | if (matchesFinal != null) return RegisterAdjunct(matchesFinal, true) 527 | 528 | return null 529 | } 530 | } 531 | } 532 | 533 | enum class Referent(override val short: String, private vararg val forms: String) : NoDefault { 534 | MONADIC_SPEAKER("1m", "l", "r", "ř"), 535 | MONADIC_ADDRESSEE("2m", "s", "š", "ž"), 536 | POLYADIC_ADDRESSEE("2p", "n", "t", "d"), 537 | MONADIC_ANIMATE_THIRD_PARTY("ma", "m", "p", "b"), 538 | POLYADIC_ANIMATE_THIRD_PARTY("pa", "ň", "k", "g"), 539 | MONADIC_INANIMATE_THIRD_PARTY("mi", "z", "ţ", "ḑ"), 540 | POLYADIC_INANIMATE_THIRD_PARTY("pi", "ẓ", "ļ", "f", "v"), 541 | MIXED_THIRD_PARTY("Mx", "c", "č", "j"), 542 | OBVIATIVE("Obv", "ll", "rr", "řř"), 543 | REDUPLICATIVE("Rdp", "th", "ph", "kh"), 544 | PROVISIONAL("PVS", "mm", "nn", "ňň"), 545 | CARRIER("[CAR]", "hl"), 546 | QUOTATIVE("[QUO]", "hm"), 547 | NAMING("[NAM]", "hn"), 548 | PHRASAL("[PHR]", "hň"); 549 | 550 | companion object { 551 | fun byForm(c: String): Referent? = values().find { c in it.forms } 552 | } 553 | } -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/Slots.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | // Formative slots 4 | 5 | fun parseCc(cc: String): Pair { 6 | val concatenation = when (cc) { 7 | "h", "hl", "hm" -> Concatenation.TYPE_ONE 8 | "hw", "hr", "hn" -> Concatenation.TYPE_TWO 9 | else -> null 10 | } 11 | 12 | val shortcut = when (cc) { 13 | "w", "hl", "hr" -> Shortcut.W 14 | "y", "hm", "hn" -> Shortcut.Y 15 | else -> null 16 | } 17 | 18 | return Pair(concatenation, shortcut) 19 | } 20 | 21 | private fun caOf( 22 | affiliation: Affiliation = Affiliation.CONSOLIDATIVE, 23 | configuration: Configuration = Configuration.UNIPLEX, 24 | extension: Extension = Extension.DELIMITIVE, 25 | perspective: Perspective = Perspective.MONADIC, 26 | essence: Essence = Essence.NORMAL, 27 | ): Slot { 28 | return Slot(affiliation, configuration, extension, perspective, essence) 29 | } 30 | 31 | fun parseVv(vv: String, shortcut: Shortcut?): Slot? { 32 | return if (vv in SPECIAL_VV_VOWELS) { 33 | parseSpecialVv(vv, shortcut) 34 | } else { 35 | parseNormalVv(vv, shortcut) 36 | } 37 | } 38 | 39 | fun parseNormalVv(v: String, shortcut: Shortcut?): Slot? { 40 | 41 | val (series, form) = seriesAndForm(v) 42 | 43 | val stem = when (form) { 44 | 1, 2 -> Stem.ONE 45 | 3, 4 -> Stem.TWO 46 | 9, 8 -> Stem.THREE 47 | 7, 6 -> Stem.ZERO 48 | else -> return null 49 | }.let { 50 | Underlineable(it) 51 | } 52 | 53 | 54 | val version = when (form) { 55 | 1, 3, 9, 7 -> Version.PROCESSUAL 56 | 2, 4, 8, 6 -> Version.COMPLETIVE 57 | else -> return null 58 | } 59 | 60 | val additional: Glossable 61 | 62 | when (shortcut) { 63 | null -> { 64 | additional = when (series) { 65 | 1 -> Slot() 66 | 2 -> CsAffix("r", Degree.FOUR) 67 | 3 -> CsAffix("t", Degree.FOUR) 68 | 4 -> CsAffix("t", Degree.FIVE) 69 | else -> return null 70 | } 71 | } 72 | 73 | Shortcut.W -> { 74 | additional = when (series) { 75 | 1 -> caOf() 76 | 2 -> caOf(perspective = Perspective.AGGLOMERATIVE) 77 | 3 -> caOf(perspective = Perspective.NOMIC) 78 | 4 -> caOf(perspective = Perspective.AGGLOMERATIVE, essence = Essence.REPRESENTATIVE) 79 | else -> return null 80 | } 81 | } 82 | 83 | Shortcut.Y -> { 84 | additional = when (series) { 85 | 1 -> caOf(extension = Extension.PROXIMAL) 86 | 2 -> caOf(essence = Essence.REPRESENTATIVE) 87 | 3 -> caOf(perspective = Perspective.ABSTRACT) 88 | 4 -> caOf(extension = Extension.PROXIMAL, essence = Essence.REPRESENTATIVE) 89 | else -> return null 90 | } 91 | } 92 | } 93 | 94 | return Slot(stem, version, additional) 95 | 96 | } 97 | 98 | fun parseSpecialVv(vv: String, shortcut: Shortcut?): Slot? { 99 | val version = when (vv) { 100 | "ëi", "eë", "ae" -> Version.PROCESSUAL 101 | "ëu", "oë", "ea" -> Version.COMPLETIVE 102 | else -> return null 103 | } 104 | 105 | val function = when (vv) { 106 | "ëi", "ëu" -> Function.STATIVE 107 | "eë", "oë" -> Function.DYNAMIC 108 | else -> null 109 | } 110 | 111 | val ca = if (shortcut != null && vv in setOf("ae", "ea")) { 112 | when (shortcut) { 113 | Shortcut.W -> caOf() 114 | Shortcut.Y -> caOf(extension = Extension.PROXIMAL) 115 | } 116 | } else if (shortcut != null) { 117 | return null 118 | } else null 119 | 120 | return Slot(version, function, ca) 121 | 122 | } 123 | 124 | fun parseVr(vr: String): Slot? { 125 | val (series, form) = seriesAndForm(vr) 126 | 127 | val specification = when (form) { 128 | 1, 9 -> Specification.BASIC 129 | 2, 8 -> Specification.CONTENTIAL 130 | 3, 7 -> Specification.CONSTITUTIVE 131 | 4, 6 -> Specification.OBJECTIVE 132 | else -> return null 133 | } 134 | val function = when (form) { 135 | 1, 2, 3, 4 -> Function.STATIVE 136 | 9, 8, 7, 6 -> Function.DYNAMIC 137 | else -> return null 138 | } 139 | 140 | val context = when (series) { 141 | 1 -> Context.EXISTENTIAL 142 | 2 -> Context.FUNCTIONAL 143 | 3 -> Context.REPRESENTATIONAL 144 | 4 -> Context.AMALGAMATIVE 145 | else -> return null 146 | } 147 | 148 | return Slot(function, specification, context) 149 | 150 | } 151 | 152 | fun parseAffixVr(vr: String): Slot? { 153 | val (series, form) = seriesAndForm(vr) 154 | .let { 155 | if (it == Pair(-1, -1)) { 156 | val zeroSeries = when (vr) { 157 | "ae" -> 1 158 | "ea" -> 2 159 | "üo" -> 3 160 | "üö" -> 4 161 | else -> return null 162 | } 163 | zeroSeries to 0 164 | } else it 165 | } 166 | 167 | val degree = Degree.byForm(form) ?: return null 168 | 169 | val context = when (series) { 170 | 1 -> Context.EXISTENTIAL 171 | 2 -> Context.FUNCTIONAL 172 | 3 -> Context.REPRESENTATIONAL 173 | 4 -> Context.AMALGAMATIVE 174 | else -> return null 175 | } 176 | 177 | return Slot(degree, context) 178 | } 179 | 180 | fun String.isGeminateCa(): Boolean = when { 181 | withIndex().any { (index, ch) -> ch == getOrNull(index + 1) } -> true 182 | this in CA_DEGEMINATIONS.keys -> true 183 | else -> false 184 | } 185 | 186 | fun String.isOvergeminatedCa(): Boolean = zipWithNext { a, b -> a == b }.count { it } >= 2 187 | 188 | fun String.degeminateCa(): String { 189 | val allomorph = CA_DEGEMINATIONS.keys.find { this.endsWith(it) } 190 | return when { 191 | allomorph != null -> replace(allomorph, CA_DEGEMINATIONS[allomorph]!!) 192 | zipWithNext().any { (a, b) -> a == b } -> 193 | zipWithNext { a, b -> if (a != b) b else "" } 194 | .joinToString("", prefix = take(1)) 195 | 196 | else -> this 197 | } 198 | } 199 | 200 | data class CaForms( 201 | val affiliation: String, 202 | val configuration: String, 203 | val extension: String, 204 | val perspectiveAndEssence: String, 205 | ) { 206 | fun affiliationStandalone() = 207 | configuration.isEmpty() && 208 | extension.isEmpty() && 209 | perspectiveAndEssence.isEmpty() 210 | 211 | fun perspectiveAndEssenceStandalone() = 212 | affiliation.isEmpty() && 213 | configuration.isEmpty() && 214 | extension.isEmpty() 215 | } 216 | 217 | fun gatherCaValues(ca: String): CaForms? { 218 | val affiliation = "[nrř]ļ\$|[lrř](?=.)".toRegex() 219 | val configuration = "[stckpţfçšzčžẓ]|[kpţf]s|[kp]š".toRegex() 220 | val extension = "(?<=^[lrř]?)(?:[gb]z|d)|[tkpgb]".toRegex() 221 | val perspectiveEssence = "^[lvj]|^tļ|[lrwyřmhnç]".toRegex() 222 | 223 | val fullRegex = "($affiliation)?($configuration)?($extension)?($perspectiveEssence)?".toRegex() 224 | 225 | val matches = fullRegex 226 | .matchEntire(ca) 227 | ?.groups 228 | ?.drop(1) 229 | ?.map { it?.value ?: "" } ?: return null 230 | 231 | return CaForms(matches[0], matches[1], matches[2], matches[3]) 232 | } 233 | 234 | fun parseCa(ca: String): Slot? { 235 | 236 | val unwoundCa = ca.substituteAll(CA_DESUBSTITUTIONS) 237 | 238 | val forms = gatherCaValues(unwoundCa) ?: return null 239 | 240 | val affiliation = if (forms.affiliationStandalone()) { 241 | when (forms.affiliation) { 242 | "" -> Affiliation.CONSOLIDATIVE 243 | "nļ" -> Affiliation.ASSOCIATIVE 244 | "rļ" -> Affiliation.COALESCENT 245 | "ň" -> Affiliation.VARIATIVE 246 | else -> return null 247 | } 248 | } else { 249 | when (forms.affiliation) { 250 | "" -> Affiliation.CONSOLIDATIVE 251 | "l" -> Affiliation.ASSOCIATIVE 252 | "r" -> Affiliation.COALESCENT 253 | "ř" -> Affiliation.VARIATIVE 254 | else -> return null 255 | } 256 | } 257 | 258 | val configuration = when (forms.configuration) { 259 | "" -> Configuration.UNIPLEX 260 | "s" -> Configuration.DUPLEX 261 | "t" -> Configuration.MULTIPLEX_SIMILAR_SEPARATE 262 | "c" -> Configuration.DUPLEX_SIMILAR_SEPARATE 263 | "k" -> Configuration.MULTIPLEX_SIMILAR_CONNECTED 264 | "ks" -> Configuration.DUPLEX_SIMILAR_CONNECTED 265 | "p" -> Configuration.MULTIPLEX_SIMILAR_FUSED 266 | "ps" -> Configuration.DUPLEX_SIMILAR_FUSED 267 | "ţ" -> Configuration.MULTIPLEX_DISSIMILAR_SEPARATE 268 | "ţs" -> Configuration.DUPLEX_DISSIMILAR_SEPARATE 269 | "f" -> Configuration.MULTIPLEX_DISSIMILAR_CONNECTED 270 | "fs" -> Configuration.DUPLEX_DISSIMILAR_CONNECTED 271 | "ç" -> Configuration.MULTIPLEX_DISSIMILAR_FUSED 272 | "š" -> Configuration.DUPLEX_DISSIMILAR_FUSED 273 | "z" -> Configuration.MULTIPLEX_FUZZY_SEPARATE 274 | "č" -> Configuration.DUPLEX_FUZZY_SEPARATE 275 | "ž" -> Configuration.MULTIPLEX_FUZZY_CONNECTED 276 | "kš" -> Configuration.DUPLEX_FUZZY_CONNECTED 277 | "ẓ" -> Configuration.MULTIPLEX_FUZZY_FUSED 278 | "pš" -> Configuration.DUPLEX_FUZZY_FUSED 279 | else -> return null 280 | } 281 | 282 | val extension = if (configuration != Configuration.UNIPLEX) { 283 | when (forms.extension) { 284 | "" -> Extension.DELIMITIVE 285 | "t" -> Extension.PROXIMAL 286 | "k" -> Extension.INCEPTIVE 287 | "p" -> Extension.ATTENUATIVE 288 | "g" -> Extension.GRADUATIVE 289 | "b" -> Extension.DEPLETIVE 290 | else -> return null 291 | } 292 | } else { 293 | when (forms.extension) { 294 | "" -> Extension.DELIMITIVE 295 | "d" -> Extension.PROXIMAL 296 | "g" -> Extension.INCEPTIVE 297 | "b" -> Extension.ATTENUATIVE 298 | "gz" -> Extension.GRADUATIVE 299 | "bz" -> Extension.DEPLETIVE 300 | else -> return null 301 | } 302 | } 303 | 304 | val (perspective, essence) = if (forms.perspectiveAndEssenceStandalone()) { 305 | when (forms.perspectiveAndEssence) { 306 | "l" -> Perspective.MONADIC to Essence.NORMAL 307 | "r" -> Perspective.AGGLOMERATIVE to Essence.NORMAL 308 | "v" -> Perspective.NOMIC to Essence.NORMAL 309 | "j" -> Perspective.ABSTRACT to Essence.NORMAL 310 | "tļ" -> Perspective.MONADIC to Essence.REPRESENTATIVE 311 | "ř" -> Perspective.AGGLOMERATIVE to Essence.REPRESENTATIVE 312 | "m", "h" -> Perspective.NOMIC to Essence.REPRESENTATIVE 313 | "n", "ç" -> Perspective.ABSTRACT to Essence.REPRESENTATIVE 314 | else -> return null 315 | } 316 | } else { 317 | when (forms.perspectiveAndEssence) { 318 | "" -> Perspective.MONADIC to Essence.NORMAL 319 | "r" -> Perspective.AGGLOMERATIVE to Essence.NORMAL 320 | "w" -> Perspective.NOMIC to Essence.NORMAL 321 | "y" -> Perspective.ABSTRACT to Essence.NORMAL 322 | "l" -> Perspective.MONADIC to Essence.REPRESENTATIVE 323 | "ř" -> Perspective.AGGLOMERATIVE to Essence.REPRESENTATIVE 324 | "m", "h" -> Perspective.NOMIC to Essence.REPRESENTATIVE 325 | "n", "ç" -> Perspective.ABSTRACT to Essence.REPRESENTATIVE 326 | else -> return null 327 | } 328 | } 329 | 330 | return Slot(affiliation, configuration, extension, perspective, essence) 331 | } 332 | 333 | fun parseVnCn(vn: String, cn: String, marksMood: Boolean = true, absoluteLevel: Boolean = false): Slot? { 334 | 335 | if (cn !in CN_CONSONANTS) return null 336 | 337 | val (series, form) = seriesAndForm(vn) 338 | 339 | if (absoluteLevel && (series != 4 || cn !in CN_PATTERN_ONE)) return null 340 | 341 | val vnValue: Glossable = if (cn in CN_PATTERN_ONE) { 342 | when (series) { 343 | 1 -> Valence.byForm(form) 344 | 2 -> Phase.byForm(form) 345 | 3 -> EffectAndPerson.byForm(form) 346 | 4 -> LevelAndRelativity(form, absoluteLevel) 347 | else -> return null 348 | } 349 | } else { 350 | Aspect.byVowel(vn) ?: return null 351 | } 352 | 353 | val cnValue: Glossable = when (cn) { 354 | "h", "w", "y" -> Mood.FACTUAL to CaseScope.NATURAL 355 | "hl", "hw" -> Mood.SUBJUNCTIVE to CaseScope.ANTECEDENT 356 | "hr", "hrw" -> Mood.ASSUMPTIVE to CaseScope.SUBALTERN 357 | "hm", "hmw" -> Mood.SPECULATIVE to CaseScope.QUALIFIER 358 | "hn", "hnw" -> Mood.COUNTERFACTIVE to CaseScope.PRECEDENT 359 | "hň", "hňw" -> Mood.HYPOTHETICAL to CaseScope.SUCCESSIVE 360 | else -> return null 361 | }.let { 362 | if (marksMood) it.first else it.second 363 | } 364 | 365 | return Slot(vnValue, cnValue) 366 | 367 | } 368 | 369 | fun parseVk(vk: String): Slot? { 370 | 371 | val (series, form) = seriesAndForm(vk) 372 | 373 | val illocution = when (series) { 374 | 1 -> Illocution.ASSERTIVE 375 | 2 -> when (form) { 376 | 1 -> Illocution.DIRECTIVE 377 | 2 -> Illocution.DECLARATIVE 378 | 3 -> Illocution.INTERROGATIVE 379 | 4 -> Illocution.VERIFICATIVE 380 | 381 | 6 -> Illocution.ADMONITIVE 382 | 7 -> Illocution.POTENTIATIVE 383 | 8 -> Illocution.HORTATIVE 384 | 9 -> Illocution.CONJECTURAL 385 | else -> return null 386 | } 387 | 388 | else -> return null 389 | } 390 | 391 | val validation = if (series == 2) null else { 392 | when (form) { 393 | 1 -> Validation.OBSERVATIONAL 394 | 2 -> Validation.RECOLLECTIVE 395 | 3 -> Validation.PURPORTIVE 396 | 4 -> Validation.REPORTIVE 397 | 5 -> Validation.UNSPECIFIED 398 | 6 -> Validation.IMAGINARY 399 | 7 -> Validation.CONVENTIONAL 400 | 8 -> Validation.INTUITIVE 401 | 9 -> Validation.INFERENTIAL 402 | else -> return null 403 | } 404 | } 405 | 406 | return Slot(illocution, validation) 407 | } 408 | 409 | fun parseIvlAffixVowel(vx: String): Slot? { 410 | val (series, form) = seriesAndForm(vx) 411 | 412 | val illocution = when (series) { 413 | 1 -> when (form) { 414 | 1 -> Illocution.ASSERTIVE 415 | 2 -> Illocution.DIRECTIVE 416 | 3 -> Illocution.DECLARATIVE 417 | 4 -> Illocution.INTERROGATIVE 418 | 5 -> Illocution.VERIFICATIVE 419 | 6 -> Illocution.ADMONITIVE 420 | 7 -> Illocution.POTENTIATIVE 421 | 8 -> Illocution.HORTATIVE 422 | 9 -> Illocution.CONJECTURAL 423 | else -> return null 424 | } 425 | 426 | 2 -> Illocution.ASSERTIVE 427 | else -> return null 428 | } 429 | 430 | val validation = if (series == 1) null else { 431 | when (form) { 432 | 1 -> Validation.OBSERVATIONAL 433 | 2 -> Validation.RECOLLECTIVE 434 | 3 -> Validation.PURPORTIVE 435 | 4 -> Validation.REPORTIVE 436 | 5 -> Validation.UNSPECIFIED 437 | 6 -> Validation.IMAGINARY 438 | 7 -> Validation.CONVENTIONAL 439 | 8 -> Validation.INTUITIVE 440 | 9 -> Validation.INFERENTIAL 441 | else -> return null 442 | } 443 | } 444 | 445 | return Slot(illocution, validation) 446 | } 447 | 448 | // Referentials 449 | 450 | fun parseSingleReferent(r: String): Slot? { 451 | val referent: Category = when (r) { 452 | "tļ" -> Perspective.AGGLOMERATIVE 453 | "ç", "x" -> Perspective.NOMIC 454 | "w", "y" -> Perspective.ABSTRACT 455 | else -> Referent.byForm(r) ?: return null 456 | } 457 | 458 | val effect = when (r) { 459 | "l", "s", "n", "m", "ň", "z", "ẓ", "ļ", "c", "th", "ll", "mm" -> Effect.NEUTRAL 460 | "r", "š", "t", "p", "k", "ţ", "f", "č", "ph", "rr", "nn" -> Effect.BENEFICIAL 461 | "ř", "ž", "d", "b", "g", "ḑ", "v", "j", "kh", "řř", "ňň" -> Effect.DETRIMENTAL 462 | else -> null 463 | } 464 | 465 | return Slot(referent, effect) 466 | } 467 | 468 | fun parseFullReferent(clusters: List): Referential? { 469 | 470 | val reflist: List = clusters.flatMap { c -> 471 | parseFullReferent(c) ?: return null 472 | } 473 | 474 | return when (reflist.size) { 475 | 0 -> null 476 | else -> Referential(reflist) 477 | } 478 | } 479 | 480 | fun parseFullReferent(c: String): Referential? { 481 | val referents = buildList { 482 | var index = 0 483 | 484 | while (index <= c.lastIndex) { 485 | 486 | val referent = if (index + 2 <= c.length && c.substring(index, index + 2) in BICONSONANTAL_REFERENTIALS) { 487 | parseSingleReferent(c.substring(index, index + 2)).also { index += 2 } 488 | } else { 489 | parseSingleReferent(c.substring(index, index + 1)).also { index++ } 490 | } 491 | 492 | if (referent != null) add(referent) 493 | } 494 | 495 | } 496 | 497 | return when (referents.size) { 498 | 0 -> null 499 | else -> Referential(referents) 500 | } 501 | 502 | } 503 | 504 | // Adjunct slots 505 | 506 | fun parseVh(vh: String): GlossString? = when (vh) { 507 | "a" -> GlossString("{scope over formative}", "{form.}") 508 | "e" -> GlossString("{scope over case/mood}", "{mood}") 509 | "i", "u" -> GlossString("{scope over formative, but not adjacent adjuncts}", "{under adj.}") 510 | "o" -> GlossString("{scope over formative and adjacent adjuncts}", "{over adj.}") 511 | else -> null 512 | } 513 | 514 | fun affixualAdjunctScope(vsCzVz: String?, isMultipleAdjunctVowel: Boolean = false): GlossString? { 515 | val scope = when (vsCzVz) { 516 | null -> if (isMultipleAdjunctVowel) "{same}" else "{VDom}" 517 | "h", "a" -> "{VDom}" 518 | "'h", "u" -> "{VSub}" 519 | "'hl", "e" -> "{VIIDom}" 520 | "'hr", "i" -> "{VIISub}" 521 | "hw", "o" -> "{formative}" 522 | "'hw", "ö" -> "{adjacent}" 523 | "ai" -> if (isMultipleAdjunctVowel) "{same}" else null 524 | else -> null 525 | } 526 | val isDefaultForm = if (isMultipleAdjunctVowel) scope == "{same}" else scope == "{VDom}" 527 | 528 | return scope?.let { GlossString(it, ignorable = isDefaultForm) } 529 | } 530 | 531 | 532 | -------------------------------------------------------------------------------- /src/ithkuil/iv/gloss/Words.kt: -------------------------------------------------------------------------------- 1 | package ithkuil.iv.gloss 2 | 3 | import ithkuil.iv.gloss.dispatch.logger 4 | 5 | // @formatter:off 6 | @Suppress("Reformat") // This looks like dogshit without the vertical alignment. 7 | fun parseWord(word: Word, marksMood: Boolean? = null): ParseOutcome { 8 | logger.info { "-> parseWord($word)" } 9 | 10 | val result = when (word.wordType) { 11 | WordType.BIAS_ADJUNCT -> parseBiasAdjunct (word) 12 | WordType.MOOD_CASESCOPE_ADJUNCT -> parseMoodCaseScopeAdjunct (word) 13 | WordType.REGISTER_ADJUNCT -> parseRegisterAdjunct (word) 14 | WordType.MODULAR_ADJUNCT -> parseModular (word, marksMood = marksMood) 15 | WordType.COMBINATION_REFERENTIAL -> parseCombinationReferential(word) 16 | WordType.AFFIXUAL_ADJUNCT -> parseAffixual (word) 17 | WordType.MULTIPLE_AFFIX_ADJUNCT -> parseMultipleAffix (word) 18 | WordType.REFERENTIAL -> parseReferential (word) 19 | WordType.FORMATIVE -> parseFormative (word) 20 | }.let { 21 | if (it is Parsed && word.hasSentencePrefix) { 22 | it.addPrefix(SENTENCE_START_GLOSS) 23 | } else it 24 | } 25 | 26 | logger.info { 27 | " parseWord($word) -> " + 28 | when (result) { 29 | is Parsed -> "Gloss(${result.gloss(GlossOptions(Precision.SHORT))})" 30 | is Error -> "Error(${result.message})" 31 | } 32 | } 33 | 34 | return result 35 | 36 | } 37 | // @formatter:on 38 | 39 | fun parseConcatenationChain(chain: ConcatenatedWords): ParseOutcome { 40 | 41 | for ((index, word) in chain.words.withIndex()) { 42 | val (concatenation, _) = parseCc(word[0]) 43 | 44 | if ((concatenation == null) xor (index == chain.words.lastIndex)) 45 | return Error("Invalid concatenation (at ${index + 1})") 46 | } 47 | 48 | val glosses = chain.words.mapIndexed { index, word -> 49 | 50 | val gloss = parseFormative(word, inConcatenationChain = true) 51 | 52 | val glossWithPrefix = if (gloss is Parsed && word.hasSentencePrefix) { 53 | if (index == 0) { 54 | gloss.addPrefix(SENTENCE_START_GLOSS) 55 | } else return Error("Sentence prefix inside concatenation chain") 56 | } else gloss 57 | 58 | when (glossWithPrefix) { 59 | is Parsed -> glossWithPrefix 60 | is Error -> return glossWithPrefix 61 | } 62 | } 63 | 64 | return ConcatenationChain(glosses) 65 | } 66 | 67 | fun parseBiasAdjunct(word: Word): ParseOutcome { 68 | val bias = Bias.byCb(word[0]) ?: return Error("Unknown bias: ${word[0]}") 69 | 70 | return Parsed(bias) 71 | } 72 | 73 | 74 | fun parseRegisterAdjunct(word: Word): ParseOutcome { 75 | val adjunct = Register.adjunctByVowel(word[1]) ?: return Error("Unknown register adjunct vowel: ${word[1]}") 76 | 77 | return Parsed(adjunct) 78 | } 79 | 80 | 81 | fun parseFormative(word: Word, inConcatenationChain: Boolean = false): ParseOutcome { 82 | 83 | val glottalIndices = mutableListOf() 84 | 85 | val groups = word.mapIndexed { index, group -> 86 | if ('\'' in group) { 87 | glottalIndices.add(index) 88 | unglottalizeVowel(group) 89 | } else group 90 | } 91 | 92 | var index = 0 93 | 94 | val (concatenation, shortcut) = if (groups[0] in CC_CONSONANTS) { 95 | index++ 96 | parseCc(groups[0]) 97 | } else Pair(null, null) 98 | 99 | val maxGlottalStops = if (shortcut == null) 2 else 3 100 | 101 | if (glottalIndices.size > maxGlottalStops) return Error("Too many glottal stops") 102 | 103 | if (!inConcatenationChain && concatenation != null) return Error("Lone concatenated formative") 104 | 105 | val relation = if (concatenation == null) { 106 | when (word.stress) { 107 | Stress.ANTEPENULTIMATE -> Relation.FRAMED 108 | else -> Relation.UNFRAMED 109 | } 110 | } else null 111 | 112 | val slotVFilledMarker = index in glottalIndices 113 | 114 | val vv: String = if (index == 0 && groups[0].isConsonant()) "a" else groups[index].also { index++ } 115 | 116 | val rootMode = when (vv) { 117 | "ëi", "eë", "ëu", "oë" -> RootMode.AFFIX 118 | "ae", "ea" -> RootMode.REFERENCE 119 | else -> RootMode.ROOT 120 | } 121 | 122 | if (rootMode == RootMode.AFFIX && shortcut != null) return Error("Shortcuts can't be used with a Cs-root") 123 | 124 | val slotII = parseVv(vv, shortcut) ?: return Error("Unknown Vv value: $vv") 125 | 126 | val cr = groups[index] 127 | 128 | if (cr.isInvalidRootForm()) return Error("Invalid root form: $cr") 129 | 130 | val root: Glossable = when (rootMode) { 131 | RootMode.ROOT -> { 132 | val stem = slotII.findIsInstance>() ?: return Error("Stem not found") 133 | Root(cr, stem) 134 | } 135 | 136 | RootMode.AFFIX -> { 137 | val form = when (val affixVr = groups[index + 1]) { 138 | in DEGREE_ZERO_CS_ROOT_FORMS -> 0 139 | else -> seriesAndForm(affixVr).second 140 | } 141 | val degree = Degree.byForm(form) ?: return Error("Unknown Cs-root degree: $form") 142 | CsAffix(cr, degree) 143 | } 144 | 145 | RootMode.REFERENCE -> { 146 | parseFullReferent(cr) ?: return Error("Unknown personal reference cluster: $cr") 147 | } 148 | 149 | } 150 | index++ 151 | 152 | val glottalShiftStartIndex = index 153 | 154 | val vr = if (shortcut != null) "a" else { 155 | groups.getOrNull(index).also { index++ } ?: return Error("Formative ended unexpectedly") 156 | } 157 | 158 | val slotIV = when (rootMode) { 159 | RootMode.ROOT, RootMode.REFERENCE -> parseVr(vr) ?: return Error("Unknown Vr value: $vr") 160 | RootMode.AFFIX -> parseAffixVr(vr) ?: return Error("Unknown Cs-root Vr value: $vr") 161 | } 162 | 163 | 164 | val csVxAffixes = if (shortcut == null) { 165 | var indexV = index 166 | 167 | buildList { 168 | while (true) { 169 | if (groups.getOrNull(indexV)?.isGeminateCa() == true) { 170 | index = indexV 171 | break 172 | } else if (indexV + 1 > groups.lastIndex || groups[indexV] in CN_CONSONANTS) { 173 | clear() 174 | indexV = index 175 | break 176 | } 177 | add(Affix(cs = groups[indexV], vx = groups[indexV + 1])) 178 | indexV += 2 179 | } 180 | } 181 | 182 | } else emptyList() 183 | 184 | 185 | if (shortcut == null) { 186 | if (!slotVFilledMarker && csVxAffixes.size >= 2) return Error("Unexpectedly many slot V affixes") 187 | if (slotVFilledMarker && csVxAffixes.size < 2) return Error("Unexpectedly few slot V affixes") 188 | } 189 | 190 | 191 | val slotV = csVxAffixes.parseAll().validateAll { return Error(it.message) } 192 | 193 | val isVerbal = word.stress in setOf(Stress.ULTIMATE, Stress.MONOSYLLABIC) 194 | 195 | var cnMoved = false 196 | 197 | val slotVI = if (shortcut != null) null else { 198 | val caForm = groups.getOrNull(index) ?: return Error("Formative ended unexpectedly") 199 | 200 | val caValue = when { 201 | caForm in CN_PATTERN_ONE -> { 202 | if (caForm == "h") return Error("Unexpected default Cn in slot VI") 203 | cnMoved = true 204 | parseVnCn("a", caForm, isVerbal) 205 | ?: return Error("Unknown Cn value in Ca: $caForm") 206 | } 207 | 208 | caForm.isGeminateCa() -> { 209 | if (caForm.isOvergeminatedCa()) return Error("Overgeminated Ca: $caForm") 210 | if (csVxAffixes.isEmpty()) return Error("Unexpected geminated Ca: $caForm") 211 | val ungeminated = caForm.degeminateCa() 212 | parseCa(ungeminated) ?: return Error("Unknown Ca value: $ungeminated") 213 | } 214 | 215 | else -> parseCa(caForm) ?: return Error("Unknown Ca value: $caForm") 216 | 217 | } 218 | 219 | index++ 220 | ForcedDefault(caValue, "{Ca}", condition = csVxAffixes.isNotEmpty()) 221 | } 222 | 223 | var endOfVxCsSlotVIndex: Int? = null 224 | 225 | val vxCsAffixes = buildList { 226 | while (true) { 227 | if (index + 1 > groups.lastIndex || groups[index + 1] in CN_CONSONANTS) 228 | break 229 | 230 | add(Affix(vx = groups[index], cs = groups[index + 1])) 231 | 232 | 233 | if (shortcut != null && index in glottalIndices) { 234 | 235 | if (slotVFilledMarker && size < 2) return Error("Unexpectedly few slot V affixes") 236 | else if (!slotVFilledMarker && size >= 2) return Error("Unexpectedly many slot V affixes") 237 | 238 | endOfVxCsSlotVIndex = size 239 | } 240 | 241 | index += 2 242 | } 243 | } 244 | 245 | if (shortcut != null && slotVFilledMarker && endOfVxCsSlotVIndex == null) return Error("Unexpectedly few slot V affixes") 246 | 247 | val endOfSlotVGloss = GlossString("{end of slot V}", "{Ca}") 248 | 249 | val slotVIIAndMaybeSlotV: List = vxCsAffixes 250 | .parseAll() 251 | .validateAll { return Error(it.message) } 252 | .let { affixList -> 253 | 254 | val endOfSlotVIndex = endOfVxCsSlotVIndex // Necessary for smart casting (KT-7186) 255 | 256 | if (endOfSlotVIndex != null) { 257 | buildList { 258 | addAll(affixList.subList(0, endOfSlotVIndex)) 259 | add(endOfSlotVGloss) 260 | addAll(affixList.subList(endOfSlotVIndex, affixList.size)) 261 | } 262 | } else affixList 263 | } 264 | 265 | val absoluteLevel = groups.getOrNull(index + 1) == "y" && 266 | groups.getOrNull(index + 3) in CN_PATTERN_ONE 267 | 268 | val slotVIII: Slot? = when { 269 | absoluteLevel -> { 270 | val vn = groups[index] + groups[index + 2] 271 | val cn = groups[index + 3] 272 | 273 | parseVnCn( 274 | vn, 275 | cn, 276 | marksMood = isVerbal, 277 | absoluteLevel = true 278 | ).also { index += 4 } 279 | ?: return Error("Unknown VnCn value: ${vn[0]}y${vn[1]}$cn") 280 | } 281 | 282 | groups.getOrNull(index + 1) in CN_CONSONANTS -> { 283 | parseVnCn( 284 | groups[index], 285 | groups[index + 1], 286 | marksMood = isVerbal 287 | )?.also { index += 2 } 288 | ?: return Error("Unknown VnCn value: ${groups[index] + groups[index + 1]}") 289 | } 290 | 291 | else -> null 292 | } 293 | 294 | val caseGlottal = if (shortcut == null) { 295 | if (cnMoved) { 296 | if (glottalIndices.any { it in glottalShiftStartIndex until groups.lastIndex }) { 297 | return Error("Unexpected glottal stop with a moved Cn") 298 | } 299 | } 300 | glottalIndices.any { it in glottalShiftStartIndex..(groups.lastIndex) } 301 | } else groups.last().isVowel() && groups.lastIndex in glottalIndices 302 | 303 | if (concatenation != null && caseGlottal) return Error("Unexpected glottal stop in concatenated formative") 304 | 305 | val vcVk = (groups.getOrNull(index) ?: "a") 306 | .let { 307 | if (caseGlottal) glottalizeVowel(it) else it 308 | } 309 | 310 | 311 | val slotIX: Glossable = if (concatenation != null) { 312 | when (word.stress) { 313 | Stress.PENULTIMATE -> Case.byVowel(vcVk) 314 | ?: return Error("Unknown Vf form $vcVk (penultimate stress)") 315 | 316 | Stress.MONOSYLLABIC, Stress.ULTIMATE -> Case.byVowel(glottalizeVowel(vcVk)) 317 | ?: return Error("Unknown Vf form $vcVk (ultimate stress)") 318 | 319 | Stress.ANTEPENULTIMATE -> return Error("Antepenultimate stress in concatenated formative") 320 | else -> return Error("Stress error") 321 | } 322 | } else { 323 | if (isVerbal) { 324 | parseVk(vcVk) ?: return Error("Unknown Vk form $vcVk") 325 | } else { 326 | Case.byVowel(vcVk) ?: return Error("Unknown Vc form $vcVk") 327 | } 328 | } 329 | index++ 330 | 331 | if (groups.lastIndex >= index) { 332 | val tail = groups.drop(index - 1).joinToString("") 333 | return Error("Formative continued unexpectedly: -$tail") 334 | } 335 | 336 | val slotList: List = listOfNotNull(concatenation, slotII, root, slotIV) + 337 | slotV + listOfNotNull(slotVI) + slotVIIAndMaybeSlotV + listOfNotNull(slotVIII, slotIX) 338 | 339 | return Parsed(slotList, stressMarked = relation) 340 | 341 | } 342 | 343 | private fun String.isInvalidRootForm(): Boolean = startsWith("h") || this in INVALID_ROOT_FORMS 344 | 345 | private inline fun Iterable<*>.findIsInstance(): R? { 346 | for (element in this) { 347 | if (element is R) return element 348 | } 349 | return null 350 | } 351 | 352 | fun parseModular(word: Word, marksMood: Boolean?): ParseOutcome { 353 | 354 | var index = 0 355 | 356 | val slot1 = when (word[0]) { 357 | "w" -> GlossString("{parent formative only}", "{parent}") 358 | "y" -> GlossString("{concatenated formative only}", "{concat.}") 359 | else -> null 360 | } 361 | 362 | if (slot1 != null) index++ 363 | 364 | val midSlotList: MutableList = mutableListOf() 365 | 366 | while (word.size > index + 2) { 367 | midSlotList.add( 368 | parseVnCn(word[index], word[index + 1], marksMood = marksMood ?: true, absoluteLevel = false) 369 | ?: return Error("Unknown VnCn: ${word[index]}${word[index + 1]}") 370 | ) 371 | index += 2 372 | } 373 | 374 | if (midSlotList.size > 3) return Error("Too many (>3) middle slots in modular adjunct: ${midSlotList.size}") 375 | 376 | val slot5 = when { 377 | midSlotList.isEmpty() -> Aspect.byVowel(word[index]) ?: return Error("Unknown aspect: ${word[index]}") 378 | else -> when (word.stress) { 379 | Stress.PENULTIMATE -> parseVnCn(word[index], "h", marksMood = true, absoluteLevel = false) 380 | ?: return Error("Unknown non-aspect Vn: ${word[index]}") 381 | 382 | Stress.ULTIMATE -> parseVh(word[index]) ?: return Error("Unknown Vh: ${word[index]}") 383 | Stress.ANTEPENULTIMATE -> return Error("Antepenultimate stress on modular adjunct") 384 | else -> return Error("Stress error: ${word.stress.name}") 385 | } 386 | 387 | } 388 | 389 | return Parsed(slot1, *midSlotList.toTypedArray(), slot5) 390 | 391 | } 392 | 393 | class Referential(private val referents: List) : Glossable, List by referents { 394 | override fun gloss(o: GlossOptions): String { 395 | return when (referents.size) { 396 | 0 -> "" 397 | 1 -> referents[0].gloss(o) 398 | else -> referents 399 | .joinToString(REFERENT_SEPARATOR, REFERENT_START, REFERENT_END) 400 | { it.gloss(o) } 401 | } 402 | } 403 | } 404 | 405 | fun parseReferential(word: Word): ParseOutcome { 406 | val essence = when (word.stress) { 407 | Stress.ULTIMATE -> Essence.REPRESENTATIVE 408 | Stress.MONOSYLLABIC, Stress.PENULTIMATE -> Essence.NORMAL 409 | Stress.ANTEPENULTIMATE -> return Error("Antepenultimate stress on referential") 410 | else -> return Error("Stress error") 411 | } 412 | 413 | if (word[0] in CP_CONSONANTS && word.size > 2) { 414 | return Error("Cp in referential not preceded by epenthetic \"üo\"") 415 | } 416 | 417 | if (word[0] == "üo" && word[1] !in CP_CONSONANTS) { 418 | return Error("Epenthetic \"üo\" not followed by a Cp form") 419 | } 420 | 421 | if (word[0] == "ë" && word[1] in CP_CONSONANTS) { 422 | return Error("Cp in referential not preceded by epenthetic \"äi\"") 423 | } 424 | 425 | val c1 = word 426 | .takeWhile { it !in setOf("w", "y") } 427 | .dropLast(1) 428 | .filter { it !in setOf("ë", "äi") } 429 | .takeIf { it.size <= 3 } ?: return Error("Too many (>3) initial consonant clusters") 430 | val refA = 431 | parseFullReferent(c1) ?: return Error("Unknown personal reference: $c1") 432 | var index = (word 433 | .indexOfFirst { it in setOf("w", "y") } 434 | .takeIf { it != -1 } ?: word.size) - 1 435 | 436 | val caseA = Case.byVowel(word[index]) ?: return Error("Unknown case: ${word[index]}") 437 | index++ 438 | 439 | when { 440 | word.getOrNull(index) in setOf("w", "y") -> { 441 | index++ 442 | val vc2 = word.getOrNull(index) ?: return Error("Referential ended unexpectedly") 443 | val caseB = Case.byVowel(vc2) ?: return Error("Unknown case: ${word[index]}") 444 | index++ 445 | 446 | val c2 = word.getOrNull(index) 447 | val refB = if (c2 != null) { 448 | parseFullReferent(c2) ?: return Error("Unknown personal reference cluster: $c2") 449 | } else null 450 | 451 | index++ 452 | if (word.getOrNull(index) == "ë") index++ 453 | 454 | if (word.size > index) return Error("Referential is too long") 455 | 456 | return Parsed(refA, Shown(caseA), Shown(caseB), refB, stressMarked = essence) 457 | 458 | } 459 | 460 | word.size > index + 1 -> return Error("Referential is too long") 461 | 462 | else -> return Parsed(refA, caseA, stressMarked = essence) 463 | } 464 | } 465 | 466 | fun parseCombinationReferential(word: Word): ParseOutcome { 467 | val essence = when (word.stress) { 468 | Stress.ULTIMATE -> Essence.REPRESENTATIVE 469 | Stress.MONOSYLLABIC, Stress.PENULTIMATE -> Essence.NORMAL 470 | Stress.ANTEPENULTIMATE -> return Error("Antepenultimate stress on combination referential") 471 | else -> return Error("Stress error") 472 | } 473 | var index = 0 474 | 475 | if (word[0] in setOf("ë", "a")) { 476 | if ((word[0] == "a") != (word[1] in CP_CONSONANTS)) { 477 | return Error("Epenthetic a must only be used with Suppletive forms") 478 | } 479 | index++ 480 | } 481 | 482 | 483 | val ref = parseFullReferent(word[index]) ?: return Error("Unknown referent: ${word[index]}") 484 | 485 | index++ 486 | 487 | val caseA = Case.byVowel(word[index]) ?: return Error("Unknown case: ${word[index]}") 488 | index++ 489 | 490 | val specification = when (word[index]) { 491 | "x" -> Specification.BASIC 492 | "xt" -> Specification.CONTENTIAL 493 | "xp" -> Specification.CONSTITUTIVE 494 | "xx" -> Specification.OBJECTIVE 495 | else -> return Error("Unknown combination referential Specification: ${word[index]}") 496 | } 497 | index++ 498 | 499 | val vxCsAffixes: MutableList = mutableListOf() 500 | while (true) { 501 | if (index + 1 > word.lastIndex) { 502 | break 503 | } 504 | 505 | val affix = Affix(word[index], word[index + 1]).parse().validate { return Error(it.message) } 506 | 507 | vxCsAffixes.add(affix) 508 | index += 2 509 | 510 | } 511 | 512 | val caseB = when (word.getOrNull(index)) { 513 | "a", null -> null 514 | "üa" -> Case.THEMATIC 515 | else -> Case.byVowel(word[index]) ?: return Error("Unknown case: ${word[index]}") 516 | } 517 | 518 | 519 | return Parsed( 520 | ref, Shown(caseA, condition = caseB != null), specification, 521 | *vxCsAffixes.toTypedArray(), 522 | caseB?.let { Shown(it) }, stressMarked = essence 523 | ) 524 | 525 | } 526 | 527 | fun parseMultipleAffix(word: Word): ParseOutcome { 528 | val concatOnly = if (word.stress == Stress.ULTIMATE) { 529 | GlossString("{concatenated formative only}", "{concat.}") 530 | } else null 531 | var index = 0 532 | if (word[0] == "ë") index++ 533 | 534 | val firstAffixVx = word[index + 1].removeSuffix("'") 535 | 536 | val czGlottal = firstAffixVx != word[index + 1] 537 | 538 | val firstAffix = Affix(cs = word[index], vx = firstAffixVx).parse().validate { return Error(it.message) } 539 | index += 2 540 | 541 | val cz = "${if (czGlottal) "'" else ""}${word[index]}" 542 | 543 | val scopeOfFirst = affixualAdjunctScope(cz) ?: return Error("Unknown Cz: ${word[index]}") 544 | index++ 545 | 546 | val vxCsAffixes: MutableList = mutableListOf() 547 | 548 | while (true) { 549 | if (index + 1 > word.lastIndex) break 550 | 551 | val affix = Affix(word[index], word[index + 1]).parse().validate { return Error(it.message) } 552 | vxCsAffixes.add(affix) 553 | index += 2 554 | } 555 | 556 | if (vxCsAffixes.isEmpty()) return Error("Only one affix found in multiple affix adjunct") 557 | 558 | val vz = word.getOrNull(index) 559 | 560 | val scopeOfRest = if (vz != null) { 561 | affixualAdjunctScope(vz, isMultipleAdjunctVowel = true) ?: return Error("Unknown Vz: $vz") 562 | } else null 563 | 564 | return Parsed(firstAffix, scopeOfFirst, *vxCsAffixes.toTypedArray(), scopeOfRest, stressMarked = concatOnly) 565 | 566 | } 567 | 568 | 569 | fun parseAffixual(word: Word): ParseOutcome { 570 | val concatOnly = if (word.stress == Stress.ULTIMATE) 571 | GlossString("{concatenated formative only}", "{concat.}") 572 | else null 573 | 574 | if (word.size < 2) return Error("Affixual adjunct too short: ${word.size}") 575 | 576 | val affix = Affix(word[0], word[1]).parse().validate { return Error(it.message) } 577 | 578 | val vs = word.getOrNull(2) 579 | val scope = affixualAdjunctScope(vs) ?: return Error("Unknown Vs: $vs") 580 | 581 | return Parsed(affix, scope, stressMarked = concatOnly) 582 | 583 | } 584 | 585 | fun parseMoodCaseScopeAdjunct(word: Word): ParseOutcome { 586 | val value: Glossable = when (val v = word[1]) { 587 | "a" -> Mood.FACTUAL 588 | "e" -> Mood.SUBJUNCTIVE 589 | "i" -> Mood.ASSUMPTIVE 590 | "o" -> Mood.SPECULATIVE 591 | "ö" -> Mood.COUNTERFACTIVE 592 | "u" -> Mood.HYPOTHETICAL 593 | "ai" -> CaseScope.NATURAL 594 | "ei" -> CaseScope.ANTECEDENT 595 | "iu" -> CaseScope.SUBALTERN 596 | "oi" -> CaseScope.QUALIFIER 597 | "ü" -> CaseScope.PRECEDENT 598 | "ui" -> CaseScope.SUCCESSIVE 599 | else -> return Error("Unknown Mood/Case-Scope adjunct vowel: $v") 600 | } 601 | 602 | return Parsed(Shown(value)) 603 | } --------------------------------------------------------------------------------