kotlinadvanced

DSL for HTML Generation

Build a type-safe HTML DSL with Kotlin: lambda receivers, @DslMarker, and nested builders.

kotlin
@DslMarker
annotation class HtmlDsl

@HtmlDsl
abstract class Element {
    abstract fun render(indent: Int): String
    protected fun indentation(level: Int) = "  ".repeat(level)
}

class TextElement(private val text: String) : Element() {
    override fun render(indent: Int) = "${indentation(indent)}$text\n"
}

@HtmlDsl
abstract class Tag(private val name: String) : Element() {
    val children = mutableListOf<Element>()
    val attributes = mutableMapOf<String, String>()

    // Attribute helpers
    var id: String
        get() = attributes["id"] ?: ""
        set(value) { attributes["id"] = value }

    var className: String
        get() = attributes["class"] ?: ""
        set(value) { attributes["class"] = value }

    fun attr(name: String, value: String) { attributes[name] = value }

    // Text content
    operator fun String.unaryPlus() {
        children.add(TextElement(this))
    }

    override fun render(indent: Int): String = buildString {
        val attrs = if (attributes.isNotEmpty())
            attributes.entries.joinToString(" ", prefix = " ") { "${it.key}=\"${it.value}\"" }
        else ""

        if (children.isEmpty()) {
            append("${indentation(indent)}<$name$attrs/>\n")
        } else {
            append("${indentation(indent)}<$name$attrs>\n")
            children.forEach { append(it.render(indent + 1)) }
            append("${indentation(indent)}</$name>\n")
        }
    }
}

// Concrete tags
class Html : Tag("html") {
    fun head(init: Head.() -> Unit) = Head().also { it.init(); children.add(it) }
    fun body(init: Body.() -> Unit) = Body().also { it.init(); children.add(it) }
}

class Head : Tag("head") {
    fun title(text: String) { children.add(object : Tag("title") { init { +text } }) }
    fun meta(init: Meta.() -> Unit) = Meta().also { it.init(); children.add(it) }
    fun link(href: String, rel: String = "stylesheet") {
        children.add(object : Tag("link") { init { attr("href", href); attr("rel", rel) } })
    }
}

class Meta : Tag("meta")
class Body : Tag("body") {
    fun h1(init: H1.() -> Unit) = H1().also { it.init(); children.add(it) }
    fun p(init: P.() -> Unit) = P().also { it.init(); children.add(it) }
    fun div(init: Div.() -> Unit) = Div().also { it.init(); children.add(it) }
    fun ul(init: Ul.() -> Unit) = Ul().also { it.init(); children.add(it) }
    fun a(href: String, init: A.() -> Unit) = A().also { it.attr("href", href); it.init(); children.add(it) }
}

class H1 : Tag("h1")
class P : Tag("p")
class A : Tag("a")
class Div : Tag("div") {
    fun p(init: P.() -> Unit) = P().also { it.init(); children.add(it) }
    fun span(init: Tag.() -> Unit) = object : Tag("span") {}.also { it.init(); children.add(it) }
}
class Ul : Tag("ul") {
    fun li(init: Tag.() -> Unit) = object : Tag("li") {}.also { it.init(); children.add(it) }
}

// Entry point
fun html(init: Html.() -> Unit): Html = Html().apply(init)

fun main() {
    val page = html {
        head {
            title("Kotlin DSL Demo")
            meta { attr("charset", "utf-8") }
            link("/styles.css")
        }
        body {
            h1 {
                className = "title"
                +"Welcome to Kotlin DSL"
            }
            div {
                id = "content"
                className = "container"
                p { +"This is a type-safe HTML builder." }
                p { +"No string concatenation needed!" }
            }
            ul {
                className = "features"
                li { +"Type-safe" }
                li { +"Readable" }
                li { +"Extensible" }
            }
            a("https://kotlinlang.org") {
                +"Learn Kotlin"
            }
        }
    }
    println(page.render(0))
}

Use Cases

  • Type-safe template generation
  • Domain-specific configuration APIs
  • Declarative UI construction patterns

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.