diff --git a/bootstrap3_ziohttp/src/test/scala/cssdsl/bootstrap3/ZioHttpTemplate2DslSpec.scala b/bootstrap3_ziohttp/src/test/scala/cssdsl/bootstrap3/ZioHttpTemplate2DslSpec.scala new file mode 100644 index 0000000..46c1426 --- /dev/null +++ b/bootstrap3_ziohttp/src/test/scala/cssdsl/bootstrap3/ZioHttpTemplate2DslSpec.scala @@ -0,0 +1,62 @@ +package cssdsl.bootstrap3 + +import munit.FunSuite +import zio.http.template2._ + +class ZioHttpTemplate2DslSpec extends FunSuite { + test("chain CSS classes onto a ZIO HTTP template2 element") { + import Dsl._ + + val result = + div(className := C.panelSuccess).panel.hiddenXs( + h3.panelTitle("Panel title") + ) + + assertEquals( + result.render, + """""" + ) + } + + test("compose C values in a template2 className attribute") { + import Dsl._ + + val result = div(className := (C.panel, C.hiddenXs)) + + assertEquals(result.render, """""") + } + + test("compose C collections in a template2 className attribute") { + import Dsl._ + + val result = div(className := List(C.panel, C.hiddenXs)) + + assertEquals(result.render, """""") + } + + test("do not convert C values into template2 text children") { + val errors = compileErrors(""" + import cssdsl.bootstrap3.Dsl._ + import zio.http.template2._ + div(C.panel) + """) + + assert(errors.nonEmpty) + } + + test("preserve an existing single-value class attribute") { + import Dsl._ + + val result = div(Dom.attr("class", "custom")).panel + + assertEquals(result.render, """
""") + } + + test("normalize an existing class attribute to space separation") { + import Dsl._ + + val result = div(Dom.multiAttr("class", AttributeSeparator.Comma, "custom")).panel + + assertEquals(result.render, """
""") + } +} diff --git a/build.sbt b/build.sbt index 4864f29..b6395ea 100644 --- a/build.sbt +++ b/build.sbt @@ -48,6 +48,12 @@ def scalatagsSettings(config: CssDslConfig) = Seq( cssDslConfig := config ) +def zioHttpTemplate2Settings(config: CssDslConfig) = Seq( + libraryDependencies += "dev.zio" %% "zio-http" % "3.11.3", + cssVariant := TargetImpl.ZioHttpTemplate2, + cssDslConfig := config +) + val bootstrap3Config = CssDslConfig( "Bootstrap 3", @@ -116,38 +122,57 @@ lazy val bootstrap3_scalajsreact = project.enablePlugins(ScalaJSPlugin, GeneratorPlugin).settings(scalaJsReactSettings(bootstrap3Config)) lazy val bootstrap3_scalatags = project.enablePlugins(GeneratorPlugin).settings(scalatagsSettings(bootstrap3Config)) +lazy val bootstrap3_ziohttp = + project + .enablePlugins(GeneratorPlugin) + .settings(zioHttpTemplate2Settings(bootstrap3Config)) + .settings(libraryDependencies += "org.scalameta" %% "munit" % "1.2.4" % Test) lazy val bootstrap4_scalajsreact = project.enablePlugins(ScalaJSPlugin, GeneratorPlugin).settings(scalaJsReactSettings(bootstrap4Config)) lazy val bootstrap4_scalatags = project.enablePlugins(GeneratorPlugin).settings(scalatagsSettings(bootstrap4Config)) +lazy val bootstrap4_ziohttp = + project.enablePlugins(GeneratorPlugin).settings(zioHttpTemplate2Settings(bootstrap4Config)) lazy val bootstrap5_scalajsreact = project.enablePlugins(ScalaJSPlugin, GeneratorPlugin).settings(scalaJsReactSettings(bootstrap5Config)) lazy val bootstrap5_scalatags = project.enablePlugins(GeneratorPlugin).settings(scalatagsSettings(bootstrap5Config)) +lazy val bootstrap5_ziohttp = + project.enablePlugins(GeneratorPlugin).settings(zioHttpTemplate2Settings(bootstrap5Config)) lazy val bulma_scalajsreact = project.enablePlugins(ScalaJSPlugin, GeneratorPlugin).settings(scalaJsReactSettings(bulmaConfig)) lazy val bulma_scalatags = project.enablePlugins(GeneratorPlugin).settings(scalatagsSettings(bulmaConfig)) +lazy val bulma_ziohttp = + project.enablePlugins(GeneratorPlugin).settings(zioHttpTemplate2Settings(bulmaConfig)) lazy val tabler_scalajsreact = project.enablePlugins(ScalaJSPlugin, GeneratorPlugin).settings(scalaJsReactSettings(tablerConfig)) lazy val tabler_scalatags = project.enablePlugins(GeneratorPlugin).settings(scalatagsSettings(tablerConfig)) +lazy val tabler_ziohttp = + project.enablePlugins(GeneratorPlugin).settings(zioHttpTemplate2Settings(tablerConfig)) lazy val semanticui_scalajsreact = project.enablePlugins(ScalaJSPlugin, GeneratorPlugin).settings(scalaJsReactSettings(semanticUiConfig)) lazy val semanticui_scalatags = project.enablePlugins(GeneratorPlugin).settings(scalatagsSettings(semanticUiConfig)) +lazy val semanticui_ziohttp = + project.enablePlugins(GeneratorPlugin).settings(zioHttpTemplate2Settings(semanticUiConfig)) lazy val fomanticui_scalajsreact = project.enablePlugins(ScalaJSPlugin, GeneratorPlugin).settings(scalaJsReactSettings(fomanticUiConfig)) lazy val fomanticui_scalatags = project.enablePlugins(GeneratorPlugin).settings(scalatagsSettings(fomanticUiConfig)) +lazy val fomanticui_ziohttp = + project.enablePlugins(GeneratorPlugin).settings(zioHttpTemplate2Settings(fomanticUiConfig)) lazy val fontawesome_scalajsreact = project.enablePlugins(ScalaJSPlugin, GeneratorPlugin).settings(scalaJsReactSettings(fontawesomeUiConfig)) lazy val fontawesome_scalatags = project.enablePlugins(GeneratorPlugin).settings(scalatagsSettings(fontawesomeUiConfig)) +lazy val fontawesome_ziohttp = + project.enablePlugins(GeneratorPlugin).settings(zioHttpTemplate2Settings(fontawesomeUiConfig)) diff --git a/generateInstructions.sbt b/generateInstructions.sbt index a2d3b5a..8243028 100644 --- a/generateInstructions.sbt +++ b/generateInstructions.sbt @@ -31,6 +31,7 @@ LocalRootProject / generateInstallInstructions := { val (op, library) = moduleNameSuffix match { case "scalatags" => "%%" -> "`scalatags.Text` (JVM)" case "scalajsreact" => "%%%" -> "scalajs-react (scala.js)" + case "ziohttp" => "%%" -> "ZIO HTTP `template2`" } List( config.name, diff --git a/project/Generator.scala b/project/Generator.scala index 1ac2f01..193a4ed 100644 --- a/project/Generator.scala +++ b/project/Generator.scala @@ -34,7 +34,14 @@ class Generator(packageName: String, def defs: List[Stat] = classes.toList.map { cls => - Defn.Val(List(Mod.Lazy()), List(Pat.Var(Term.Name(ident(cls)))), None, q"this.op($cls)") + val name = Term.Name(ident(cls)) + q"def $name: A = this.op($cls)" + } + + def cachedDefs: List[Stat] = + classes.toList.map { cls => + val name = Pat.Var(Term.Name(ident(cls))) + q"override lazy val $name: A = this.op($cls)" } def allDefs = defs :+ q"protected def op(clz: String): A" @@ -51,6 +58,11 @@ class Generator(packageName: String, ..$allDefs } + trait CachedClasses[A] extends Classes[A] { + ..$cachedDefs + } + + ..${variant.support} ${variant.C} ${variant.implicitClass} diff --git a/project/TargetImpl.scala b/project/TargetImpl.scala index 41b29e7..c2f7c46 100644 --- a/project/TargetImpl.scala +++ b/project/TargetImpl.scala @@ -3,6 +3,7 @@ import scala.meta._ sealed trait TargetImpl { def imports: List[Import] + def support: List[Stat] = Nil def C: Defn.Object def implicitClass: Defn.Class } @@ -15,7 +16,7 @@ object TargetImpl { override def C = q""" - object C extends Classes[TagMod] { + object C extends CachedClasses[TagMod] { protected override def op(clz: String) = ^.cls := clz } """ @@ -29,6 +30,43 @@ object TargetImpl { """ } + object ZioHttpTemplate2 extends TargetImpl { + override def imports = List( + q"import zio.http.template2.Dom" + ) + + override def support = List( + q"final case class ClassName(value: String) extends AnyVal", + q"implicit def classNameToString(className: ClassName): String = className.value", + q"implicit def classNamesToStrings(classNames: Iterable[ClassName]): Iterable[String] = classNames.map(_.value)" + ) + + override def C = + q""" + object C extends CachedClasses[ClassName] { + protected override def op(clz: String): ClassName = ClassName(clz) + } + """ + + override def implicitClass = { + // Keep the literal synthetic: parsed string literals require a tokenizer when Scalameta 4.16 renders the tree. + val classAttribute = Lit.String("class") + q""" + implicit class DomElementExtensionMethods(self: Dom.Element) + extends Classes[Dom.Element] { + protected override def op(clz: String): Dom.Element = { + val classes = self.attributes.get($classAttribute) match { + case Some(Dom.AttributeValue.MultiValue(values, _)) => values + case Some(value) => Vector(value.toString) + case None => Vector.empty + } + self(Dom.multiAttr($classAttribute, classes :+ clz)) + } + } + """ + } + } + object Scalatags extends TargetImpl { override def imports = List( q"import scalatags.Text.all._" @@ -36,7 +74,7 @@ object TargetImpl { override def C = q""" - object C extends Classes[Modifier] { + object C extends CachedClasses[Modifier] { protected override def op(clz: String) = cls := clz } """