Skip to Content
Course content

137: Practice Exercise: Building a Custom Kotlin DSL for HTML

Click on the "Edit" button in the top corner of the screen to edit your slide content.

So, we're diving into one of the coolest parts of Kotlin: building a Domain Specific Language (DSL). You've seen this in libraries like Ktor or Compose, but building your own HTML builder is the best way to actually wrap your head around how the "magic" works. It's basically just a clever combination of extension functions and function literals with receivers.

How do I actually get that nested "tree" structure to work?

The secret is the T.() -> Unit syntax. When you define a function that takes a lambda with a receiver, you're telling Kotlin: "Inside this block, the keyword this refers to an instance of T."

To build an HTML tree, I usually start with a base class for all tags. Here is a simplified version of how I'd set up a basic Tag class and a body builder:

class Tag(val name: String) {
    private val children = mutableListOf<Tag>()
    var text: String = ""

    fun addTag(tag: Tag) {
        children.add(tag)
    }

    override fun toString(): String {
        val inner = if (text.isNotEmpty()) text else children.joinToString("") { it.toString() }
        return "<${name}>$inner</${name}>"
    }
}

class Body : Tag("body") {
    fun p(init: P.() -> Unit) {
        val p = P().apply(init)
        addTag(p)
    }
}

class P : Tag("p") {
    fun text(value: String) {
        this.text = value
    }
}

fun html(init: Body.() -> Unit): String {
    val body = Body().apply(init)
    return "<html>${body}</html>"
}

Now, when you call html { p { text("Hello") } }, you aren't just calling functions; you're configuring an object hierarchy. It feels like a configuration file, but it's actually fully typed code.

What's the best way to handle attributes like "class" or "id"?

You don't want your function signatures to become a nightmare of optional parameters. I've found that the cleanest way is to either use a Map for attributes or create a small helper class. For a simple DSL, adding a property to the Tag class is usually enough to get the job done without over-engineering it.

class Tag(val name: String) {
    var attributes = mutableMapOf<String, String>()
    // ... previous children logic ...

    override fun toString(): String {
        val attrString = attributes.entries.joinToString(" ") { "${it.key}=\"${it.value}\"" }
        val openTag = if (attrString.isEmpty()) "<${name}>" else "<${name} $attrString>"
        // ... rest of toString ...
    }
}

// Now we can add a helper function to the Tag class
fun Tag.attr(name: String, value: String) {
    this.attributes[name] = value
}

This lets you write p { attr("class", "main-text"); text("Hi") }. It keeps the DSL flexible because you can add any attribute without changing the function definition of every single tag.

Wait, can't I accidentally put a body tag inside a paragraph tag?

Yes, and that's a huge problem. Because of how receivers work, any function available to the parent is also available to the child if you aren't careful. This is where @DslMarker comes in. It's a specialized annotation that tells the compiler: "Don't let me call a function from an outer receiver if I'm already inside an inner receiver of the same marker."

Here is how you'd implement it. First, define your marker:

@DslMarker
annotation class HtmlTagMarker

Then, apply it to all your builder classes:

@HtmlTagMarker
class Body : Tag("body") { ... }

@HtmlTagMarker
class P : Tag("p") { ... }

Now, if you try to call body { p { body { ... } } }, the compiler will throw an error. I highly recommend always using a marker for your DSLs; otherwise, your API becomes a playground for illogical nesting that's a pain to debug later.




📋 Practical Task

Exercise: Building a "Blog Post" Layout DSL

Your task is to extend the HTML DSL concepts from this lesson to create a specialized "Blog Post" builder. You need to implement a DSL that allows a developer to define a blog page with the following constraints:

  • The top-level function should be blogPost { ... }.
  • Inside blogPost, you must be able to define exactly one title tag and one content block.
  • The content block should allow multiple paragraph tags and at least one image tag.
  • The image tag must require a src attribute to be passed as a parameter to its function (e.g., image(src = "photo.jpg")).
  • Use a @DslMarker to ensure that title cannot be called inside the content block.

Goal: Write the classes and functions necessary so that the following code compiles and produces a string representation of the HTML:

val myPage = blogPost {
    title("My Kotlin Journey")
    content {
        paragraph("Learning DSLs is challenging but rewarding.")
        image(src = "kotlin_logo.png")
        paragraph("I love how type-safe this is!")
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.