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>