package nl.astraeus.komp import kotlinx.browser.document import kotlinx.html.* import org.w3c.dom.Element import org.w3c.dom.Node import org.w3c.dom.events.Event import kotlin.collections.MutableList import kotlin.collections.MutableMap import kotlin.collections.arrayListOf import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.isNotEmpty import kotlin.collections.iterator import kotlin.collections.last import kotlin.collections.lastIndex import kotlin.collections.mutableListOf import kotlin.collections.mutableMapOf import kotlin.collections.set import kotlin.collections.withIndex private fun attributeHash(key: String, value: String): Int = 3 * key.hashCode() + 5 * value.hashCode() private fun MutableMap<String, String>.kompHash(): Int { var result = 0 for ((name, value) in this) { result += attributeHash(name, value) } return result } private fun MutableMap<String, (Event) -> Unit>.kompHash(): Int { var result = 0 for ((name, event) in this) { result += attributeHash(name, event.toString()) } return result } private fun MutableList<VDOMElement>.kompHash(): Int { var result = 0 for (vdom in this) { result += 3 * vdom.hash.hashCode() } return result } enum class VDOMElementType { TAG, TEXT, ENTITY, UNSAFE, COMMENT } class VDOMElementHash( var baseHash: Int, var contentHash: Int, var typeHash: Int, var namespaceHash: Int = 0, var attributesHash: Int = 0, var eventsHash: Int = 0, var childNodesHash: Int = 0 ) { override fun hashCode(): Int = baseHash + 3 * contentHash + 5 * typeHash + 7 * namespaceHash + 11 * attributesHash + 13 * eventsHash + 15 * childNodesHash override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is VDOMElementHash) return false return other.hashCode() == this.hashCode() } } class VDOMElement( val baseHash: Int, var content: String, var namespace: String? = null, var type: VDOMElementType = VDOMElementType.TAG, ) { val attributes: MutableMap<String, String> = mutableMapOf() val events: MutableMap<String, (Event) -> Unit> = mutableMapOf() val childNodes: MutableList<VDOMElement> = mutableListOf() val hash = VDOMElementHash( baseHash, content.hashCode(), type.hashCode() ) var id: String = "" set(value) { field = value attributes["id"] = value } var komponent: Komponent? = null fun setKompEvent(event: String, value: (Event) -> Unit) { val eventName = if (event.startsWith("on")) { event.substring(2) } else { event } val recalculate = events.containsKey(eventName) events[eventName] = value if (recalculate) { hash.eventsHash = events.kompHash() } else { hash.eventsHash += attributeHash(eventName, value.toString()) } } fun appendChild(element: VDOMElement) { childNodes.add(element) //hash.childNodesHash += element.hash.hashCode() } fun updateChildHash() { hash.childNodesHash = childNodes.kompHash() } fun removeAttribute(attr: String) { if (attributes.containsKey(attr)) { hash.attributesHash -= attributeHash(attr, attributes[attr] ?: "") } attributes.remove(attr) } fun setAttribute(attr: String, value: String) { if (attributes.containsKey(attr)) { hash.attributesHash -= attributeHash(attr, attributes[attr] ?: "") } if (attr.toLowerCase() == "id") { id = value } attributes[attr] = value hash.attributesHash += attributeHash(attr, value) } fun findNodeHashIndex(hash: Int): Int { for ((index, node) in this.childNodes.withIndex()) { if (node.type == VDOMElementType.TAG && node.hash.hashCode() == hash) { return index } } return -2 } fun createElement(): Node { val result = when (type) { VDOMElementType.TAG -> { val result: Element = if (namespace != null) { document.createElementNS(namespace, content) } else { document.createElement(content) } for ((name, value) in attributes) { result.setAttribute(name, value) } for ((name, value) in events) { result.addEventListener(name, value) } for (child in childNodes) { result.appendChild(child.createElement()) } result } VDOMElementType.ENTITY, VDOMElementType.UNSAFE, VDOMElementType.TEXT -> { document.createTextNode(content) } VDOMElementType.COMMENT -> { document.createComment(content) } } komponent?.also { it.element = result } return result } } interface HtmlConsumer : TagConsumer<VDOMElement> { fun append(node: VDOMElement) } class HtmlBuilder( val baseHash: Int ) : HtmlConsumer { private val path = arrayListOf<VDOMElement>() private var lastLeaved: VDOMElement? = null override fun onTagStart(tag: Tag) { val element = VDOMElement(baseHash, tag.tagName, tag.namespace) for (entry in tag.attributesEntries) { element.setAttribute(entry.key, entry.value) } if (path.isNotEmpty()) { path.last().appendChild(element) } path.add(element) } override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { when { path.isEmpty() -> throw IllegalStateException("No current tag") path.last().content.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag") else -> path.last().let { node -> if (value == null) { node.removeAttribute(attribute) } else { node.setAttribute(attribute, value) } } } } override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) { when { path.isEmpty() -> throw IllegalStateException("No current tag") path.last().content.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag") else -> path.last().setKompEvent(event, value) } } override fun onTagEnd(tag: Tag) { if (path.isEmpty() || path.last().content.toLowerCase() != tag.tagName.toLowerCase()) { throw IllegalStateException("We haven't entered tag ${tag.tagName} but trying to leave") } lastLeaved = path.removeAt(path.lastIndex) lastLeaved?.updateChildHash() } override fun onTagContent(content: CharSequence) { if (path.isEmpty()) { throw IllegalStateException("No current DOM node") } path.last().appendChild(VDOMElement(baseHash, content.toString(), type = VDOMElementType.TEXT)) } override fun onTagContentEntity(entity: Entities) { if (path.isEmpty()) { throw IllegalStateException("No current DOM node") } // stupid hack as browsers don't support createEntityReference path.last().appendChild(VDOMElement(baseHash, entity.text, type = VDOMElementType.ENTITY)) } override fun append(node: VDOMElement) { path.last().appendChild(node) } override fun onTagContentUnsafe(block: Unsafe.() -> Unit) { with(DefaultUnsafe()) { block() path.last().appendChild(VDOMElement(baseHash, toString(), type = VDOMElementType.UNSAFE)) } } override fun onTagComment(content: CharSequence) { if (path.isEmpty()) { throw IllegalStateException("No current DOM node") } path.last().appendChild(VDOMElement(baseHash, content.toString(), type = VDOMElementType.COMMENT)) } override fun finalize(): VDOMElement { return lastLeaved ?: throw IllegalStateException("We can't finalize as there was no tags") } companion object { fun create(content: HtmlBuilder.() -> Unit): VDOMElement { val komponent = DummyKomponent() val consumer = HtmlBuilder(komponent.hashCode()) content.invoke(consumer) return consumer.finalize() } } }