├── .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 | }
--------------------------------------------------------------------------------