diff --git a/README.md b/README.md index 44b3ceb..1813170 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # TextExplorer + This is a tool designed for the exploration and comparison of variants of textual data. ## Usage @@ -7,8 +8,9 @@ TODO ## Development -TODO +To create plugins for TextExplorer, see the [API ReadMe](api/README.md) for details. ## Support -If you have any problems or questions about this project, please get in touch. You can also [open an issue](https://github.com/Paulanerus/TextExplorer/issues) on GitHub. \ No newline at end of file +If you have any problems or questions about this project, please get in touch. You can +also [open an issue](https://github.com/Paulanerus/TextExplorer/issues) on GitHub. \ No newline at end of file diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..1653394 --- /dev/null +++ b/api/README.md @@ -0,0 +1,279 @@ +# API Development + +This ReadMe provides an overview of the TextExplorer API and guides you through developing your own plugin. A sample +implementation is available in [plugins/demo](../plugins/demo). + +While using a build tool is not mandatory, this documentation employs [Gradle](https://gradle.org/) in its examples to +align with the project's overall setup. + +## Environment setup + +1. Install Java 21. +2. Create a new Gradle (Kotlin DSL) project. +3. Download the latest [API release](https://github.com/Paulanerus/TextExplorer/releases/latest) and add the JAR as a + local dependency: + +```kotlin +implementation(files("libs/api-0.32.2.jar")) +``` + +4. Reload the Gradle project. + +## First plugin + +To create your first plugin, follow these steps: + +1. Create a Kotlin class, e.g., `PluginMain`, and implement the `IPlugin` interface: + +```kotlin +class PluginMain : IPlugin { + override fun init() { + println("Hello world from plugin.") + } +} +``` + +The `init` function is executed when the application starts or the plugin is loaded via the interface. Use this function +to initialize external services or perform setup tasks. + +2. Annotate the class with `@PluginMetadata` to declare the plugin's metadata. You can include additional information + like the version, author, or a short description. + +3. Optionally, use the `@PluginOrder` annotation to specify the load order for the plugin. + +4. Update the Gradle `jar` task to include the plugin's main class in the JAR file manifest: + +```kotlin +tasks.jar { + manifest { + attributes["Main-Class"] = "PluginMain" // Adjust the path if necessary + } +} +``` + +## Provide data + +To connect existing data to your plugin, annotate it with `@RequiresData` and specify the identifier of the data pool. +For example, to connect to `demo_data`, annotate the class with: +`@RequiresData("demo_data")`. + +To import your own data, a "data-layout" is required. It's recommended to create a separate Kotlin file (e.g., +`Data.kt`) containing the relevant classes for the data structure. + +Consider the following CSV files: + +**authors.csv:** + +```text +author_id,full_name,year_born,birth_place +...,...,...,... +... +``` + +**quotes.csv:** + +```text +quote,date,author_id +...,...,...,... +... +``` + +In Data.kt, define your data classes to match the column names in the CSV files: + +```kotlin +@DataSource("authors") +data class Author( + @Unique(true) val author_id: Long, + val full_name: String, + val year_born: String +) + +@DataSource("quotes") +data class Quote( + @Index(Language.ENGLISH) val quote: String, + val date: String, + @Link(Author::class) val author_id: Long +) +``` + ++ `@DataSource`: Links the respective CSV file to the class. Ensure the class name matches the CSV name (the file + extension is optional). + ++ `@Unique`: Marks the field as unique. If `true` and the type is `Long`, it will serve as the identifier and will not + be auto-generated by TextExplorer. + ++ `@Index`: Marks the field as indexed in the specified language, which is essential for search functionality. It can be + marked as the default value and must occur at least once in any class. + ++ `@Link`: Establishes a relationship between two classes. Use this with `@Index` to enable related data display in the + interface. The field name must be the same in both classes. + +Finally, update the plugin to include the created data classes as part of its data requirements: + +```kotlin +@RequiresData("demo_data", sources = [Author::class, Quote::class]) +class PluginMain : IPlugin { + ... +} +``` + +This will import the data appropriately, making it available for use in TextExplorer. + +**Currently only data in csv-files is supported.** + +## Variants + +Different terms can refer to the same entity, such as "USA," "US," "United States," or "America" for the country. +TextExplorer provides the `Variant API` to handle these cases. + +With the Variant API, you can define a column in a CSV file as the "base" term (the one the user will search for), and +additional columns can store variant terms. This allows users to search for any variant. + +**Example: Countries** + +**countries.csv** + +```text +base,variant +USA,USA +USA,US +USA,United States +USA,America +... +``` + +In this example, "USA" is the base term, and the other terms are variants. The TextExplorer representation would look +like this: + +```kotlin +@Variant(base = "base", ["variant"]) +@DataSource("countries") +data class Country(val base: String, val variant: String) +``` + +To connect this data to your plugin, you need to update the `@RequiresData` annotation to include the `Country` class: + +```kotlin +@RequiresData("countries", sources = [Country::class, Author::class, Quote::class]) +class PluginMain : IPlugin { + ... +} +``` + +This setup allows the user to search for `@countries:USA`, and TextExplorer will return all entries that match any of +the variants declared for "USA." + +## Tagging API + +The Tagging API allows you to highlight specific words (e.g., names) within the Tagging View. + +To implement this functionality, simply implement the tag function from the Taggable interface in your Plugin Main class. +This function takes the field name and its corresponding value as parameters. + +Additionally, annotate the function with `@ViewFilter`, which specifies a filter name and the fields it accepts. +Optionally, you can use the global parameter to apply the tags to the DiffView. + +To highlight the name "Tom" in every field, the implementation would look like this: + +```kotlin +@ViewFilter("Name Highlighter", fields = ["quote"], global = true) +override fun tag(field: String, value: String): Map = mapOf("Tom" to Tag("NAME", Color.blue)) +``` + +In this example: + ++ Only the quote field is passed to the tag function. ++ The word "Tom" is mapped to the Tag with the identifier `NAME` and the color blue. ++ Every occurrence of "Tom" in the quote field will be highlighted in blue. + +## Pre filtering + +**Not yet implemented** + +## Access plugin data + +**Not yet implemented** + +## Export plugin + +To export the plugin, run the Gradle `jar` task: + +```shell +./gradlew jar +``` + +For Windows use: + +```shell +gradlew.bat jar +``` + +### A full example: + +**PluginMain:** + +```kotlin +@PluginOrder(3) +@RequiresData("demo_data", sources = [Country::class, Author::class, Quote::class]) +@PluginMetadata("demo", author = "Author", version = "1.0.0", description = "A short description.") +class PluginMain : IPlugin, Taggable { + + override fun init() { + println("Hello world from plugin.") + } + + @ViewFilter("Name Highlighter", fields = ["quote"], global = true) + override fun tag(field: String, value: String): Map = mapOf("Tom" to Tag("NAME", Color.blue)) +} +``` + +**Data.kt:** + +```kotlin +@Variant(base = "base", ["variant"]) +@DataSource("countries") +data class Country(val base: String, val variant: String) + +@DataSource("authors") +data class Author( + @Unique(true) val author_id: Long, + val full_name: String, + val year_born: String +) + +@DataSource("quotes") +data class Quote( + @Index(Language.ENGLISH) val quote: String, + val date: String, + @Link(Author::class) val author_id: Long +) +``` + +**build.gradle.kts** + +```kotlin +plugins { + kotlin("jvm") version "2.0.21" +} + +group = "com.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation(files("libs/api-0.32.2.jar")) +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "PluginMain" + } +} + +kotlin { + jvmToolchain(21) +} +``` \ No newline at end of file diff --git a/api/src/main/kotlin/dev/paulee/api/data/Data.kt b/api/src/main/kotlin/dev/paulee/api/data/Data.kt index 4960a80..ac5ff62 100644 --- a/api/src/main/kotlin/dev/paulee/api/data/Data.kt +++ b/api/src/main/kotlin/dev/paulee/api/data/Data.kt @@ -51,6 +51,9 @@ enum class Language { @Target(AnnotationTarget.CLASS) annotation class Variant(val base: String, val variants: Array) +@Target(AnnotationTarget.CLASS) +annotation class PreFilter(val key: String, val linkKey: String, val value: String) + @Target(AnnotationTarget.CLASS) annotation class RequiresData(val name: String, val sources: Array> = []) diff --git a/api/src/main/kotlin/dev/paulee/api/data/provider/StorageProvider.kt b/api/src/main/kotlin/dev/paulee/api/data/provider/StorageProvider.kt index c92ec53..2534a62 100644 --- a/api/src/main/kotlin/dev/paulee/api/data/provider/StorageProvider.kt +++ b/api/src/main/kotlin/dev/paulee/api/data/provider/StorageProvider.kt @@ -19,13 +19,15 @@ interface IStorageProvider : Closeable { name: String, ids: Set = emptySet(), whereClause: List = emptyList(), + filter: List = emptyList(), offset: Int = 0, - limit: Int = Int.MAX_VALUE + limit: Int = Int.MAX_VALUE, ): List> fun count( name: String, ids: Set = emptySet(), - whereClause: List = emptyList() + whereClause: List = emptyList(), + filter: List = emptyList(), ): Long } \ No newline at end of file diff --git a/core/src/main/kotlin/dev/paulee/core/data/DataServiceImpl.kt b/core/src/main/kotlin/dev/paulee/core/data/DataServiceImpl.kt index eb4b22e..adea8b8 100644 --- a/core/src/main/kotlin/dev/paulee/core/data/DataServiceImpl.kt +++ b/core/src/main/kotlin/dev/paulee/core/data/DataServiceImpl.kt @@ -35,7 +35,7 @@ private class DataPool(val indexer: Indexer, dataInfo: RequiresData) { val links = mutableMapOf() - val replacements = mutableMapOf() + val metadata = mutableMapOf() private val keyValueRgx = "\\w+:\\w+|\\w+:\"[^\"]*\"".toRegex() @@ -43,7 +43,9 @@ private class DataPool(val indexer: Indexer, dataInfo: RequiresData) { dataInfo.sources.forEach { clazz -> val file = clazz.findAnnotation()?.file ?: return@forEach - clazz.findAnnotation()?.let { replacements[file] = it } + clazz.findAnnotation()?.let { metadata[file] = it } + + clazz.findAnnotation()?.let { metadata[file] = it } val normalized = normalizeDataSource(file) @@ -81,17 +83,14 @@ private class DataPool(val indexer: Indexer, dataInfo: RequiresData) { if (this.defaultIndexField.isNullOrEmpty()) { println("${dataInfo.name} has no default index field.") - this.fields.filter { it.value } - .entries - .firstOrNull()?.key - .let { - if (it == null) { - println("${dataInfo.name} has no indexable fields.") - } else { - println("'$it' was chosen instead.") - this.defaultIndexField = it - } + this.fields.filter { it.value }.entries.firstOrNull()?.key.let { + if (it == null) { + println("${dataInfo.name} has no indexable fields.") + } else { + println("'$it' was chosen instead.") + this.defaultIndexField = it } + } } } @@ -262,8 +261,8 @@ class DataServiceImpl(private val storageProvider: IStorageProvider) : IDataServ override fun hasSelectedPool(): Boolean = this.currentPool != null && this.currentField != null - override fun getAvailablePools(): Set = dataPools.filter { it.value.fields.any { it.value } } - .flatMap { entry -> + override fun getAvailablePools(): Set = + dataPools.filter { it.value.fields.any { it.value } }.flatMap { entry -> entry.value.fields.filter { it.value }.map { "${entry.key}.${it.key.substringBefore(".")}" } }.toSet() @@ -277,14 +276,17 @@ class DataServiceImpl(private val storageProvider: IStorageProvider) : IDataServ val dataPool = this.dataPools[this.currentPool] ?: return Pair(emptyList(), emptyMap()) - val indexResult = dataPool.search(this.handleReplacements(dataPool.replacements, query)) + val (filterQuery, filter) = this.getPreFilter(query, dataPool) - if (indexResult.isEmpty()) return Pair(emptyList(), emptyMap()) + val indexResult = dataPool.search(this.handleReplacements(dataPool.metadata, filterQuery)) + + if (filter.isEmpty() && indexResult.isEmpty()) return Pair(emptyList(), emptyMap()) val entries = this.storageProvider.get( "${this.currentPool}.${this.currentField}", indexResult.ids, indexResult.tokens, + filter, offset = pageCount * this.pageSize, limit = pageSize ) @@ -299,11 +301,9 @@ class DataServiceImpl(private val storageProvider: IStorageProvider) : IDataServ if (fields.isEmpty()) return@forEach - links[keyField] = - this.storageProvider.get( - "${this.currentPool}.$valSource", - whereClause = fields.map { "$valField:$it" }.toList() - ) + links[keyField] = this.storageProvider.get( + "${this.currentPool}.$valSource", whereClause = fields.map { "$valField:$it" }.toList() + ) } val result = PageResult(entries, links) @@ -319,12 +319,19 @@ class DataServiceImpl(private val storageProvider: IStorageProvider) : IDataServ val dataPool = this.dataPools[this.currentPool] ?: return Pair(-1, emptySet()) - val indexResult = dataPool.search(this.handleReplacements(dataPool.replacements, query)) + val (filterQuery, filter) = this.getPreFilter(query, dataPool) - if (indexResult.isEmpty()) return return Pair(0, emptySet()) + val indexResult = dataPool.search(this.handleReplacements(dataPool.metadata, filterQuery)) + + if (filter.isEmpty() && indexResult.isEmpty()) return return Pair(0, emptySet()) val count = - this.storageProvider.count("${this.currentPool}.${this.currentField}", indexResult.ids, indexResult.tokens) + this.storageProvider.count( + "${this.currentPool}.${this.currentField}", + indexResult.ids, + indexResult.tokens, + filter + ) return Pair(ceil(count / pageSize.toDouble()).toLong(), indexResult.indexedValues) } @@ -353,4 +360,40 @@ class DataServiceImpl(private val storageProvider: IStorageProvider) : IDataServ } else "" } } + + private fun getPreFilter(query: String, dataPool: DataPool): Pair> { + val regex = Regex("@[^:\\s]+:[^:\\s]+:[^:\\s]+") + + val filters = regex.findAll(query).map { it.value }.toSet() + + val queryWithoutFilter = filters.fold(query) { acc, filter -> acc.replace(filter, "") }.trim() + + return queryWithoutFilter to filters.filter { it.startsWith("@") && it.count { c -> c == ':' } == 2 } + .flatMap { rawFilter -> + val (filter, linkValue, value) = rawFilter.substring(1).split(":", limit = 3) + + dataPool.metadata.entries.filter { it.key == filter && it.value is PreFilter } + .flatMap { (key, transform) -> + val preFilter = transform as PreFilter + + val linkEntries = dataPool.links.filterKeys { it.startsWith(key) }.mapValues { link -> + val (source, field) = link.value.split(".", limit = 2) + source to field + } + + linkEntries.values.flatMap { (source, field) -> + val ids = storageProvider.get( + "$currentPool.$source", whereClause = listOf("${preFilter.linkKey}:$linkValue") + ).mapNotNull { it[field]?.let { id -> "$field:$id" } } + + val transformKey = preFilter.key + + storageProvider.get( + "$currentPool.$key", + whereClause = ids + "${preFilter.value}:$value" + ).mapNotNull { it[transformKey]?.let { id -> "$transformKey:$id" } } + } + } + }.distinct() + } } \ No newline at end of file diff --git a/core/src/main/kotlin/dev/paulee/core/data/provider/BinaryProvider.kt b/core/src/main/kotlin/dev/paulee/core/data/provider/BinaryProvider.kt index a293465..0ce8b04 100644 --- a/core/src/main/kotlin/dev/paulee/core/data/provider/BinaryProvider.kt +++ b/core/src/main/kotlin/dev/paulee/core/data/provider/BinaryProvider.kt @@ -75,8 +75,9 @@ internal class BinaryProvider : IStorageProvider { name: String, ids: Set, whereClause: List, + filter: List, offset: Int, - limit: Int + limit: Int, ): List> { TODO("Not yet implemented") } @@ -84,7 +85,8 @@ internal class BinaryProvider : IStorageProvider { override fun count( name: String, ids: Set, - whereClause: List + whereClause: List, + filter: List, ): Long { TODO("Not yet implemented") } diff --git a/core/src/main/kotlin/dev/paulee/core/data/provider/SQLiteProvider.kt b/core/src/main/kotlin/dev/paulee/core/data/provider/SQLiteProvider.kt index 4a51c6a..92ba570 100644 --- a/core/src/main/kotlin/dev/paulee/core/data/provider/SQLiteProvider.kt +++ b/core/src/main/kotlin/dev/paulee/core/data/provider/SQLiteProvider.kt @@ -39,42 +39,58 @@ internal class SQLiteProvider : IStorageProvider { name: String, ids: Set, whereClause: List, + filter: List, offset: Int, - limit: Int + limit: Int, ): List> { - val sourceName = name.substringBefore(".") - val tableName = name.substringAfter(".") - - val db = dataSources[sourceName] ?: return emptyList() - - var entries = whereClause.filter { it.contains(":") }.groupBy { it.substringBefore(":") } - .mapValues { it.value.map { it.substringAfter(":") } }.toMutableMap() - - val primaryKey = db.primaryKeyOf(tableName) ?: return emptyList() - - if (ids.isNotEmpty()) entries += primaryKey to ids.map { it.toString() }.toList() - - return db.selectAll(tableName, entries, offset = offset, limit = limit) + val (tableName, entries) = this.getEntries(name, ids, whereClause, filter) ?: return emptyList() + return dataSources[name.substringBefore(".")]!!.selectAll(tableName, entries, offset = offset, limit = limit) } override fun count( name: String, ids: Set, - whereClause: List + whereClause: List, + filter: List, ): Long { + val (tableName, entries) = this.getEntries(name, ids, whereClause, filter) ?: return 0 + return dataSources[name.substringBefore(".")]!!.count(tableName, entries) + } + + private fun getEntries( + name: String, + ids: Set, + whereClause: List, + filter: List, + ): Pair>>? { val sourceName = name.substringBefore(".") val tableName = name.substringAfter(".") - val db = dataSources[sourceName] ?: return 0 + val db = dataSources[sourceName] ?: return null var entries = whereClause.filter { it.contains(":") }.groupBy { it.substringBefore(":") } .mapValues { it.value.map { it.substringAfter(":") } }.toMutableMap() - val primaryKey = db.primaryKeyOf(tableName) ?: return 0 + val primaryKey = db.primaryKeyOf(tableName) ?: return null if (ids.isNotEmpty()) entries += primaryKey to ids.map { it.toString() }.toList() - return db.count(tableName, entries) + if (filter.isNotEmpty()) { + val groupedFilters = filter.filter { it.contains(":") }.groupBy { it.substringBefore(":") } + .mapValues { it.value.map { it.substringAfter(":") } }.toMutableMap() + + if (entries.isEmpty()) return tableName to groupedFilters + + entries.replaceAll { key, values -> + groupedFilters[key]?.let { filterValues -> values.filter { it in filterValues } } ?: values + } + + entries.entries.removeAll { it.value.isEmpty() } + + if (entries.isEmpty()) return null + } + + return tableName to entries } override fun close() = dataSources.values.forEach { it.close() } diff --git a/core/src/main/kotlin/dev/paulee/core/data/sql/Database.kt b/core/src/main/kotlin/dev/paulee/core/data/sql/Database.kt index 874e692..e5ad24d 100644 --- a/core/src/main/kotlin/dev/paulee/core/data/sql/Database.kt +++ b/core/src/main/kotlin/dev/paulee/core/data/sql/Database.kt @@ -3,7 +3,6 @@ package dev.paulee.core.data.sql import dev.paulee.api.data.DataSource import dev.paulee.api.data.Unique import dev.paulee.core.normalizeDataSource -import dev.paulee.core.toSnakeCase import org.jetbrains.annotations.Nullable import java.io.Closeable import java.nio.file.Path @@ -81,24 +80,23 @@ private class Table(val name: String, columns: List) { connection: Connection, whereClause: Map> = emptyMap>(), offset: Int = 0, - limit: Int = Int.MAX_VALUE + limit: Int = Int.MAX_VALUE, ): List> { val query = buildString { append("SELECT * FROM ") append(name) if (whereClause.isNotEmpty()) { - val clause = whereClause.entries.joinToString(" AND ") { (column, values) -> - val columnType = getColumnType(column) ?: return@joinToString "" + val clause = whereClause.entries.filter { getColumnType(it.key) != null } + .joinToString(" AND ") { (column, values) -> + val columnType = getColumnType(column) ?: return@joinToString "" - val inClause = values.joinToString( - ", ", - prefix = "IN (", - postfix = ")" - ) { if (columnType == ColumnType.TEXT) "'$it'" else it } + val inClause = values.joinToString( + ", ", prefix = "IN (", postfix = ")" + ) { if (columnType == ColumnType.TEXT) "'$it'" else it } - "$column $inClause" - } + "$column $inClause" + } if (clause.isNotEmpty()) { append(" WHERE ") @@ -134,17 +132,16 @@ private class Table(val name: String, columns: List) { append(name) if (whereClause.isNotEmpty()) { - val clause = whereClause.entries.joinToString(" AND ") { (column, values) -> - val columnType = getColumnType(column) ?: return@joinToString "" + val clause = whereClause.entries.filter { getColumnType(it.key) != null } + .joinToString(" AND ") { (column, values) -> + val columnType = getColumnType(column) ?: return@joinToString "" - val inClause = values.joinToString( - ", ", - prefix = "IN (", - postfix = ")" - ) { if (columnType == ColumnType.TEXT) "'$it'" else it } + val inClause = values.joinToString( + ", ", prefix = "IN (", postfix = ")" + ) { if (columnType == ColumnType.TEXT) "'$it'" else it } - "$column $inClause" - } + "$column $inClause" + } if (clause.isNotEmpty()) { append(" WHERE ") @@ -220,7 +217,7 @@ internal class Database(path: Path) : Closeable { name: String, whereClause: Map> = emptyMap>(), offset: Int = 0, - limit: Int = Int.MAX_VALUE + limit: Int = Int.MAX_VALUE, ): List> { val table = tables.find { it.name == name } ?: return emptyList() @@ -243,8 +240,7 @@ internal class Database(path: Path) : Closeable { conn.autoCommit = false return try { - runCatching { conn.block() } - .onSuccess { + runCatching { conn.block() }.onSuccess { conn.commit() }.onFailure { conn.rollback() diff --git a/core/src/main/kotlin/dev/paulee/core/plugin/PluginServiceImpl.kt b/core/src/main/kotlin/dev/paulee/core/plugin/PluginServiceImpl.kt index d0163a0..74ca831 100644 --- a/core/src/main/kotlin/dev/paulee/core/plugin/PluginServiceImpl.kt +++ b/core/src/main/kotlin/dev/paulee/core/plugin/PluginServiceImpl.kt @@ -14,6 +14,7 @@ import kotlin.io.path.isDirectory import kotlin.io.path.walk import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.functions +import kotlin.reflect.full.hasAnnotation import kotlin.reflect.full.primaryConstructor class PluginServiceImpl : IPluginService { @@ -41,6 +42,8 @@ class PluginServiceImpl : IPluginService { this.collectClasses(path).filter { it != entryPoint } .forEach { runCatching { Class.forName(it, true, classLoader) } } + if (!plugin::class.hasAnnotation()) return null + if (init) plugin.init() this.plugins.add(plugin) diff --git a/gradle.properties b/gradle.properties index 81ca371..52c1b76 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ kotlin.version=2.1.0 compose.version=1.7.1 lucene.version=10.0.0 -api.version=0.32.2 -core.version=0.30.3 -ui.version=0.21.7 -app.version=1.7.0 +api.version=0.33.2 +core.version=0.32.0 +ui.version=0.21.9 +app.version=1.8.0 diff --git a/plugins/demo/src/main/kotlin/dev/paulee/demo/DemoPlugin.kt b/plugins/demo/src/main/kotlin/dev/paulee/demo/DemoPlugin.kt index 7975471..805eca9 100644 --- a/plugins/demo/src/main/kotlin/dev/paulee/demo/DemoPlugin.kt +++ b/plugins/demo/src/main/kotlin/dev/paulee/demo/DemoPlugin.kt @@ -1,16 +1,20 @@ package dev.paulee.demo import dev.paulee.api.data.RequiresData +import dev.paulee.api.data.ViewFilter import dev.paulee.api.plugin.IPlugin import dev.paulee.api.plugin.PluginMetadata -import dev.paulee.api.plugin.PluginOrder +import dev.paulee.api.plugin.Tag +import dev.paulee.api.plugin.Taggable -@PluginOrder(4) -@RequiresData(name = "demo", [Verse::class]) -@PluginMetadata(name = "Demo-Plugin", version = "1.0.0", author = "Paul") -class DemoPlugin : IPlugin { +@RequiresData(name = "greek_variant", [Occurrence::class, Name::class, Manuscript::class, Verse::class]) +@PluginMetadata(name = "GreekVariant-Plugin", version = "1.0.0", author = "Paul") +class DemoPlugin : IPlugin, Taggable { override fun init() { - println("${greeting()} from Demo-Plugin") + println("Loaded GreekVariant Plugin") } + + @ViewFilter("DemoTag", fields = ["text"], global = true) + override fun tag(field: String, value: String): Map = emptyMap() } \ No newline at end of file diff --git a/plugins/demo/src/main/kotlin/dev/paulee/demo/Utility.kt b/plugins/demo/src/main/kotlin/dev/paulee/demo/Utility.kt index c12bc13..97e82e8 100644 --- a/plugins/demo/src/main/kotlin/dev/paulee/demo/Utility.kt +++ b/plugins/demo/src/main/kotlin/dev/paulee/demo/Utility.kt @@ -1,8 +1,47 @@ package dev.paulee.demo -import dev.paulee.api.data.DataSource +import dev.paulee.api.data.* -fun greeting() = "Hello world" +@PreFilter(key = "verse_id", linkKey = "variant", value = "occurrence") +@DataSource("occurrences") +data class Occurrence(val verse_id: Long, val variantID: Long, val occurrence: String, @Link(Name::class) val wordID: Long) -@DataSource("verses.csv") -data class Verse(val text: String, val year: String, val book: Int) \ No newline at end of file +@Variant(base = "label_en", variants = ["label_el_norm", "variant"]) +@DataSource("names") +data class Name( + val label_en: String, + val gender: String, + val label_el_norm: String, + val factgrid: String, + val variant: String, + val wordID: Long, + val variantID: Long, +) + +@DataSource("manuscripts") +data class Manuscript( + val ga: String, + val source: String, + val docID: String, + val pagesCount: Long, + val leavesCount: Long, + val century: String, + val label: String, + val dbpedia: String, +) + +@DataSource("verses") +data class Verse( + val bkv: String, + val edition_date: String, + val edition_version: String, + val encoding_version: String, + val founder: String, + @Link(Manuscript::class) val ga: String, + val lection: String, + val nkv: String, + val source: String, + val transcript: String, + @Index(Language.GREEK, default = true) val text: String, + @Unique(true) val verse_id: Long, +) \ No newline at end of file diff --git a/ui/src/main/kotlin/dev/paulee/ui/components/DiffViewer.kt b/ui/src/main/kotlin/dev/paulee/ui/components/DiffViewer.kt index 9356a31..b721daa 100644 --- a/ui/src/main/kotlin/dev/paulee/ui/components/DiffViewer.kt +++ b/ui/src/main/kotlin/dev/paulee/ui/components/DiffViewer.kt @@ -172,7 +172,7 @@ private fun TagView(entries: List>, taggable: Taggable?, mod } } - Column { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { (0 until greatestSize).forEach { index -> val columnName = columns.getOrNull(currentColumnIndex) ?: return@forEach @@ -186,7 +186,7 @@ private fun TagView(entries: List>, taggable: Taggable?, mod MarkedText( text = value, highlights = tags, - textAlign = TextAlign.Center, + textAlign = TextAlign.Left, ) } } @@ -252,7 +252,7 @@ private fun DiffView( } } - Column { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { (0 until greatestSize).forEach { index -> val columnName = columns.getOrNull(currentColumnIndex) ?: return@forEach