zio.http.template.Dom.scala Maven / Gradle / Ivy
/*
* Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package zio.http.template
import scala.collection.immutable.ListMap
import zio.schema.Schema
import zio.http.MediaType
import zio.http.codec.{BinaryCodecWithSchema, HttpContentCodec, TextBinaryCodec}
import zio.http.internal.OutputEncoder
/**
* Light weight DOM implementation that can be rendered as a html string.
*
* @see
* Void
* elements only have a start tag; end tags must not be specified for void
* elements.
*/
sealed trait Dom { self =>
def encode: CharSequence =
encode(EncodingState.NoIndentation)
def encode(spaces: Int): CharSequence =
encode(EncodingState.Indentation(0, spaces))
private[template] def encode(state: EncodingState, encodeHtml: Boolean = true): CharSequence = self match {
case Dom.Element(name, children) =>
val encode = if (name == "script" || name == "style") false else encodeHtml
val attributes = children.collect {
case self: Dom.Attribute => self.encode
case self: Dom.BooleanAttribute => self.encode
}
val innerState = state.inner
val elements = children.collect {
case self: Dom.Element => self
case self: Dom.Text => self
case self: Dom.Raw => self
}
val noElements = elements.isEmpty
val noAttributes = attributes.isEmpty
val isVoid = Element.isVoid(name)
def inner: CharSequence =
elements match {
case Seq(singleText: Dom.Text) => singleText.encode(innerState, encode)
case _ =>
s"${innerState.nextElemSeparator}${elements.map(_.encode(innerState, encode)).mkString(innerState.nextElemSeparator)}${state.nextElemSeparator}"
}
if (noElements && noAttributes && isVoid) s"<$name/>"
else if (noElements && isVoid)
s"<$name ${attributes.mkString(" ")}/>"
else if (noAttributes)
s"<$name>$inner$name>"
else
s"<$name ${attributes.mkString(" ")}>$inner$name>"
case Dom.Text(data) if encodeHtml => OutputEncoder.encodeHtml(data.toString)
case Dom.Text(data) => data
case Dom.Attribute(name, value) => s"""$name="${OutputEncoder.encodeHtml(value.toString)}""""
case Dom.Empty => ""
case Dom.Raw(raw) => raw
case Dom.BooleanAttribute(name, None) => s"$name"
case Dom.BooleanAttribute(name, Some(value)) => s"""$name="${value}""""
}
}
object Dom {
implicit val schema: Schema[Dom] =
Schema[String].transform(Dom.raw, _.encode.toString)
implicit val htmlCodec: HttpContentCodec[Dom] = {
HttpContentCodec(
ListMap(
MediaType.text.`html` ->
BinaryCodecWithSchema.fromBinaryCodec(TextBinaryCodec.fromSchema(Schema[Dom])),
),
)
}
def attr(name: CharSequence, value: CharSequence): Dom = Dom.Attribute(name, value)
def booleanAttr(name: CharSequence, value: Option[Boolean] = None): Dom = Dom.BooleanAttribute(name, value)
def element(name: CharSequence, children: Dom*): Dom = Dom.Element(name, children)
def empty: Dom = Empty
def text(data: CharSequence): Dom = Dom.Text(data)
def raw(raw: CharSequence): Dom = Dom.Raw(raw)
private[zio] final case class Element(name: CharSequence, children: Seq[Dom]) extends Dom
private[zio] final case class Text(data: CharSequence) extends Dom
private[zio] final case class Raw(raw: CharSequence) extends Dom
private[zio] final case class Attribute(name: CharSequence, value: CharSequence) extends Dom
private[zio] final case class BooleanAttribute(name: CharSequence, value: Option[Boolean] = None) extends Dom
private[zio] object Empty extends Dom
}