diff --git a/src/commonMain/kotlin/stream.kt b/src/commonMain/kotlin/stream.kt index 8ba0fdb3..63fd9cd0 100644 --- a/src/commonMain/kotlin/stream.kt +++ b/src/commonMain/kotlin/stream.kt @@ -7,7 +7,7 @@ import kotlinx.html.org.w3c.dom.events.Event class HTMLStreamBuilder( val out: O, val prettyPrint: Boolean, - val xhtmlCompatible: Boolean + val xhtmlCompatible: Boolean, ) : TagConsumer { private var level = 0 private var ln = true @@ -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 diff --git a/src/commonTest/kotlin/AttributesTest.kt b/src/commonTest/kotlin/AttributesTest.kt index 84478bde..f534a606 100644 --- a/src/commonTest/kotlin/AttributesTest.kt +++ b/src/commonTest/kotlin/AttributesTest.kt @@ -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 { @@ -20,4 +21,49 @@ class AttributesTest { assertEquals(message, html) assertEquals(dataTest, dataTestAttribute) } -} \ No newline at end of file + + @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( + """ +
+ """.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("Invalid attribute name '$attrName' validated!") { + buildString { + appendHTML(false).div { + attributes[attrName] = "Should Fail!" + } + } + } + } + } +}