Newer
Older
iw-utils / Laminarize.scala
//> using lib "org.jsoup:jsoup:1.14.3"
//> using lib "dev.zio::zio:2.0.0-RC2"
//> using lib "dev.zio::zio-streams:2.0.0-RC2"
//
package works.iterative.tailwind.util

import zio.Task
import org.jsoup.nodes.Document
import org.jsoup.Jsoup
import org.jsoup.select.NodeVisitor
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import scala.jdk.CollectionConverters._
import org.jsoup.nodes.TextNode
import zio.ZIOAppDefault
import zio.ZIO
import zio.stream.ZStream
import java.io.EOFException

object Laminarize extends ZIOAppDefault:

  class LaminarNodeVisitor() extends NodeVisitor:

    private var written: Boolean = false

    private val code: java.lang.StringBuilder = new java.lang.StringBuilder

    private def append(s: String): java.lang.StringBuilder = code.append(s)
    private def append(c: Char): java.lang.StringBuilder = code.append(c)

    private def mangleAttrName(attr: String): String =
        def toCap(words: List[String]): String = words match {
          case h :: t => (h :: t.map(_.capitalize)).mkString("")
          case _ => ""
        }

        def dashToCap(c: String): String = toCap(c.split("-").toList)

        if (attr.startsWith("aria")) {
          s"aria.${toCap(attr.split("-").toList.tail)}"
        } else {
          dashToCap(attr)
        }

    private def mapAttr(k: String): String =
      Map(
        "class" -> "cls",
        "type" -> "tpe",
        "viewbox" -> "viewBox",
        "stroke-linecap" -> "strokeLineCap",
        "stroke-linejoin" -> "strokeLineJoin",
        "for" -> "forId"
      ).withDefault(mangleAttrName)(k)

    private def mapValue(k: String, v: String): String =
      val unquoted = List("aria-hidden")
      if (unquoted.contains(k)) v else s"\"${v}\""

    override def head(node: Node, depth: Int): Unit =
      node match {
        case el: Element => Some(() => appendElement(el, depth))
        case t: TextNode if !t.text.isBlank => Some(() => appendTripleQuoted(t.text.trim))
        case _ => None
      } map (appendNext(depth))

    override def tail(node: Node, depth: Int): Unit =
      node match {
        case el: Element =>
          append("\n")
          appendIndent(depth)
          append(")")
        case _           => ()
      }

    def getCode(): String = code.toString()

    private def appendNext(depth: Int)(f: () => Unit): Unit =
          if (written) {
            append(",")
          }
          append("\n")
          written = true
          appendIndent(depth)
          f()

    private def appendIndent(size: Int): Unit = code.append("\t" * size)

    private def appendQuoted(t: String): Unit =
      append('"').append(t.replaceAll("\"", "\\\"")).append('"')

    private def appendTripleQuoted(t: String): Unit =
      append(""""""""").append(t).append(""""""""")

    private def appendElement(el: Element, depth: Int): Unit =
      append(el.normalName).append("(\n")
      val attrs = el.attributes.asList.asScala
      attrs.zipWithIndex.foreach { (attribute, idx) =>
        if (idx != 0) append(",\n")
        appendIndent(depth + 1)
        append(mapAttr(attribute.getKey))
         .append(" := ")
        append(mapValue(attribute.getKey, attribute.getValue))
      }
      if (attrs.isEmpty) written = false

  def elementToLaminar(el: Element): String =
    val visitor = LaminarNodeVisitor()
    el.children.traverse(visitor)
    visitor.getCode()

  def htmlToLaminar(html: String): Task[String] =
    for {
      doc <- Task.attempt(Jsoup.parseBodyFragment(html))
    } yield elementToLaminar(doc.body)

  override def run =
    for {
      console <- ZIO.environment[zio.Console]
      input <- ZStream
        .repeatZIO(console.get.readLine.asSome.catchSome {
          case _: EOFException =>
            ZIO.succeed(None)
        })
        .takeWhile(_.isDefined)
        .map(_.get)
        .fold("")((a, s) => a + "\n" + s)
      code <- htmlToLaminar(input)
      _ <- console.get.printLine(code)
    } yield ()