Skip to content

Commit

Permalink
Reworked the validation of attribute names to better correspond to HT…
Browse files Browse the repository at this point in the history
…ML standards (#276)
  • Loading branch information
severn-everett authored Jun 17, 2024
1 parent a502c9f commit 2454cff
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 16 deletions.
25 changes: 10 additions & 15 deletions src/commonMain/kotlin/stream.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import kotlinx.html.org.w3c.dom.events.Event
class HTMLStreamBuilder<out O : Appendable>(
val out: O,
val prettyPrint: Boolean,
val xhtmlCompatible: Boolean
val xhtmlCompatible: Boolean,
) : TagConsumer<O> {
private var level = 0
private var ln = true
Expand Down Expand Up @@ -164,23 +164,18 @@ private val escapeMap = mapOf(
Array(maxCode + 1) { mappings[it.toChar()] }
}

private val letterRangeLowerCase = 'a'..'z'
private val letterRangeUpperCase = 'A'..'Z'
private val digitRange = '0'..'9'

private fun Char._isLetter() = this in letterRangeLowerCase || this in letterRangeUpperCase
private fun Char._isDigit() = this in digitRange

private fun String.isValidXmlAttributeName() =
!startsWithXml()
&& this.isNotEmpty()
&& (this[0]._isLetter() || this[0] == '_')
&& this.all { it._isLetter() || it._isDigit() || it in "._:-" }
this.isNotEmpty()
&& !startsWithXml()
// See https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 for which characters are forbidden
// \u000C is the form-feed character. \f is not supported in Kotlin, so it's necessary to use the
// unicode literal.
&& this.none { it in "\t\n\u000C />\"'=" }

private fun String.startsWithXml() = length >= 3
&& (this[0].let { it == 'x' || it == 'X' })
&& (this[1].let { it == 'm' || it == 'M' })
&& (this[2].let { it == 'l' || it == 'L' })
&& (this[0].let { it == 'x' || it == 'X' })
&& (this[1].let { it == 'm' || it == 'M' })
&& (this[2].let { it == 'l' || it == 'L' })

internal fun Appendable.escapeAppend(value: CharSequence) {
var lastIndex = 0
Expand Down
48 changes: 47 additions & 1 deletion src/commonTest/kotlin/AttributesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import kotlinx.html.div
import kotlinx.html.stream.appendHTML
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class AttributesTest {

Expand All @@ -20,4 +21,49 @@ class AttributesTest {
assertEquals(message, html)
assertEquals(dataTest, dataTestAttribute)
}
}

@Test
fun testNonLetterNames() {
val html = buildString {
appendHTML(false).div {
attributes["[quoted_bracket]"] = "quoted_bracket"
attributes["(parentheses)"] = "parentheses"
attributes["_underscore"] = "underscore"
attributes["#pound"] = "pound"
attributes["@alpine.attr"] = "alpineAttr"
}
}
assertEquals(
"""
<div [quoted_bracket]="quoted_bracket" (parentheses)="parentheses" _underscore="underscore" #pound="pound" @alpine.attr="alpineAttr"></div>
""".trimIndent(),
html,
)
}

@Test
fun testInvalidAttributeNames() {
listOf(
"", // Must not be empty
"XMLAttr", // Cannot start with XML
"xmlAttr", // That's case-insensitive btw
"\"", // No double quotes
"'", // No single quotes, either
"a b", // No spaces
"A\n", // No newline
"A\t", // No tab
"A\u000C", // No form feed
"A>", // No greater-than sign
"A/", // No forward-slash (solidus)
"A=", // No equals sign
).forEach { attrName ->
assertFailsWith<IllegalArgumentException>("Invalid attribute name '$attrName' validated!") {
buildString {
appendHTML(false).div {
attributes[attrName] = "Should Fail!"
}
}
}
}
}
}

0 comments on commit 2454cff

Please sign in to comment.