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