Challenge 3: Extending the HTML DSL — Solution
abstract class Tag(private val name: String) {
private val children = mutableListOf()
private var text: String = ""
fun initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
protected fun setText(value: String) { text = value }
fun render(indent: String = ""): String {
val body = if (text.isNotEmpty()) text else children.joinToString("") { "\n$it" }
return "$indent<$name>$body$name>"
}
override fun toString() = render()
}
class Html : Tag("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
class Head : Tag("head") {
fun title(text: String) = initTag(Tag("title") {}) { setText(text) }
}
class Body : Tag("body") {
fun h1(text: String) = initTag(Tag("h1") {}) { setText(text) }
fun p(text: String) = initTag(Tag("p") {}) { setText(text) }
fun ul(init: Ul.() -> Unit) = initTag(Ul(), init)
}
class Ul : Tag("ul") {
fun li(text: String) = initTag(Tag("li") {}) { setText(text) }
}
fun html(init: Html.() -> Unit): Html {
val h = Html()
h.init()
return h
}
fun main() {
val page = html {
body {
ul {
li("First")
li("Second")
}
}
}
println(page.render())
}
/*
Output (approximately, exact whitespace depends on render()'s simple
joinToString-based nesting):
Notes:
- Ul follows the exact same pattern as Head and Body: a Tag subclass
with its own builder methods (li here) that call initTag() the same
way h1/p/title already did.
- body's new ul(init: Ul.() -> Unit) method mirrors head()/body() on
Html — same initTag() call, just targeting a different Tag subtype.
- Because Tag is a single shared base class, adding a new tag type only
requires: (1) a new Tag subclass with its own builder method(s), and
(2) one new builder method on whichever parent tag should be able to
contain it (Body, in this case) — the rendering logic itself (render())
doesn't need to change at all.
*/