使用Kotlin编写适用于HTML类型安全的构建器

117 阅读2分钟

当我们需要在APP上面发送一篇文章的时候,通常都需要文章能够支持编辑各种文本、字体颜色、甚至需要在文字的任意位置添加链接、图片,这时通常以一种特殊的形式保存文本,并且上传到服务器,下次在获取解析,以此还原文章。

本文将建基于 Kotlin 的适用于采用半声明方式构建复杂层次数据结构领域专用语(DSL)实现一个HTML构建器。代码如下:


interface Element {
    fun render(builder: StringBuilder, indent: String)
}

class TextElement(private val text: String) : Element {
    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent$text\n")
    }
}

@DslMarker
annotation class HtmlTagMarker

@HtmlTagMarker
abstract class Tag(val name: String) : Element {

    val children = arrayListOf<Element>()
    val attributes = hashMapOf<String, String>()

    protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }

    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent<$name${renderAttributes()}>\n")
        for (c in children) {
            c.render(builder, "$indent  ")
        }
        builder.append("$indent</$name>\n")
    }

    private fun renderAttributes(): String {
        val builder = StringBuilder()
        for ((attr, value) in attributes) {
            builder.append(" $attr="$value"")
        }
        return builder.toString()
    }

    override fun toString(): String {
        val builder = StringBuilder()
        render(builder, "")
        return builder.toString()
    }
}

abstract class TagWithText(name: String) : Tag(name) {
    operator fun String.unaryPlus() {
        children.add(TextElement(this))
    }
}

class HTML : TagWithText("html") {
    fun head(init: Head.() -> Unit) = initTag(Head(), init)

    fun body(init: Body.() -> Unit) = initTag(Body(), init)
}

class Head : TagWithText("head") {
    fun title(init: Title.() -> Unit) = initTag(Title(), init)
}

class Title : TagWithText("title") {

    fun h1(init: H1.() -> Unit) = initTag(H1(), init)
}

abstract class BodyTag(name: String) : TagWithText(name) {
    fun b(init: B.() -> Unit) = initTag(B(), init)
    fun p(init: P.() -> Unit) = initTag(P(), init)
    fun h1(init: H1.() -> Unit) = initTag(H1(), init)
    fun a(href: String, init: A.() -> Unit) {
        val a = initTag(A(), init)
        a.href = href
    }
}

class Body : BodyTag("body")
class B : BodyTag("b")
class P : BodyTag("p")
class H1 : BodyTag("h1")

class A : BodyTag("a") {
    var href: String
        get() = attributes["href"]!!
        set(value) {
            attributes["href"] = value
        }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

它构建一个 HTML 树。代码中大量使用了扩展函数带有接收者的 lambda 表达式,学会它也能帮助我们更加的熟悉Kotlin写出更加优雅的代码。使用方法如下:

fun result() =
    html {
        head {

            title {
                +"XML encoding with Kotlin"
                
                h1 {
                    +"111111111"
                }
            }
        }
        body {

            h1 { +"XML encoding with Kotlin" }

            p { +"this format can be used as an alternative markup to XML" }

            // 一个具有属性和文本内容的元素
            a(href = "https://kotlinlang.org") { +"Kotlin" }

            // 混合的内容
            p {
                +"This is some"
                b { +"mixed" }
                +"text. For more see the"
                a(href = "https://kotlinlang.org") { +"Kotlin" }
                +"project"
            }

            p { +"some text" }
        }
    }

fun main() {
    println(result().toString())
}