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 val attrMap: Map[String, String] =
      Map("class" -> "cls", "type" -> "tpe", "viewbox" -> "viewBox", "stroke-linecap" -> "strokeLineCap", "stroke-linejoin" -> "strokeLineJoin").withDefault { cls =>
        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 (cls.startsWith("aria")) {
          s"aria.${toCap(cls.split("-").toList.tail)}"
        } else {
          dashToCap(cls)
        }
      }
    override def head(node: Node, depth: Int): Unit =
      node match {
        case el: Element => Some(() => writeElement(el, depth))
        case t: TextNode if !t.text.isBlank => Some(() => writeTripleQuoted(t.text.trim))
        case _ => None
      } map (writeNext(depth))
    override def tail(node: Node, depth: Int): Unit =
      node match {
        case el: Element =>
          code.append("\n")
          writeIndent(depth)
          code.append(")")
        case _           => ()
      }
    def getCode(): String = code.toString()
    private def writeNext(depth: Int)(f: () => Unit): Unit =
          if (written) {
            code.append(",")
          }
          code.append("\n")
          written = true
          writeIndent(depth)
          f()
    private def writeIndent(size: Int): Unit = code.append("\t" * size)
    private def writeQuoted(t: String): Unit =
      code.append('"').append(t.replaceAll("\"", "\\\"")).append('"')
    private def writeTripleQuoted(t: String): Unit =
      code.append(""""""""").append(t).append(""""""""")
    private def writeElement(el: Element, depth: Int): Unit =
      code.append(el.normalName).append("(\n")
      val attrs = el.attributes.asList.asScala
      attrs.zipWithIndex.foreach { (attribute, idx) =>
        if (idx != 0) code.append(",\n")
        code.append("\t" * (depth + 1))
        code
          .append(attrMap(attribute.getKey))
          .append(" := ")
        writeQuoted(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 ()