Careeroza
Building a Type-Safe HTML DSL
Builders
Combine classes with builder functions that take lambdas with receivers, and you get a nested, readable, compile-checked DSL, like kotlinx.html or Jetpack Compose.
Example
abstract class Tag(val name: String) {
val children = mutableListOf<Tag>()
val attrs = linkedMapOf<String, String>()
protected fun <T : Tag> add(tag: T, init: T.() -> Unit): T { tag.init(); children += tag; return tag }
open fun render(indent: String = ""): String = buildString {
val a = attrs.entries.joinToString("") { " ${it.key}=\"${it.value}\"" }
appendLine("$indent<$name$a>")
children.forEach { append(it.render("$indent ")) }
appendLine("$indent</$name>")
}
}
class Text(private val text: String) : Tag("") {
override fun render(indent: String) = "$indent$text\n"
}
class P : Tag("p") { operator fun String.unaryPlus() { children += Text(this) } }
class Li : Tag("li") { operator fun String.unaryPlus() { children += Text(this) } }
class Ul : Tag("ul") { fun li(init: Li.() -> Unit) = add(Li(), init) }
class Body : Tag("body") {
fun p(init: P.() -> Unit) = add(P(), init)
fun ul(cssClass: String, init: Ul.() -> Unit) = add(Ul(), init).also { it.attrs["class"] = cssClass }
}
class Html : Tag("html") { fun body(init: Body.() -> Unit) = add(Body(), init) }
fun html(init: Html.() -> Unit): Html = Html().apply(init)
fun main() {
val page = html {
body {
p { +"Top courses" }
ul("courses") {
for (c in listOf("Kotlin", "Java", "React")) li { +c }
}
}
}
print(page.render())
}Output
<html>
<body>
<p>
Top courses
</p>
<ul class="courses">
<li>
Kotlin
</li>
<li>
Java
</li>
<li>
React
</li>
</ul>
</body>
</html>