Newer
Older
komp / src / jsMain / kotlin / nl / astraeus / komp / HtmlBuilder.kt
rnentjes on 29 Jun 2021 3 KB Cleanup
package nl.astraeus.komp

import kotlinx.html.DefaultUnsafe
import kotlinx.html.Entities
import kotlinx.html.Tag
import kotlinx.html.TagConsumer
import kotlinx.html.Unsafe
import org.w3c.dom.events.Event

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()
    }
  }
}