k3lp documentation
Open-source implementation to parse and work with Unicode Keyboard3 (UTS #35 part 7) files in Kotlin Multiplatform.
Multiple modules are published, it is sufficient however to include the core module, as this also transitively includes lib.meta and lib.text for you.
To use k3lp in your project, add this to your Gradle files:
// settings.gradle.kts
repositories {
mavenCentral()
}
// build.gradle.kts
dependencies {
implementation("org.k3lp:k3lp-core:$k3lpVersion")
}
High-level structure
K3lp makes a distinction between the model and runtime components. The model is simply a parsed representation of the keyboard3 XML file, with all variables, imports, and escaping already resolved. To actually process input, the runtime input method and editor classes are used.
Model
The following hello world example demonstrates how to compile a keyboard3 file to a model:
data class InMemoryFileRef(val uniqueName: String) : SourceFileRef {
override fun toString(): String {
return uniqueName
}
}
const val LAYOUT_XML = """
<!-- any in-memory keyboard3.xml file -->
"""
@OptIn(WillRequireMigrationToRichErrors::class)
suspend fun helloWorld() {
val file = TextSourceFile(InMemoryFileRef("LAYOUT_XML"), LAYOUT_XML)
when (val result = file.compileToModel()) {
is K3CompileResult.Success -> {
result.model // the compiled model
result.reports // all warnings, if any
}
is K3CompileResult.Failure -> {
result.error // the final reason why compilation failed
result.reports // all errors and warnings
}
}
}
This simple mode does not support imports, as no import resolver is configured. To allow imports, simply create and provide an import resolver:
const val IMPORT_EXAMPLE_XML = """
<!-- any in-memory importable file -->
"""
@OptIn(WillRequireMigrationToRichErrors::class)
suspend fun exampleWithImports() {
val file = TextSourceFile(InMemoryFileRef("LAYOUT_XML"), LAYOUT_XML)
val importResolver = K3ImportResolver { path, _ ->
if (path == "import_example.xml") {
TextSourceFile(InMemoryFileRef(IMPORT_EXAMPLE_XML), IMPORT_EXAMPLE_XML)
} else {
error("no such file $path")
}
}
when (val result = file.compileToModel(importResolver)) {
is K3CompileResult.Success -> {} // ...
is K3CompileResult.Failure -> {} // ...
}
}
Above examples have in common that no file IO is involved at all. This design decision is intentional - k3lp only operates on in-memory file strings. It does not care if a file actually came from the disk or from a network drive. This allows k3lp to be very flexible and usable cross-platform. If file IO is needed, you can use your platform's file IO, or any other library like kotlinx.io.
Runtime
To use a compiled model, an input method and editor must be created. Below is a minimal implementation that simply tracks the application "editor text" in a var string.
class ExampleEditor : K3Editor {
var outputText = ""
override fun replaceText(
range: IntRange,
text: String,
newSelection: K3TextRange,
newComposition: K3TextRange?,
) {
outputText = outputText.replaceRange(range, text)
}
override fun setComposition(newComposition: K3TextRange?) {
// not supported in this small example editor
}
}
class ExampleInputMethodState(
model: K3Model,
editor: ExampleEditor,
content: K3Content = K3Content.Empty,
touchLayerId: K3LayerId = K3LayerId.BASE,
) : K3InputMethodState<ExampleInputMethodState, ExampleEditor>(
model, editor, content, touchLayerId,
) {
override fun copy(
model: K3Model,
editor: ExampleEditor,
content: K3Content,
touchLayerId: K3LayerId,
) = ExampleInputMethodState(model, editor, content, touchLayerId)
}
class ExampleInputMethod(
model: K3Model,
editor: ExampleEditor,
) : K3InputMethod<ExampleInputMethodState, ExampleEditor, ExampleInputMethod.UpdateStateScope>(
initialState = ExampleInputMethodState(model, editor),
) {
// External reset of the input context
suspend fun reset(value: String) {
updateState {
state.editor.outputText = value
resetContent(
newSelection = K3TextRange(value.length),
newSurrounding = K3SurroundingText(textBefore = value),
)
}
}
override fun updateStateScopeOf(state: ExampleInputMethodState): UpdateStateScope {
return UpdateStateScope(state)
}
class UpdateStateScope(
state: ExampleInputMethodState,
) : K3InputMethod.UpdateStateScope<ExampleInputMethodState, ExampleEditor>(state) {
//
}
}
This can be used like this:
suspend fun exampleRuntime(model: K3Model) {
val editor = K3TestEditor()
val inputMethod = K3TestInputMethod(model, editor)
// emitting a key
inputMethod.updateState {
emitKeyById(...)
}
// emitting text
inputMethod.updateState {
emit("example".asK3String())
}
// emitting backspace
inputMethod.updateState {
emitBackspace()
}
}
See K3TestRunner for a more advanced example.
All modules:
The core module provides the keyboard3 file to model compiler and runtime.
The lib.meta module provides highly abstracted multiplatform definitions for platform-level data structures, which are shared across all modules of k3lp.
The lib.text module provides the foundation for multiplatform marker-aware text processing, regex processing, normalization, and UnicodeSet functionality, all of which are heavily used in the core model and runtime processing.