-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHtmlReplace.kt
168 lines (146 loc) · 4.7 KB
/
HtmlReplace.kt
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package com.github.kronstein.html
import kotlinx.html.Entities
import kotlinx.html.HTMLTag
import kotlinx.html.HtmlInlineTag
import kotlinx.html.HtmlTagMarker
import kotlinx.html.Tag
import kotlinx.html.TagConsumer
import kotlinx.html.Unsafe
import kotlinx.html.consumers.delayed
import kotlinx.html.org.w3c.dom.events.Event
import kotlinx.html.stream.appendHTML
import kotlinx.html.unsafe
import kotlinx.html.visitAndFinalize
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
/**
* Безопасно заменяет один или несколько тэгов в HTML.
* @param html исходный HTML
* @param cssQuery селекторы и замены
*/
fun htmlReplace(
html: String,
cssQuery: Map<String, TagConsumer<StringBuilder>.(Element) -> Unit>,
): String {
val document = Jsoup.parse(html)
val elements = document.body().children()
val elementsToReplace = cssQuery.flatMap { (selector, function) ->
document.select(selector).map { it to function }
}.toMap()
return buildString {
with(appendHTML(prettyPrint = false).replacing(elementsToReplace)) {
elements.forEach {
jsoupElement(it)
}
}
}.trim()
}
/**
* Безопасно заменяет один или несколько тэгов в HTML.
* @param html исходный HTML
* @param cssQuery селектор и замена
*/
fun htmlReplace(
html: String,
cssQuery: Pair<String, TagConsumer<StringBuilder>.(Element) -> Unit>,
) = htmlReplace(
html = html,
cssQuery = mapOf(cssQuery)
)
private class JsoupElement(
val element: Element,
consumer: TagConsumer<*>,
): HTMLTag(
tagName = element.tag().normalName(),
consumer = consumer,
emptyTag = false,
inlineTag = true,
initialAttributes = element.attributes().associate {
it.key to it.value
}
), HtmlInlineTag {}
@HtmlTagMarker
private fun <T> TagConsumer<T>.jsoupElement(
element: Element,
): T {
return JsoupElement(
element = element,
consumer = this,
).visitAndFinalize(
consumer = this,
block = {
element.childNodes().forEach {
when (it) {
is Element -> jsoupElement(it)
else -> unsafe {
raw(it.outerHtml())
}
}
}
}
)
}
private fun <T> TagConsumer<T>.replacing(
elementsToReplace: Map<Element, TagConsumer<T>.(Element) -> Unit>,
): TagConsumer<T> = JsoupReplaceConsumer(
downstream = this,
elementsToReplace = elementsToReplace,
).delayed()
private class JsoupReplaceConsumer<T>(
private val downstream: TagConsumer<T>,
private val elementsToReplace: Map<Element, TagConsumer<T>.(Element) -> Unit>,
): TagConsumer<T> {
private var currentLevel = 0
private var skippedLevels = HashSet<Int>()
private var dropLevel: Int? = null
override fun onTagStart(tag: Tag) {
currentLevel++
if (dropLevel == null) {
when (tag) {
is JsoupElement -> elementsToReplace[tag.element]?.let { block ->
downstream.block(tag.element)
dropLevel = currentLevel
} ?: downstream.onTagStart(tag)
else -> downstream.onTagStart(tag)
}
}
}
override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) {
throw UnsupportedOperationException("this filter doesn't support onTagAttributeChange")
}
override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) {
throw UnsupportedOperationException("this filter doesn't support onTagEvent")
}
override fun onTagEnd(tag: Tag) {
if (canPassCurrentLevel()) {
downstream.onTagEnd(tag)
}
skippedLevels.remove(currentLevel)
if (dropLevel == currentLevel) {
dropLevel = null
}
currentLevel--
}
override fun onTagContent(content: CharSequence) {
if (canPassCurrentLevel()) {
downstream.onTagContent(content)
}
}
override fun onTagContentEntity(entity: Entities) {
if (canPassCurrentLevel()) {
downstream.onTagContentEntity(entity)
}
}
override fun onTagContentUnsafe(block: Unsafe.() -> Unit) {
if (canPassCurrentLevel()) {
downstream.onTagContentUnsafe(block)
}
}
private fun canPassCurrentLevel() = dropLevel == null && currentLevel !in skippedLevels
override fun onTagComment(content: CharSequence) {
if (canPassCurrentLevel()) {
downstream.onTagComment(content)
}
}
override fun finalize(): T = downstream.finalize()
}