Skip to content

Commit 2454cff

Browse files
Reworked the validation of attribute names to better correspond to HTML standards (#276)
1 parent a502c9f commit 2454cff

File tree

2 files changed

+57
-16
lines changed

2 files changed

+57
-16
lines changed

src/commonMain/kotlin/stream.kt

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import kotlinx.html.org.w3c.dom.events.Event
77
class HTMLStreamBuilder<out O : Appendable>(
88
val out: O,
99
val prettyPrint: Boolean,
10-
val xhtmlCompatible: Boolean
10+
val xhtmlCompatible: Boolean,
1111
) : TagConsumer<O> {
1212
private var level = 0
1313
private var ln = true
@@ -164,23 +164,18 @@ private val escapeMap = mapOf(
164164
Array(maxCode + 1) { mappings[it.toChar()] }
165165
}
166166

167-
private val letterRangeLowerCase = 'a'..'z'
168-
private val letterRangeUpperCase = 'A'..'Z'
169-
private val digitRange = '0'..'9'
170-
171-
private fun Char._isLetter() = this in letterRangeLowerCase || this in letterRangeUpperCase
172-
private fun Char._isDigit() = this in digitRange
173-
174167
private fun String.isValidXmlAttributeName() =
175-
!startsWithXml()
176-
&& this.isNotEmpty()
177-
&& (this[0]._isLetter() || this[0] == '_')
178-
&& this.all { it._isLetter() || it._isDigit() || it in "._:-" }
168+
this.isNotEmpty()
169+
&& !startsWithXml()
170+
// See https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 for which characters are forbidden
171+
// \u000C is the form-feed character. \f is not supported in Kotlin, so it's necessary to use the
172+
// unicode literal.
173+
&& this.none { it in "\t\n\u000C />\"'=" }
179174

180175
private fun String.startsWithXml() = length >= 3
181-
&& (this[0].let { it == 'x' || it == 'X' })
182-
&& (this[1].let { it == 'm' || it == 'M' })
183-
&& (this[2].let { it == 'l' || it == 'L' })
176+
&& (this[0].let { it == 'x' || it == 'X' })
177+
&& (this[1].let { it == 'm' || it == 'M' })
178+
&& (this[2].let { it == 'l' || it == 'L' })
184179

185180
internal fun Appendable.escapeAppend(value: CharSequence) {
186181
var lastIndex = 0

src/commonTest/kotlin/AttributesTest.kt

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import kotlinx.html.div
22
import kotlinx.html.stream.appendHTML
33
import kotlin.test.Test
44
import kotlin.test.assertEquals
5+
import kotlin.test.assertFailsWith
56

67
class AttributesTest {
78

@@ -20,4 +21,49 @@ class AttributesTest {
2021
assertEquals(message, html)
2122
assertEquals(dataTest, dataTestAttribute)
2223
}
23-
}
24+
25+
@Test
26+
fun testNonLetterNames() {
27+
val html = buildString {
28+
appendHTML(false).div {
29+
attributes["[quoted_bracket]"] = "quoted_bracket"
30+
attributes["(parentheses)"] = "parentheses"
31+
attributes["_underscore"] = "underscore"
32+
attributes["#pound"] = "pound"
33+
attributes["@alpine.attr"] = "alpineAttr"
34+
}
35+
}
36+
assertEquals(
37+
"""
38+
<div [quoted_bracket]="quoted_bracket" (parentheses)="parentheses" _underscore="underscore" #pound="pound" @alpine.attr="alpineAttr"></div>
39+
""".trimIndent(),
40+
html,
41+
)
42+
}
43+
44+
@Test
45+
fun testInvalidAttributeNames() {
46+
listOf(
47+
"", // Must not be empty
48+
"XMLAttr", // Cannot start with XML
49+
"xmlAttr", // That's case-insensitive btw
50+
"\"", // No double quotes
51+
"'", // No single quotes, either
52+
"a b", // No spaces
53+
"A\n", // No newline
54+
"A\t", // No tab
55+
"A\u000C", // No form feed
56+
"A>", // No greater-than sign
57+
"A/", // No forward-slash (solidus)
58+
"A=", // No equals sign
59+
).forEach { attrName ->
60+
assertFailsWith<IllegalArgumentException>("Invalid attribute name '$attrName' validated!") {
61+
buildString {
62+
appendHTML(false).div {
63+
attributes[attrName] = "Should Fail!"
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)