diff --git a/src/commonMain/kotlin/api.kt b/src/commonMain/kotlin/api.kt index 54a48b3e..064f31c5 100644 --- a/src/commonMain/kotlin/api.kt +++ b/src/commonMain/kotlin/api.kt @@ -12,8 +12,13 @@ interface TagConsumer { fun onTagContentUnsafe(block: Unsafe.() -> Unit) fun onTagComment(content: CharSequence) fun finalize(): R + @ExperimentalKotlinxHtmlApi + val head: Any? } +@RequiresOptIn +annotation class ExperimentalKotlinxHtmlApi + @HtmlTagMarker interface Tag { val tagName: String diff --git a/src/commonMain/kotlin/delayed-consumer.kt b/src/commonMain/kotlin/delayed-consumer.kt index baf810b3..ac4fa8ca 100644 --- a/src/commonMain/kotlin/delayed-consumer.kt +++ b/src/commonMain/kotlin/delayed-consumer.kt @@ -48,6 +48,9 @@ class DelayedConsumer(val downstream: TagConsumer) : TagConsumer { return downstream.finalize() } + @ExperimentalKotlinxHtmlApi + override val head: Any? get() = downstream.head + override fun onTagContentUnsafe(block: Unsafe.() -> Unit) { processDelayedTag() return downstream.onTagContentUnsafe(block) diff --git a/src/commonMain/kotlin/filter-consumer.kt b/src/commonMain/kotlin/filter-consumer.kt index 8fc60af1..de99f583 100644 --- a/src/commonMain/kotlin/filter-consumer.kt +++ b/src/commonMain/kotlin/filter-consumer.kt @@ -81,6 +81,9 @@ private class FilterTagConsumer(val downstream: TagConsumer, val predicate } override fun finalize(): T = downstream.finalize() + + @ExperimentalKotlinxHtmlApi + override val head: Any? get() = downstream.head } fun TagConsumer.filter(predicate: PredicateResults.(Tag) -> PredicateResult): TagConsumer = diff --git a/src/commonMain/kotlin/finalize-consumer.kt b/src/commonMain/kotlin/finalize-consumer.kt index 22b9484c..425b72e1 100644 --- a/src/commonMain/kotlin/finalize-consumer.kt +++ b/src/commonMain/kotlin/finalize-consumer.kt @@ -26,6 +26,9 @@ class FinalizeConsumer(val downstream: TagConsumer, val block: (F, Bool override fun onTagComment(content: CharSequence) = downstream.onTagComment(content) override fun finalize() = block(downstream.finalize(), level > 0) + + @ExperimentalKotlinxHtmlApi + override val head: Any? get() = downstream.head } public fun TagConsumer.onFinalize(block: (from: T, partial: Boolean) -> Unit): TagConsumer = diff --git a/src/commonMain/kotlin/measure-consumer.kt b/src/commonMain/kotlin/measure-consumer.kt index ffec48ca..c2346f2c 100644 --- a/src/commonMain/kotlin/measure-consumer.kt +++ b/src/commonMain/kotlin/measure-consumer.kt @@ -41,6 +41,9 @@ private class TimeMeasureConsumer(val downstream: TagConsumer, val start: } override fun finalize(): TimedValue = TimedValue(downstream.finalize(), start.elapsedNow()) + + @ExperimentalKotlinxHtmlApi + override val head: Any? get() = downstream.head } fun TagConsumer.measureTime(timeSource: TimeSource = TimeSource.Monotonic): TagConsumer> = TimeMeasureConsumer(this, timeSource.markNow()) diff --git a/src/commonMain/kotlin/stream.kt b/src/commonMain/kotlin/stream.kt index 63fd9cd0..f3ccbe12 100644 --- a/src/commonMain/kotlin/stream.kt +++ b/src/commonMain/kotlin/stream.kt @@ -86,6 +86,9 @@ class HTMLStreamBuilder( override fun finalize(): O = out + @ExperimentalKotlinxHtmlApi + override val head: O get() = out + override fun onTagContentUnsafe(block: Unsafe.() -> Unit) { UnsafeImpl.block() } diff --git a/src/jsMain/kotlin/dom-js.kt b/src/jsMain/kotlin/dom-js.kt index b1622317..516f4f39 100644 --- a/src/jsMain/kotlin/dom-js.kt +++ b/src/jsMain/kotlin/dom-js.kt @@ -2,6 +2,7 @@ package kotlinx.html.dom import kotlinx.html.DefaultUnsafe import kotlinx.html.Entities +import kotlinx.html.ExperimentalKotlinxHtmlApi import kotlinx.html.Tag import kotlinx.html.TagConsumer import kotlinx.html.Unsafe @@ -113,12 +114,13 @@ class JSDOMBuilder(val document : Document) : TagConsumer = JSDOMBuilder(this) val Document.create : TagConsumer get() = JSDOMBuilder(this) diff --git a/src/jsTest/kotlin/interoperable.kt b/src/jsTest/kotlin/interoperable.kt new file mode 100644 index 00000000..5ae47281 --- /dev/null +++ b/src/jsTest/kotlin/interoperable.kt @@ -0,0 +1,32 @@ +import kotlinx.browser.document +import kotlinx.html.ExperimentalKotlinxHtmlApi +import kotlinx.html.Tag +import kotlinx.html.js.div +import kotlinx.html.dom.append +import org.w3c.dom.HTMLElement +import org.w3c.dom.Node +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalKotlinxHtmlApi::class) +class InteroperableImplTest { + @Test fun testInteroperableDOM() { + val wrapper = wrapper() + + wrapper.append.div { + interop { + document.createElement("svg") + } + } + + assertEquals(expected = "
", actual = wrapper.innerHTML) + } + + private fun wrapper() = document.body!!.append.div {} + + // Stand in for how a library might want to interop with kotlinx-html + private fun Tag.interop(block: () -> Node) { + val last = consumer.head as HTMLElement + last.appendChild(block()) + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/dom-jvm.kt b/src/jvmMain/kotlin/dom-jvm.kt index b13b6e26..caf04cb6 100644 --- a/src/jvmMain/kotlin/dom-jvm.kt +++ b/src/jvmMain/kotlin/dom-jvm.kt @@ -1,6 +1,7 @@ package kotlinx.html.dom import kotlinx.html.Entities +import kotlinx.html.ExperimentalKotlinxHtmlApi import kotlinx.html.Tag import kotlinx.html.TagConsumer import kotlinx.html.Unsafe @@ -100,6 +101,9 @@ class HTMLDOMBuilder(val document : Document) : TagConsumer { override fun finalize() = lastLeaved ?: throw IllegalStateException("No tags were emitted") + @ExperimentalKotlinxHtmlApi + override val head: Element get() = path.last() + override fun onTagContentUnsafe(block: Unsafe.() -> Unit) { UnsafeImpl.block() } diff --git a/src/jvmTest/kotlin/interoperable.kt b/src/jvmTest/kotlin/interoperable.kt new file mode 100644 index 00000000..f17811a9 --- /dev/null +++ b/src/jvmTest/kotlin/interoperable.kt @@ -0,0 +1,30 @@ +import kotlinx.html.ExperimentalKotlinxHtmlApi +import kotlinx.html.Tag +import kotlinx.html.div +import kotlinx.html.dom.createHTMLDocument +import kotlinx.html.dom.serialize +import org.w3c.dom.Document +import org.w3c.dom.Element +import org.w3c.dom.Node +import javax.xml.parsers.DocumentBuilderFactory +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalKotlinxHtmlApi::class) +class InteroperableImplTest { + @Test fun testInteroperableDOM() { + DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() + val tree = createHTMLDocument().div { + interop { + it.createElement("svg") + } + } + assertEquals(expected = "\n
", actual = tree.serialize(prettyPrint = false)) + } + + // Stand in for how a library might want to interop with kotlinx-html + private fun Tag.interop(block: (Document) -> Node) { + val last = consumer.head as Element + last.appendChild(block(last.ownerDocument)) + } +} \ No newline at end of file diff --git a/src/wasmJsMain/kotlin/dom-js.kt b/src/wasmJsMain/kotlin/dom-js.kt index f5ae9d13..b1e71d22 100644 --- a/src/wasmJsMain/kotlin/dom-js.kt +++ b/src/wasmJsMain/kotlin/dom-js.kt @@ -2,6 +2,7 @@ package kotlinx.html.dom import kotlinx.html.DefaultUnsafe import kotlinx.html.Entities +import kotlinx.html.ExperimentalKotlinxHtmlApi import kotlinx.html.Tag import kotlinx.html.TagConsumer import kotlinx.html.Unsafe @@ -117,10 +118,12 @@ class JSDOMBuilder(val document: Document) : TagConsumer override fun finalize(): R = lastLeaved?.asR() ?: throw IllegalStateException("We can't finalize as there was no tags") + @ExperimentalKotlinxHtmlApi + override val head: Element get() = path.last() + private inline fun Element.asR(): R { return jsCast(this) } - } fun jsCast(any: JsAny): T = js("(any)") diff --git a/src/wasmJsTest/kotlin/interoperable.kt b/src/wasmJsTest/kotlin/interoperable.kt new file mode 100644 index 00000000..5ae47281 --- /dev/null +++ b/src/wasmJsTest/kotlin/interoperable.kt @@ -0,0 +1,32 @@ +import kotlinx.browser.document +import kotlinx.html.ExperimentalKotlinxHtmlApi +import kotlinx.html.Tag +import kotlinx.html.js.div +import kotlinx.html.dom.append +import org.w3c.dom.HTMLElement +import org.w3c.dom.Node +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalKotlinxHtmlApi::class) +class InteroperableImplTest { + @Test fun testInteroperableDOM() { + val wrapper = wrapper() + + wrapper.append.div { + interop { + document.createElement("svg") + } + } + + assertEquals(expected = "
", actual = wrapper.innerHTML) + } + + private fun wrapper() = document.body!!.append.div {} + + // Stand in for how a library might want to interop with kotlinx-html + private fun Tag.interop(block: () -> Node) { + val last = consumer.head as HTMLElement + last.appendChild(block()) + } +} \ No newline at end of file