0%

Kotlin DSL 之 HTML DSL

Kotlin 作为有高级特性的语言,凭借高阶函数也能支持实现 DSL 。

知识储备

函数

Kotlin 中,函数是“一等公民”。

定义一个函数 sum:

1
2
3
fun sum(a: Int, b: Int): Int {
return a + b
}

函数类型

Kotlin 中,函数是有类型的。上面 sum 函数的类型是 (Int, Int) -> Int

高阶函数

函数参数中有函数,或者返回函数的函数,就是高阶函数。

这里定义一个函数 transform,用于对 2 个 Int 值进行转换操作:

1
2
3
fun transform(a: Int, b: Int, trans: (Int, Int) -> Int): Int {
return trans(a, b)
}

函数引用

既然函数是有类型的,那函数本身就是一个函数对象(参考类与对象的关系),我们也可以通过引用的方式使用函数:

1
2
val func = ::sum
func(1, 2)

上面定义的高阶函数 transform,便可以使用函数引用的方式来进行函数的调用:

1
transform(1, 2, ::sum)

匿名函数

Kotlin 在缺省函数名的情况下定义出来的函数就是匿名函数。

对于上述的 sum 函数,其匿名函数形态:

1
2
3
val func = fun(a: Int, b: Int): Int {
return a + b
}

对于上述的 transform 函数,便可以使用匿名函数的方式来调用:

1
2
3
transform(1, 2, fun(a: Int, b: Int): Int {
return a + b
})

Lambda语法糖

匿名函数的表达方式较为繁琐,可以使用 Lambda 演算 来对齐进行简化,简化后的表达式就是 Lambda 表达式

上面的匿名函数的 Lambda 表达式形态:

1
val func : (Int, Int) -> Int = {a: Int, b: Int -> a + b}

因为 Kotlin 支持类型推导,可以简化:

1
2
3
val func = {a: Int, b: Int -> a + b}
// or
val func2: (Int, Int) -> Int = {a, b -> a + b}

对于前述 transform 函数,便可以使用 Lambda 表达式的方式调用:

1
2
3
transform(1, 2, {a, b ->
a + b
})

如果高阶函数最后一个参数是函数,可以将 Lambda 表达式移至函数外:

1
2
3
transform(1, 2) { a, b ->
a + b
}

扩展函数

在不修改已有类的前提下,对其新增方法,可以通过扩展函数来实现。例如:

1
2
3
fun View.isVisible(): Boolean {
return this.visibility == View.VISIBLE
}

上面的 View 是 接收者(Receiver)类型。

扩展函数也是函数,也有其函数类型:

1
val visibleFunc: (View) -> Boolean = View::isVisible

可以看出,扩展函数函数类型的第一个参数就是 Receiver 类型。

运算符重载

Java 中没有运算符重载,Kotlin 能进行运算符重载。重载运算符能通过扩展函数或者成员函数来实现。

看一个扩展函数实现的案例:

1
2
3
4
5
6
7
8
9
data class Point(val x: Int, val y: Int)

// 重载 一元减号 运算符
operator fun Point.unaryMinus() = Point(-x, -y)

fun main() {
val point = Point(1, 2)
print(-point) // Point(x=-1, y=-2)
}

invoke 运算符也可以重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ContentParser {
fun parse(file: File): String {
//...
}

operator fun invoke(file: File) = parse(file)
}

fun main() {
val parser = ContentParser()
val file = File("/path/to/file")
// 方式1: invoke 函数
parser.invoke(file)
// 方式2: 直接作为类似于函数的方式调用
parser(file)
}

HTML DSL 设计实现

对于如下 HTML 代码:

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<meta charset="utf-8"></meta>
</head>
<body>
<div style="width: 200px; height: 200px; line-height: 200px; background-color: #FF4081; text-align: center;">
<span style="color: white; font-family: Microsoft YaHei;">Hello Kotlin DSL</span>
</div>
</body>
</html>

可以看到 HTML 本身是非常结构化的,HTML 由标签组成,每个标签可以有属性(属性 key, 属性 value )和值,这里的值包括 子标签 和 文本。

我们的目标是用 DSL 化的语言来描述上述 HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
val htmlContent =
"html" {
"head" {
"meta" {
"charset"("utf-8")
}
}

"body" {
"div" {
"style"("width: 200px; height: 200px; line-height: 200px; background-color: #FF4081; text-align: center;")
"span" {
"style"("color: white; font-family: Microsoft YaHei;")
+"Hello Kotlin DSL"
}
}
}
}
.render()

分析下上面的 DSL:

1
2
3
"html" {
//...
}

原型应该是: fun String.invoke(function: FunctionType),用来表示一个 HTML 节点

1
"charset"("utf-8")

原型应该是: operator fun String.invoke(propertyValue: String),用来表示一个 HTML 属性的 key 和 value

1
+"Hello Kotlin DSL"

原型应该是: operator fun String.unaryPlus(),用来表示 HTML 标签的文本。

以上便是此 HTML DSL 的要点。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import java.io.File

// Kotlin HTML DSL
interface Node {
fun render(): String
}

class StringNode(private val content: String) : Node {
override fun render(): String {
return content
}

override fun toString() = render()
}

// TagNode 表示 HTML 中的任一标签
class TagNode(private val tagName: String) : Node {

// 标签中的子元素
private val elements = ArrayList<Node>()

// 当前标签的属性值
private val properties = HashMap<String, Any>()

override fun render(): String {
return """<$tagName${
properties.takeIf { it.size > 0 }?.map { "${it.key}=\"${it.value}\"" }?.joinToString(" ", " ") ?: ""
}>${elements.joinToString("") { it.render() }}</$tagName>""".trimIndent()
}

operator fun String.invoke(block: TagNode.() -> Unit): TagNode {
val tagNode = TagNode(this)
tagNode.block()
this@TagNode.elements += tagNode
return tagNode
}

operator fun String.invoke(propertyValue: Any) {
this@TagNode.properties[this] = propertyValue
}

operator fun String.unaryPlus() {
this@TagNode.elements += StringNode(this)
}

override fun toString() = render()

}

operator fun String.invoke(block: TagNode.() -> Unit): Node {
val tagNode = TagNode(this)
tagNode.block()
return tagNode
}


fun main() {
val htmlContent =
"html" {
"head" {
"meta" {
"charset"("utf-8")
}
}

"body" {
"div" {
"style"("width: 200px; height: 200px; line-height: 200px; background-color: #FF4081; text-align: center;")
"span" {
"style"("color: white; font-family: Microsoft YaHei;")
+"Hello Kotlin DSL"
}
}
}
}
.render()

println(htmlContent)

File("HelloKotlinDSL.html").writeText(htmlContent)

val bodyContent = "body" {
"h1" {
+"Hello Kotlin"
}
}
println(bodyContent)

val relativeLayout = "RelativeLayout" {
"android:layout_width"("match_parent")
"android:layout_height"("wrap_content")

"TextView" {
"android:id"("@+id/textview")
"android:layout_width"("200dp")
"android:height"("wrap_content")
"android:text"("Hello Kotlin DSL")
}
}
println(relativeLayout)
}

参考