Skip to content

L-Briand/KTM

Repository files navigation

KTM

A Mustache implementation in pure Kotlin Multiplatform.

To see how it compares to Mustache.java look here

Import from maven

Library

Multiplatform:
repositories {
    mavenCentral()
}
val commonMain by getting {
    dependencies {
        implementation("net.orandja.ktm:core:2.0.0")
    }
}

Jvm:

repositories {
    mavenCentral()
}
dependencies {
    implementation("net.orandja.ktm:core:2.0.0")
}

Ksp code generator plugin

Enable ksp plugin:

plugins {
    // ...
    id("com.google.devtools.ksp") version "1.9.22-1.0.17" // or later
}
Multiplatform:
repositories {
    mavenCentral()
}
dependencies {
    add("kspJvm", "net.orandja.ktm:ksp:2.0.0")
    // add("kspJs", "net.orandja.ktm:ksp:2.0.0")
    // add("kspNative", "net.orandja.ktm:ksp:2.0.0")
    // ...
}

Jvm :

dependencies {
    // ...
    ksp("net.orandja.ktm:ksp:2.0.0")
}

Note

If you provide ksp arguments.

ksp {
    arg("ktm.auto_adapters_package", "com.example")
}

This will generate the com.example.AutoKtmAdaptersModule with all generated adapters from @KtmContext annotated classes.

Then set it as default with

Ktm.setDefaultAdapters(AutoKtmAdaptersModule)

Examples

Render a document with automatically generated adapter

@KtmContext
data class User(val firstName: String, val lastName: String) {
    @KtmName("name")
    fun fullName() = "$firstName $lastName"
}

Ktm.setDefaultAdapters {
    +UserKtmAdapter
}

// or Ktm.setDefaultAdapters(AutoKtmAdaptersModule) 
// if you have setup a package name for it

val document = "Hello {{ name }}".toMustacheDocument()
val data = User("John", "Doe")

assert("Hello John Doe" == document.render(data))

Render a document with custom build contexts

val document = "Hello {{ name }}".toMustacheDocument()
val context = Ktm.ctx.make {
    "firstName" by "John"
    "lastName" by "Doe"
    "name" by delegateValue { "${findValue("firstName")} ${findValue("lastName")}" }
}
assert("Hello John Doe" == document.render(data))

Custom context can be useful when composing.

@KtmContext
data class User(val name: String)
Ktm.setDefaultAdapters { +UserKtmAdapter }

val john = User("John")

val documents = Ktm.ctx.make {
    "content" by "Hello {{ name }}".toMustacheDocument()
    // If you do not transform it to a mustache document,
    // it will be done on the fly when rendering
    "header" by "Header for {{ name }}"
}

val template = "{{> header }}\n\n{{> content }}".toMustacheDocument()

val context = Ktm.ctx.make {
    like(documents)
    like(john)
}

assert("Header for John\nHello John" == template.render(context))

QuickStart

Mustache API is bundled into the Ktm object. You will use it when you create documents, contexts, or use adapters.

To render a document, you need two things:

1. A template

Something like Hello {{ name }}. This needs to be parsed to be used.

Use the Ktm.doc object to read and parse the documents into usable objects. If you are running kotlin with a JVM, you can use extension functions to parse files and IO with .file(File), .path(Path), .resource(name: String), .inputStream(InputStream) or .reader(Reader).

Example:

val mustacheTemplate: String = "Hello {{ name }}"
var document: MDocument
// default way
document = Ktm.doc.string(mustacheTemplate)
// quick way, only work on strings
document = mustacheTemplate.toMustacheDocument()

2. A context

You generally need some kind of map key: value to match your mustache tags. To create such map, use the Ktm.ctx object. With it, you can create template contexts anywhere in your code.

Let's assume this document:

{{# greeting }}
Hello {{ name }},
{{/ greeting }}
Today's tasks:
{{# tasks }}
    - {{ . }}
{{/ tasks }}

Creating context by hand

You can build a context by hand with methods in Ktm.ctx.

val context = Ktm.ctx.ctxMap(
    "greeting" to Ktm.ctx.ctxMap(
        "name" to Ktm.ctx.string("John")
    )
)

This method is quite cumbersome, to mitigate this, and write things more quickly you can use the make function.

The by keyword can be used to associate key value pairs in the context. Also, every method in Ktm.ctx can be used in the make scope.

val context = Ktm.ctx.make {
    "greeting" by make {
        "name" by "Jon"
    }
    "tasks" by ctxList(value("Eat"), value("Work"))
}

In a more simple way, you can also use kotlin's maps and list to define your data

val context = Ktm.ctx.make {
    "greeting" by mapOf("name" to "Jon")
    "tasks" by listOf("Eat", "Work")
}

Maybe you don't have the full scope of your context, and some parts need to be dynamic.

Here we have a function that defines the tasks without knowing name. We then add it to the main context. During render, it will call the lambda and search for the name tag.

val tasks = Ktm.ctx.makeList {
    +"Call for lunch."
    +delegateValue {
        "Welcome ${findValue("name")} to the office."
    }
}

val context: MContext = Ktm.ctx.make {
    "name" by "John"
    "tasks" by tasks
}

Creating context with ksp

Create a class representing your data and annotate it with @KtmContext.

@KtmContext
data class User(@KtmName("name") val user: String)

The ksp plugin will take these classes and create adapters (UserKtmAdapter, TasksKtmAdapter) in the same package. It will create bindings for any property or function inside it.

Adapters are key components for Ktm. From a given type (here User) it has the ability convert it into MContext.

val context: MContext = TasksKtmAdapter.toMustacheContext(User("John"))
assert("John" == "{{ name }}".render(context))

Generally, you want to set up sets of adapters to transform all your objects into context easily. Then use it when you build contexts.

val adapters = Ktm.adapters.make {
    +UserKtmAdapter
}
val context = Ktm.ctx.make(adapters) {
    "content" by User("John")
}
assert("John", "{{ content.name }}".render(context))

Or set a new default set of adapters altogether for every time you create a context.

Ktm.setDefaultAdapters {
    +UserKtmAdapter
}
// now 'Ktm.adapters' contains adapter for User.
// and by default 'Ktm.ctx.make' use 'Ktm.adapters'
val userContext = Ktm.adapters.contextOf(User("John"))
val contentContext = Ktm.ctx.make {
    "content" by User("John")
}
assert("John" == "{{ name }}".render(userContext))
assert("John" == "{{ content.name }}".render(userContext))

You can always add your own adapter on top of others.

@KtmContext
data class User(val user: String)
Ktm.setDefaultAdapters { +UserKtmAdapter }

val customAdapters = Ktm.adapters.make {
    +KtmAdapter<User> { adapters, value ->
        Ktm.ctx.make {
            "userName" by value.user
        }
    }
}
val context = customAdapters.contextOf(User("John"))
assert("John", "{{userName}}".render())

KtmAdapter Modules

If you set the ksp argument:

ksp {
    arg("ktm.auto_adapters_package", "my.pkg")
}

This will generate the my.pkg.AutoKtmAdaptersModule with all generated adapters from @KtmContext annotated classes. You can then set it as default with:

Ktm.setDefaultAdapters(AutoKtmAdaptersModule)

Extends the class KtmAdapterModule to create custom modules. You can later create sets of adapters with them.

Deep dive