Skip to Content
Course content

73: Practice Exercise: Building a Simple DSL for HTML Generation

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

How do I actually get that "nested" look where tags sit inside other tags?

If you've looked at the code for other DSLs, you've probably seen this pattern and wondered where the magic happens. The "secret sauce" is the function literal with receiver. Instead of just passing a lambda like () -> Unit, we pass Element.() -> Unit.

This tells Kotlin: "Inside this block of code, the keyword this refers to an instance of Element." It essentially moves the scope of the lambda inside the class, allowing you to call member functions of that class without any qualifiers. I've found that this is the single biggest "aha!" moment when learning DSLs.

class Element(val name: String) {
    val children = mutableListOf<Element>()
    
    // This is the key: 'init' is a function literal with receiver
    fun element(name: String, init: Element.() -> Unit) {
        val child = Element(name)
        child.init() // Execute the lambda in the context of the child
        children.add(child)
    }
}

// Usage
val root = Element("html")
root.element("body") { 
    // I'm now inside the 'body' Element instance
    element("p") { 
        // I'm now inside the 'p' Element instance
    }
}

How should I handle HTML attributes without making the function calls ugly?

You don't want to pass a massive Map of attributes every time you create a tag. That ruins the "clean" feeling of a DSL. The most elegant way to handle this is by using optional named arguments in your builder functions. It keeps the common cases short but allows for specificity when you need it.

In a real production environment, I might use a more complex attribute builder, but for a simple HTML generator, named arguments are usually plenty. Check out how I'd handle a div with a CSS class here:

class Element(val name: String, val attributes: Map<String, String> = emptyMap()) {
    // ... rest of the class
}

fun Element.div(classes: String? = null, id: String? = null, init: Element.() -> Unit) {
    val attrs = mutableMapOf<String, String>()
    if (classes != null) attrs["class"] = classes
    if (id != null) attrs["id"] = id
    
    // Logic to create the element with these attributes
    element("div", attrs, init) 
}

// Now it looks like this:
div(classes = "container", id = "main-content") {
    // nested content
}

How do I actually turn this object tree into a real HTML string?

Building the tree is the easy part; rendering it is where you have to be careful about indentation and closing tags. I recommend using a StringBuilder and a recursive function. You basically walk down the tree, append the opening tag, visit the children, and then append the closing tag.

Don't forget to handle the indentation. If you just slap the tags together, your generated HTML will be one giant, unreadable line. I usually pass a depth integer through the recursion to handle the leading spaces.

fun Element.render(depth: Int = 0): String {
    val indent = "  ".repeat(depth)
    val attrs = attributes.entries.joinToString(" ") { " ${it.key}=\"${it.value}\"" }
    val sb = StringBuilder()
    
    sb.append("${indent}<${name}${attrs}>\n")
    
    children.forEach { child ->
        sb.append(child.render(depth + 1))
    }
    
    sb.append("${indent}</${name}>\n")
    return sb.toString()
}



📋 Practical Task

Exercise: Build a Developer Portfolio Page Generator

Your task is to expand the HTML DSL concepts we just discussed to create a specialized "Portfolio Generator." Instead of generic element() calls, you need to implement a type-safe DSL that specifically supports the following tags: html, body, h1, div, p, and a (for links).

Requirements:

  • Implement a render() method that produces a properly indented HTML string.
  • The a tag must accept a href attribute as a named argument.
  • The div tag must accept a classes attribute as a named argument.
  • Create a main function that uses your DSL to generate a portfolio page containing:
    • An h1 with your name.
    • A div with the class "projects" containing at least two p tags, each with an a tag linking to a project.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.