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.
While using a build tool is not mandatory, this documentation employs Gradle in its examples to align with the project's overall setup.
- Install Java 21.
- Create a new Gradle (Kotlin DSL) project.
- Download the latest API release and add the JAR as a local dependency:
implementation(files("libs/api-0.40.2.jar"))
- Reload the Gradle project.
To create your first plugin, follow these steps:
- Create a Kotlin class, e.g.,
PluginMain
, and implement theIPlugin
interface:
class PluginMain : IPlugin {
override fun init(storageProvider: IStorageProvider) {
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. The StorageProvider instance can be used to access imported
data.
-
Annotate the class with
@PluginMetadata
to declare the plugin's metadata. You can include additional information like the version, author, or a short description. -
Optionally, use the
@PluginOrder
annotation to specify the load order for the plugin. -
Update the Gradle
jar
task to include the plugin's main class in the JAR file manifest:
tasks.jar {
manifest {
attributes["Main-Class"] = "PluginMain" // Adjust the path if necessary
}
}
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:
author_id,full_name,year_born,birth_place
...,...,...,...
...
quotes.csv:
quote,date,author_id
...,...,...,...
...
In Data.kt, define your data classes to match the column names in the CSV files:
@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. Iftrue
and the type isLong
, 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:
@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.
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
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:
@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:
@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."
Pre-filters can be used to select objects matching a condition before searching with the initial query. For this
purpose, a data class needs to be annotated with @PreFilter
and must have a link to another class that stores
additional
information. One can say a pre-filter acts on datasets, resembling a bridge between two data sets.
An example could be a set of entries where information is stored indicating whether a quote contains certain words:
occurrence.csv
quote_id,occurrence,word_id
77,false,12
78,true,12
words.csv
word_id,word,type
12,House,Noun
quotes.csv
quote_id,quote,author
...
77,...,...
78,...,...
...
For this example a reference implementation would look like this:
@PreFilter(key = "quote_id", linkKey = "type", value = "occurrence")
@DataSource("occurrences")
data class Occurrence(val quote_id: Long, val occurrence: Int, @Link(Word::class) val word_id: Int)
@DataSource("words")
data class Word(val word_id: Long, val word: String, val type: String)
@DataSource("quotes")
data class Quote(@Unique(true) val quote_id: Long, @Index(Language.ENGLISH) val quote: String, val author: String)
@PreFilter
has three parameters: key, linkKey, and value. The key field must match the identifier of a class that has a
field marked with @Index
, which will be used to retrieve the actual objects. linkKey determines the column for the first
value in the request and must match a field from the linked class (e.g., Word in this example). The last parameter,
value, specifies the column in the pre-filter dataset.
A query for this example would look like this: @occurrences:Noun:false
. This query would return the quote with the
quote_id 77.
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. The
alwaysShow field can be used to make certain columns always visible
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:
@ViewFilter("Name Highlighter", fields = ["quote"], alwaysShow = ["author_id"], global = true)
override fun tag(field: String, value: String): Map<String, Tag> = 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.
The Drawable interface allows you to extend the user interface of your application. Since UI extension capabilities vary based on the UI framework in use, this interface does not include any pre-defined functions. However, when using the standard UI implementation with Compose, the UI will invoke functions like:
fun composeContent(entries: List<Map<String, String>>): @Composable () -> Unit = {
//Compose components
}
to extend the user interface.
Important: The Drawable interface and the exact structure of the function are required.
To export the plugin, run the Gradle jar
task:
./gradlew jar
For Windows use:
gradlew.bat jar
PluginMain:
@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(storageProvider: IStorageProvider) {
println("Hello world from plugin.")
}
@ViewFilter("Name Highlighter", fields = ["quote"], alwaysShow = ["author_id"], global = true)
override fun tag(field: String, value: String): Map<String, Tag> = mapOf("Tom" to Tag("NAME", Color.blue))
}
Data.kt:
@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
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)
}