From 981978341bec5954157748a838779fcb4e781918 Mon Sep 17 00:00:00 2001 From: Lionel Briand Date: Wed, 31 Jan 2024 11:11:03 +0100 Subject: [PATCH] Update readme --- README.MD | 345 ++++++++++++++++++ README.md | 239 ------------ .../net/orandja/ktm/ExtensionCharSequence.kt | 8 + .../kotlin/net/orandja/ktm/ExtensionEnum.kt | 2 +- .../net/orandja/ktm/ExtensionMDocument.kt | 3 + .../net/orandja/ktm/adapters/KtmAdapter.kt | 26 -- .../net/orandja/ktm/adapters/KtmAdapters.kt | 6 +- .../kotlin/net/orandja/ktm/base/MContext.kt | 1 + .../ktm/composition/builder/ContextFactory.kt | 14 +- .../composition/builder/ContextListBuilder.kt | 7 +- .../composition/builder/ContextMapBuilder.kt | 10 +- .../ktm/composition/builder/context/Multi.kt | 3 +- .../net/orandja/ktm/test/AdapterExtensions.kt | 20 +- .../net/orandja/ktm/test/DelegatedContext.kt | 2 +- .../ktm/test/EnumTransformationTest.kt | 14 +- .../net/orandja/ktm/test/spec/JsonContext.kt | 2 +- .../ktm/test/spec/SpecificationTest.kt | 4 +- .../kotlin/net/orandja/ktm/ksp/sample/User.kt | 10 + .../ktm/ksp/sample/AutoKtmAdaptersTest.kt | 8 +- .../orandja/ktm/ksp/sample/NormalCaseTest.kt | 4 +- .../net/orandja/ktm/ksp/sample/ReadmeTest.kt | 51 +++ .../ktm/ksp/generation/AdapterGenerator.kt | 9 +- .../ktm/ksp/generation/AdapterToken.kt | 68 ++-- .../orandja/ktm/ksp/generation/AutoToken.kt | 19 +- 24 files changed, 526 insertions(+), 349 deletions(-) create mode 100644 README.MD delete mode 100644 README.md create mode 100644 ksp-sample/src/commonMain/kotlin/net/orandja/ktm/ksp/sample/User.kt create mode 100644 ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/ReadmeTest.kt diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..429a6c7 --- /dev/null +++ b/README.MD @@ -0,0 +1,345 @@ +# KTM + +A [Mustache](https://mustache.github.io) implementation in pure Kotlin Multiplatform. + +# Import from maven + +### Library + +
+Multiplatform: + +```kotlin +repositories { + mavenCentral() +} +val commonMain by getting { + dependencies { + implementation("net.orandja.ktm:core:1.0.0") + } +} +``` + +
+ +Jvm: + +```kotlin +repositories { + mavenCentral() +} +dependencies { + implementation("net.orandja.ktm:core:1.0.0") +} +``` + +### Ksp code generator plugin + +Enable ksp plugin: + +```kotlin +plugins { + // ... + id("com.google.devtools.ksp") version "1.9.22-1.0.7" // or later +} +``` + +
+Multiplatform: + +```kotlin +repositories { + mavenCentral() +} +dependencies { + add("kspJvm", "net.orandja.ktm:ksp:0.0.1") + // add("kspJs", "net.orandja.ktm:ksp:0.0.1") + // add("kspNative", "net.orandja.ktm:ksp:0.0.1") + // ... +} +``` + +
+ +Jvm : + +```kotlin +dependencies { + // ... + ksp("net.orandja.ktm:ksp:0.0.1") +} + +``` + +> [!NOTE] +> If you provide ksp arguments. +> +> ```kotlin +> 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 +> ```kotlin +> Ktm.setDefaultAdapters(AutoKtmAdaptersModule) +> ``` + +# Examples + +Render a document with automatically generated adapter + +```kotlin +@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 + +```kotlin +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. + +```kotlin +@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: + +```kotlin +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: + +```mustache +{{# 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`. + +```kotlin +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. + +```kotlin +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 + +```kotlin +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. + +```kotlin +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`. + +```kotlin +@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`. + +```kotlin +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. + +```kotlin +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. + +```kotlin +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. + +```kotlin +@KtmContext +data class User(val user: String) +Ktm.setDefaultAdapters { +UserKtmAdapter } + +val customAdapters = Ktm.adapters.make { + +KtmAdapter { 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: + +```kotlin +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: + +```kotlin +Ktm.setDefaultAdapters(AutoKtmAdaptersModule) +``` + +Extends the class `KtmAdapterModule` to create custom modules. You can later create +sets of adapters with them. + +# Deep dive + +- [Create documents](docs/mdocs/create_documents.md) diff --git a/README.md b/README.md deleted file mode 100644 index 80410a6..0000000 --- a/README.md +++ /dev/null @@ -1,239 +0,0 @@ -# KTM - -A [Mustache](https://mustache.github.io) implementation in pure Kotlin Multiplatform. - -```kotlin -val document = "Hello {{ name }}!".toMustacheDocument() -val context = Ktm.ctx.make { name by "Jon" } -val render = document.render(context) -assert("Hello Jon!" == render) -``` - -# Import from maven - -### Library - -
-Multiplatform: - -```kotlin -repositories { - mavenCentral() -} -val commonMain by getting { - dependencies { - implementation("net.orandja.ktm:core:1.0.0") - } -} -``` - -
- -Jvm: - -```kotlin -repositories { - mavenCentral() -} -dependencies { - implementation("net.orandja.ktm:core:1.0.0") -} -``` - -### Ksp code generator plugin - -Enable ksp plugin: - -```kotlin -plugins { - // ... - id("com.google.devtools.ksp") version "1.9.22-1.0.7" // or later -} -``` - -
-Multiplatform: - -```kotlin -repositories { - mavenCentral() -} -dependencies { - add("kspJvm", "net.orandja.ktm:ksp:0.0.1") - // add("kspJs", "net.orandja.ktm:ksp:0.0.1") - // add("kspNative", "net.orandja.ktm:ksp:0.0.1") - // ... -} -``` - -
- -Jvm : - -```kotlin -dependencies { - // ... - ksp("net.orandja.ktm:ksp:0.0.1") -} - -``` - -# QuickStart - -With Mustache, to render a document you need three 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 a usable object. If you are -running kotlin JVM, you can use extension functions to parse files and IO -with `.file(File)`, `.path(Path)`, `.resource(String)`, `.inputStream(InputStream)` -or `.reader(Reader)`. - -```kotlin -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. The returned context will be used to -replace your document tags. With it, you can create contexts anywhere in your code -and call your mapper to create more complex context. - -Since classes can't be used to create contexts manually, yet, you have to be -declarative. Even if it's a bit verbose, it gives you full control. - -Let's assume this document: - -```handlebars -{{# greeting }} - Hello {{ name }}, -{{/ greeting }} - -Today tasks: -{{# tasks }} - - {{ . }} -{{/ tasks }} -``` - -You can create a fully declarative context. - -Every method in `Ktm.ctx` can be used in the `make scope`. - -```kotlin -val context = Ktm.ctx.make { // make scope - "greeting" by make { // make scope - "name" by "Jon" - } - "tasks" by makeList { // make scope - +"Sleep" - +"Eat" - } -} -``` - -If you already have some map or list (**Only String**) to fill the context, you can -use them: - -```kotlin -val user = mapOf("name" to "Jon") -val tasks = listOf("Sleep", "Eat") - -val context = Ktm.ctx.make { - "greeting" by user - "tasks" by tasks -} -``` - -Maybe you don't have the full scope of your context, and it needs to be dynamic. - -Here we have a function that defines the tasks without knowing `mister`. We then add -it to the root context. When rendering, it will search for `mister` tag to create the -entry. - -```kotlin -fun tasks() = Ktm.ctx.makeList { - +"Call for lunch." - +stringDelegate { - "Welcome ${getValue("mister")} to the office." - } -} - -val context: MContext = Ktm.ctx.make { - "name" by "Jon" - "greeting" by true - "mister" by "M. Smith" - "tasks" by tasks() -} -``` - -### 3. Partials - -You can create a pool of document that will be used as partials of a mustache -template with `Ktm.pool`. - -Here is a quick example: - -```kotlin -val base: MDocument = Ktm.doc.string("{{> header}}\n{{> body }}") - -val pool = Ktm.pool.make { - "base" by base - "header" by string("Hello {{ name }},\n") - "body" by "You need to tell {{ other }} of your accomplishment !" -} - -val context = Ktm.ctx.make { - "name" by "Jon" - "other" by "Lola" -} - -val render = base.render(context, pool) -val render = pool.render("base", context) -``` - -Both `render` are equivalent: - -``` -Hello Jon, -You need to tell Lola of your accomplishment ! -``` - -As you can see, the `Ktm.pool.make` is like the `Ktm.ctx.make` scope. You can compose -your partials with the `by` keyword. Additionally, you can use all the methods from -the `Ktm.doc` to parse your documents in the `Ktm.pool.make` scope. - -To render a document you can choose to render from an external document -with `document.render(context, partials)` or from the pool directly -with `pool.render("document_key", context)`. - - -> [!NOTE] -> Documents aren't cached. If you create two pools that read the same document. -> Like : -> -> ```kotlin -> val p1 = Ktm.pool.make { "a" by resource("/path") } -> val p2 = Ktm.pool.make { "a" by resource("/path") } -> ``` -> -> The file will be in memory twice. To avoid it, create the document before: -> -> ```kotlin -> val a = Ktm.doc.resource("/path") -> val p1 = Ktm.pool.make { "a" by a } -> val p2 = Ktm.pool.make { "a" by a } -> ``` - -# Deep dive - -- [Create documents](docs/mdocs/create_documents.md) -- **TODO** [Create contexts](docs/mdocs/create_contexts.md) -- **TODO** [Create partials](docs/mdocs/create_partials.md) -- **TODO** [Render to a stream](docs/mdocs/render_to_stream.md) diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionCharSequence.kt b/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionCharSequence.kt index 091f101..62d25a5 100644 --- a/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionCharSequence.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionCharSequence.kt @@ -14,6 +14,14 @@ import net.orandja.ktm.composition.builder.ContextMapBuilder */ fun CharSequence.toMustacheDocument(): MDocument = Ktm.doc.string(this) + +/** + * Renders the given `CharSequence` like a [MDocument] with the provided [context] + * It will find the correct context in the default Ktm adapters + */ +inline fun CharSequence.render(context: T, adapters: KtmAdapter.Provider = Ktm.adapters) = + toMustacheDocument().render(adapters.contextOf(context)) + /** * Renders the given `CharSequence` like a [MDocument] with the provided [MContext] * diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionEnum.kt b/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionEnum.kt index c0db94a..201172d 100644 --- a/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionEnum.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionEnum.kt @@ -34,7 +34,7 @@ inline fun > T.EnumMustacheContext( override fun get(node: NodeContext, tag: String): MContext? = when (tag) { name -> MContext.Yes in values.map { it.name } -> MContext.No - "ordinal" -> Ktm.ctx.string(ordinal.toString()) + "ordinal" -> Ktm.ctx.value(ordinal.toString()) "name" -> ContextValue(name) "values" -> valuesContext else -> null diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionMDocument.kt b/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionMDocument.kt index f2b3626..ad23df6 100644 --- a/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionMDocument.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/ExtensionMDocument.kt @@ -6,6 +6,9 @@ import net.orandja.ktm.base.MDocument import net.orandja.ktm.composition.builder.ContextMapBuilder +inline fun MDocument.render(context: T) = render(Ktm.adapters.contextOf(context)) + + /** * Renders the given MDocument using the provided MContext and MPool. * diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/adapters/KtmAdapter.kt b/core/src/commonMain/kotlin/net/orandja/ktm/adapters/KtmAdapter.kt index c515798..d839ae5 100644 --- a/core/src/commonMain/kotlin/net/orandja/ktm/adapters/KtmAdapter.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/adapters/KtmAdapter.kt @@ -2,7 +2,6 @@ package net.orandja.ktm.adapters import net.orandja.ktm.Ktm import net.orandja.ktm.base.MContext -import net.orandja.ktm.composition.builder.ContextMapBuilder import kotlin.reflect.KType /** @@ -28,28 +27,3 @@ fun interface KtmAdapter { fun get(kType: KType): KtmAdapter<*>? } } - -/** - * Extension of [KtmAdapter] where the superclass only need to configure the content of the context. - * - * ```kotlin - * data class User(val name: String) - * - * val userAdapter = KtmMapAdapter { value -> - * // 'this' is the same as Ktm.ctx.make { ... } - * "name" by value.name - * } - * - * val context = adapter.toMustacheContext(User("John")) - * "Hello {{ name }}".render(context) // Hello John - * ``` - * - * @see ContextMapBuilder - * @see KtmAdapter - */ -fun interface KtmMapAdapter : KtmAdapter { - override fun toMustacheContext(adapters: KtmAdapter.Provider, value: T): MContext = - Ktm.ctx.make(adapters) { configure(value) } - - fun ContextMapBuilder.configure(value: T) -} diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/adapters/KtmAdapters.kt b/core/src/commonMain/kotlin/net/orandja/ktm/adapters/KtmAdapters.kt index 0da5583..7ba8ee5 100644 --- a/core/src/commonMain/kotlin/net/orandja/ktm/adapters/KtmAdapters.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/adapters/KtmAdapters.kt @@ -21,18 +21,18 @@ class DelegatedKtmAdapter( object AnyKtmAdapter : KtmAdapter { override fun toMustacheContext(adapters: KtmAdapter.Provider, value: Any?): MContext = - Ktm.ctx.string(value?.toString()) + Ktm.ctx.value(value?.toString()) override fun toString(): String = "KtmAdapter(Any)" } internal object StringKtmAdapter : KtmAdapter { - override fun toMustacheContext(adapters: KtmAdapter.Provider, value: String?): MContext = Ktm.ctx.string(value) + override fun toMustacheContext(adapters: KtmAdapter.Provider, value: String?): MContext = Ktm.ctx.value(value) override fun toString(): String = "KtmAdapter(String)" } internal object BooleanKtmAdapter : KtmAdapter { - override fun toMustacheContext(adapters: KtmAdapter.Provider, value: Boolean?): MContext = Ktm.ctx.bool(value) + override fun toMustacheContext(adapters: KtmAdapter.Provider, value: Boolean?): MContext = Ktm.ctx.value(value) override fun toString(): String = "KtmAdapter(Boolean)" } diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/base/MContext.kt b/core/src/commonMain/kotlin/net/orandja/ktm/base/MContext.kt index 83ddbf3..0759493 100755 --- a/core/src/commonMain/kotlin/net/orandja/ktm/base/MContext.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/base/MContext.kt @@ -50,6 +50,7 @@ sealed interface MContext { */ fun interface Map : MContext { fun get(node: NodeContext, tag: String): MContext? + fun get(tag: String): MContext? = get(NodeContext(this), tag) override fun accept(context: In, visitor: Visitor) = visitor.map(context, this) } diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextFactory.kt b/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextFactory.kt index bc4a9ac..ba202fe 100644 --- a/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextFactory.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextFactory.kt @@ -26,30 +26,30 @@ open class ContextFactory { val no = MContext.No val yes = MContext.Yes - inline fun string(value: CharSequence?) = if (value == null) no else ctxValue(value) - inline fun string(value: CharSequence) = ctxValue(value) + inline fun value(value: CharSequence?) = if (value == null) no else ctxValue(value) + inline fun value(value: CharSequence) = ctxValue(value) - inline fun bool(value: Boolean?) = if (value == null) no else bool(value) - inline fun bool(value: Boolean) = if (value) yes else no + inline fun value(value: Boolean?) = if (value == null) no else value(value) + inline fun value(value: Boolean) = if (value) yes else no inline fun document(value: CharSequence?) = if (value == null) no else document(value) inline fun document(value: CharSequence) = ctxDocument(Ktm.doc.string(value)) fun list(vararg items: String) = list(items.toList()) fun list(items: Iterable?) = if (items == null) no else { - val contexts = items.map(::string) + val contexts = items.map(::value) if (contexts.isEmpty()) yes else ctxList(contexts) } fun map(vararg group: Pair) = map(group.toMap()) fun map(group: Map?) = if (group == null) no else { - val contexts = group.mapValues { string(it.value) } + val contexts = group.mapValues { value(it.value) } if (contexts.isEmpty()) yes else ctxMap(contexts) } fun merge(vararg contexts: MContext) = merge(contexts.toList()) fun merge(contexts: List): MContext.Map { - val mapContexts = contexts.filter { it is MContext.Map } + val mapContexts = contexts.filter { it is MContext.Map }.asReversed() @Suppress("UNCHECKED_CAST") return MultiMapContext(mapContexts as List) } diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextListBuilder.kt b/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextListBuilder.kt index 7cbb3c2..f9e7058 100644 --- a/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextListBuilder.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextListBuilder.kt @@ -2,6 +2,7 @@ package net.orandja.ktm.composition.builder import net.orandja.ktm.adapters.KtmAdapter import net.orandja.ktm.base.MContext +import net.orandja.ktm.base.MDocument import net.orandja.ktm.contextOf class ContextListBuilder( @@ -11,13 +12,17 @@ class ContextListBuilder( fun build(): MContext = if (backing.isEmpty()) yes else ctxList(backing) operator fun CharSequence?.unaryPlus() { - backing += string(this) + backing += value(this) } operator fun Boolean.unaryPlus() { backing += if (this) yes else no } + operator fun MDocument.unaryPlus() { + backing += ctxDocument(this) + } + operator fun MContext?.unaryPlus() { backing += this ?: no } diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextMapBuilder.kt b/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextMapBuilder.kt index ab1716a..0d42306 100755 --- a/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextMapBuilder.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/ContextMapBuilder.kt @@ -1,7 +1,6 @@ package net.orandja.ktm.composition.builder import net.orandja.ktm.adapters.KtmAdapter -import net.orandja.ktm.adapters.KtmMapAdapter import net.orandja.ktm.base.MContext import net.orandja.ktm.base.MDocument import net.orandja.ktm.composition.builder.context.ContextMap @@ -36,7 +35,7 @@ class ContextMapBuilder( infix fun String.by(value: CharSequence?) { - getUpdatableContext().value[this] = string(value) + getUpdatableContext().value[this] = value(value) } infix fun String.by(value: Boolean) { @@ -71,13 +70,12 @@ class ContextMapBuilder( key by value } - fun addBackingContext(context: MContext) { + fun like(context: MContext) { context.accept(Unit, backingContextAdder) } - inline fun configureLike(value: T, adapter: KtmAdapter = getOrThrow()) { - if (adapter is KtmMapAdapter) with(adapter) { configure(value) } - else addBackingContext(adapter.toMustacheContext(this, value)) + inline fun like(value: T, adapter: KtmAdapter = getOrThrow()) { + like(adapter.toMustacheContext(this, value)) } private val backingContextAdder = object : MContext.Visitor.Default(Unit) { diff --git a/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/context/Multi.kt b/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/context/Multi.kt index b3f4d11..8c76456 100644 --- a/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/context/Multi.kt +++ b/core/src/commonMain/kotlin/net/orandja/ktm/composition/builder/context/Multi.kt @@ -13,9 +13,8 @@ import kotlin.jvm.JvmInline @JvmInline value class MultiMapContext(val contexts: List) : MContext.Map { override fun get(node: NodeContext, tag: String): MContext? { - val newNode = NodeContext(this, node) for (ctx in contexts) { - return ctx.get(newNode, tag) ?: continue + return ctx.get(node, tag) ?: continue } return null } diff --git a/core/src/commonTest/kotlin/net/orandja/ktm/test/AdapterExtensions.kt b/core/src/commonTest/kotlin/net/orandja/ktm/test/AdapterExtensions.kt index 325414c..126f9b1 100644 --- a/core/src/commonTest/kotlin/net/orandja/ktm/test/AdapterExtensions.kt +++ b/core/src/commonTest/kotlin/net/orandja/ktm/test/AdapterExtensions.kt @@ -2,8 +2,8 @@ package net.orandja.ktm.test import net.orandja.ktm.* import net.orandja.ktm.adapters.DelegatedKtmAdapter -import net.orandja.ktm.adapters.KtmMapAdapter -import net.orandja.ktm.composition.builder.ContextMapBuilder +import net.orandja.ktm.adapters.KtmAdapter +import net.orandja.ktm.base.MContext import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -14,9 +14,11 @@ class AdapterExtensions { } open class Foo(val value: String) { - object Adapter : KtmMapAdapter { - override fun ContextMapBuilder.configure(value: Foo) { - "value" by value.value + object Adapter : KtmAdapter { + override fun toMustacheContext(adapters: KtmAdapter.Provider, value: Foo): MContext { + return Ktm.ctx.make { + "value" by value.value + } } } } @@ -25,9 +27,11 @@ class AdapterExtensions { class Merged(val value: String) - private val MergedKtmAdapter = KtmMapAdapter { - configureLike(EnumVariants.FOO) - configureLike(ExtendedFoo(it.value)) + private val MergedKtmAdapter = KtmAdapter { adapters, value -> + Ktm.ctx.make(adapters) { + like(EnumVariants.FOO) + like(ExtendedFoo(value.value)) + } } private val adapters = Ktm.adapters.make { diff --git a/core/src/commonTest/kotlin/net/orandja/ktm/test/DelegatedContext.kt b/core/src/commonTest/kotlin/net/orandja/ktm/test/DelegatedContext.kt index f5a985b..7d6ba42 100644 --- a/core/src/commonTest/kotlin/net/orandja/ktm/test/DelegatedContext.kt +++ b/core/src/commonTest/kotlin/net/orandja/ktm/test/DelegatedContext.kt @@ -9,7 +9,7 @@ class DelegatedContext { val context = Ktm.ctx.make { "secret" by "secret" "delegate" by delegate { - string(findValue("secret")) + value(findValue("secret")) } "value" by delegateValue { findValue("secret") ?: "not found" } "map" by delegateMap { diff --git a/core/src/commonTest/kotlin/net/orandja/ktm/test/EnumTransformationTest.kt b/core/src/commonTest/kotlin/net/orandja/ktm/test/EnumTransformationTest.kt index 7b53d00..b4b377d 100644 --- a/core/src/commonTest/kotlin/net/orandja/ktm/test/EnumTransformationTest.kt +++ b/core/src/commonTest/kotlin/net/orandja/ktm/test/EnumTransformationTest.kt @@ -1,7 +1,7 @@ package net.orandja.ktm.test import net.orandja.ktm.* -import net.orandja.ktm.adapters.KtmMapAdapter +import net.orandja.ktm.adapters.KtmAdapter import kotlin.test.Test import kotlin.test.assertEquals @@ -21,11 +21,13 @@ class EnumTransformationTest { assertEquals("", "{{# values }}<{{.}}>{{/ values }}".render(context)) } - val CustomAdapter = KtmMapAdapter { value -> - when (value) { - EnumVariants.FOO -> "FOO" by "A" - EnumVariants.BAR -> "BAR" by "B" - EnumVariants.BAZ -> "BAZ" by "C" + val CustomAdapter = KtmAdapter { adapters, value -> + Ktm.ctx.make { + when (value) { + EnumVariants.FOO -> "FOO" by "A" + EnumVariants.BAR -> "BAR" by "B" + EnumVariants.BAZ -> "BAZ" by "C" + } } } diff --git a/core/src/commonTest/kotlin/net/orandja/ktm/test/spec/JsonContext.kt b/core/src/commonTest/kotlin/net/orandja/ktm/test/spec/JsonContext.kt index 7f7655a..d380ae2 100755 --- a/core/src/commonTest/kotlin/net/orandja/ktm/test/spec/JsonContext.kt +++ b/core/src/commonTest/kotlin/net/orandja/ktm/test/spec/JsonContext.kt @@ -35,5 +35,5 @@ fun toPrimitive(json: JsonPrimitive): MContext { false -> return MContext.No null -> Unit } - return Ktm.ctx.string(json.content) + return Ktm.ctx.value(json.content) } diff --git a/core/src/commonTest/kotlin/net/orandja/ktm/test/spec/SpecificationTest.kt b/core/src/commonTest/kotlin/net/orandja/ktm/test/spec/SpecificationTest.kt index 4e54611..3946ea3 100755 --- a/core/src/commonTest/kotlin/net/orandja/ktm/test/spec/SpecificationTest.kt +++ b/core/src/commonTest/kotlin/net/orandja/ktm/test/spec/SpecificationTest.kt @@ -49,7 +49,9 @@ class SpecificationTest { try { val dataContext = jsonToContext(test.data) val partialsContext = MContext.Map { _, tag -> - Ktm.ctx.document(test.partials?.get(tag)) + test.partials ?: return@Map null + val content = test.partials[tag] ?: return@Map null + Ktm.ctx.document(content) } val context = diff --git a/ksp-sample/src/commonMain/kotlin/net/orandja/ktm/ksp/sample/User.kt b/ksp-sample/src/commonMain/kotlin/net/orandja/ktm/ksp/sample/User.kt new file mode 100644 index 0000000..fcfc759 --- /dev/null +++ b/ksp-sample/src/commonMain/kotlin/net/orandja/ktm/ksp/sample/User.kt @@ -0,0 +1,10 @@ +package net.orandja.ktm.ksp.sample + +import net.orandja.ktm.ksp.KtmContext +import net.orandja.ktm.ksp.KtmName + +@KtmContext +data class User(val firstName: String, val lastName: String) { + @KtmName("name") + fun fullName() = "$firstName $lastName" +} \ No newline at end of file diff --git a/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/AutoKtmAdaptersTest.kt b/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/AutoKtmAdaptersTest.kt index 9ab839e..4e23bf9 100644 --- a/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/AutoKtmAdaptersTest.kt +++ b/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/AutoKtmAdaptersTest.kt @@ -1,7 +1,7 @@ package net.orandja.ktm.ksp.sample import net.orandja.ktm.Ktm -import net.orandja.ktm.adapters.KtmMapAdapter +import net.orandja.ktm.adapters.KtmAdapter import net.orandja.ktm.base.MContext import net.orandja.ktm.makeKtmAdapterModule import net.orandja.ktm.render @@ -12,8 +12,10 @@ import kotlin.test.assertEquals class AutoKtmAdaptersTest { class Foo(val foo: String) - private val FooAdapter = KtmMapAdapter { - "foo" by it.foo + private val FooAdapter = KtmAdapter { adapters, value -> + Ktm.ctx.make(adapters) { + "foo" by value.foo + } } val customModule = makeKtmAdapterModule { diff --git a/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/NormalCaseTest.kt b/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/NormalCaseTest.kt index 141c4e7..ee9a932 100644 --- a/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/NormalCaseTest.kt +++ b/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/NormalCaseTest.kt @@ -1,8 +1,6 @@ package net.orandja.ktm.ksp.sample import net.orandja.ktm.Ktm -import net.orandja.ktm.base.MContext -import net.orandja.ktm.base.NodeContext import net.orandja.ktm.contextOf import net.orandja.ktm.render import kotlin.test.Test @@ -34,7 +32,7 @@ class NormalCaseTest { val adapters = AutoKtmAdaptersModule.createAdapters() val context = adapters.contextOf(data) val richContext = Ktm.ctx.make(adapters) { - configureLike(data) + like(data) "id" by "secret" } assertEquals("", "{{ id }}".render(context)) diff --git a/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/ReadmeTest.kt b/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/ReadmeTest.kt new file mode 100644 index 0000000..ee885c3 --- /dev/null +++ b/ksp-sample/src/jvmTest/kotlin/net/orandja/ktm/ksp/sample/ReadmeTest.kt @@ -0,0 +1,51 @@ +package net.orandja.ktm.ksp.sample + +import net.orandja.ktm.Ktm +import net.orandja.ktm.render +import net.orandja.ktm.toMustacheDocument +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class ReadmeTest { + @Test + fun _1() { + Ktm.setDefaultAdapters { +UserKtmAdapter } + Ktm.setDefaultAdapters(AutoKtmAdaptersModule) + + val document = "Hello {{ name }}".toMustacheDocument() + val data = User("John", "Doe") + + assertEquals("Hello John Doe", document.render(data)) + } + + @Test + fun _2() { + val document = "Hello {{ name }}".toMustacheDocument() + val context = Ktm.ctx.make { + "firstName" by "John" + "lastName" by "Doe" + "name" by delegateValue { "${findValue("firstName")} ${findValue("lastName")}" } + } + assertEquals("Hello John Doe", document.render(context)) + } + + @Test + fun _3() { + Ktm.setDefaultAdapters { +UserKtmAdapter } + val john = User("John", "Doe") + + val documents = Ktm.ctx.make { + "content" by "Hello {{ firstName }}".toMustacheDocument() + "header" by "Header for {{ firstName }}" + } + + val template = "{{> header }}\n\n{{> content }}".toMustacheDocument() + + val context = Ktm.ctx.make { + like(documents) + like(john) + } + + assertEquals("Header for John\nHello John", template.render(context)) + } +} \ No newline at end of file diff --git a/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AdapterGenerator.kt b/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AdapterGenerator.kt index 1b25c5c..2f49e98 100644 --- a/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AdapterGenerator.kt +++ b/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AdapterGenerator.kt @@ -6,7 +6,6 @@ import com.google.devtools.ksp.processing.KSPLogger import net.orandja.ktm.EnumKtmAdapter import net.orandja.ktm.Ktm import net.orandja.ktm.base.MContext -import net.orandja.ktm.base.NodeContext import net.orandja.ktm.ksp.visitor.VisitorResult import net.orandja.ktm.streamRender import net.orandja.ktm.toMustacheDocument @@ -45,8 +44,8 @@ internal object AdapterGenerator { tk.simpleName, tk.sanitizedSimpleName, Ktm.ctx.make(adapters) { - configureLike(tk) - addBackingContext(partials) + like(tk) + like(partials) } ) @@ -55,8 +54,8 @@ internal object AdapterGenerator { tk.simpleName, tk.sanitizedSimpleName, Ktm.ctx.make(adapters) { - addBackingContext(partials) - configureLike(tk) + like(partials) + like(tk) } ) diff --git a/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AdapterToken.kt b/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AdapterToken.kt index aa40b19..62c5dac 100644 --- a/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AdapterToken.kt +++ b/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AdapterToken.kt @@ -1,15 +1,18 @@ package net.orandja.ktm.ksp.generation -import net.orandja.ktm.adapters.KtmMapAdapter -import net.orandja.ktm.composition.builder.ContextMapBuilder +import net.orandja.ktm.Ktm +import net.orandja.ktm.adapters.KtmAdapter +import net.orandja.ktm.base.MContext import net.orandja.ktm.contextOf import org.intellij.lang.annotations.Language sealed class AdapterToken(val kind: Kind) { - companion object : KtmMapAdapter { - override fun ContextMapBuilder.configure(value: AdapterToken) { - "kind" by delegate { contextOf(value.kind) } + companion object : KtmAdapter { + override fun toMustacheContext(adapters: KtmAdapter.Provider, value: AdapterToken): MContext { + return Ktm.ctx.make(adapters) { + "kind" by delegate { contextOf(value.kind) } + } } } @@ -30,11 +33,13 @@ sealed class AdapterToken(val kind: Kind) { val sanitizedSimpleName: String, ) : AdapterToken(Kind.ENUM_CLASS) { companion object { - val Adapter = KtmMapAdapter { - configureLike(it) - "package_name" by it.packageName - "simple_name" by it.simpleName - "sanitized_simple_name" by it.sanitizedSimpleName + val Adapter = KtmAdapter { adapters, value -> + Ktm.ctx.make(adapters) { + like(value) + "package_name" by value.packageName + "simple_name" by value.simpleName + "sanitized_simple_name" by value.sanitizedSimpleName + } } @Language("mustache") @@ -64,15 +69,19 @@ sealed class AdapterToken(val kind: Kind) { val fields: List ) : AdapterToken(Kind.CLASS) { companion object { - val Adapter = KtmMapAdapter { - configureLike(it) - "package_name" by it.packageName - "simple_name" by it.simpleName - "sanitized_simple_name" by it.sanitizedSimpleName - "fields" by contextOf(it.fields) - if (it.classTypeParameterCount == 0) "type_parameter" by no - else "type_parameter" by (0..") { "*" } - } + val Adapter = + KtmAdapter { adapters, value -> + Ktm.ctx.make(adapters) { + like(value) + "package_name" by value.packageName + "simple_name" by value.simpleName + "sanitized_simple_name" by value.sanitizedSimpleName + "fields" by contextOf(value.fields) + if (value.classTypeParameterCount == 0) "type_parameter" by no + else "type_parameter" by (0..") + { "*" } + } + } @Language("mustache") val Template = """ @@ -80,7 +89,6 @@ sealed class AdapterToken(val kind: Kind) { import net.orandja.ktm.Ktm import net.orandja.ktm.adapters.KtmAdapter - import net.orandja.ktm.adapters.KtmMapAdapter import net.orandja.ktm.composition.builder.ContextMapBuilder import net.orandja.ktm.contextOf import net.orandja.ktm.contextOfCallable @@ -115,15 +123,17 @@ sealed class AdapterToken(val kind: Kind) { val isFunction: Boolean, ) : AdapterToken(Kind.CLASS_FIELD) { companion object { - val Adapter = KtmMapAdapter { - configureLike(it) - "name" by it.name - "fieldName" by it.fieldName - "isCallable" by it.isCallable - "isDynamic" by if (it.isCallable) false else it.isDynamic - "isFunction" by it.isFunction - "node" by (it.nodeAsParameter || it.nodeAsReceiver) - "nodeReceiver" by (it.isCallable && it.isFunction && it.nodeAsReceiver) + val Adapter = KtmAdapter { adapters, value -> + Ktm.ctx.make(adapters) { + like(value) + "name" by value.name + "fieldName" by value.fieldName + "isCallable" by value.isCallable + "isDynamic" by if (value.isCallable) false else value.isDynamic + "isFunction" by value.isFunction + "node" by (value.nodeAsParameter || value.nodeAsReceiver) + "nodeReceiver" by (value.isCallable && value.isFunction && value.nodeAsReceiver) + } } @Language("mustache") diff --git a/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AutoToken.kt b/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AutoToken.kt index e54a8e4..992d9ce 100644 --- a/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AutoToken.kt +++ b/ksp/src/main/kotlin/net/orandja/ktm/ksp/generation/AutoToken.kt @@ -1,6 +1,7 @@ package net.orandja.ktm.ksp.generation -import net.orandja.ktm.adapters.KtmMapAdapter +import net.orandja.ktm.Ktm +import net.orandja.ktm.adapters.KtmAdapter import net.orandja.ktm.contextOf import org.intellij.lang.annotations.Language @@ -11,9 +12,11 @@ data class AutoToken( companion object { const val FILE_NAME = "AutoKtmAdaptersModule" - val Adapter = KtmMapAdapter { - "package_name" by it.packageName - "adapters" by contextOf(it.adapters) + val Adapter = KtmAdapter { adapters, value -> + Ktm.ctx.make(adapters) { + "package_name" by value.packageName + "adapters" by contextOf(value.adapters) + } } @Language("mustache") @@ -41,9 +44,11 @@ data class AutoToken( val fileName: String, ) { companion object { - val Adapter = KtmMapAdapter { - "package_name" by it.packageName - "file_name" by it.fileName + val Adapter = KtmAdapter { adapters, value -> + Ktm.ctx.make(adapters) { + "package_name" by value.packageName + "file_name" by value.fileName + } } } }