Skip to content

Commit

Permalink
Doc.toHtmlSnippet based on Html (#2025)
Browse files Browse the repository at this point in the history
* Doc.toHtmlSnippet based on Html

* Fix imports
  • Loading branch information
vigoo authored Mar 13, 2023
1 parent b285fae commit 65e3054
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 133 deletions.
138 changes: 56 additions & 82 deletions zio-http/src/main/scala/zio/http/codec/Doc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package zio.http.codec

import zio.http.html

/**
* A `Doc` models documentation for an endpoint or input.
*/
Expand Down Expand Up @@ -113,93 +115,50 @@ sealed trait Doc { self =>
writer.toString()
}

def toHtmlSnippet: String = {
val writer = new StringBuilder

def renderSpan(span: Span, indent: Int): String = {
def render(s: String): String = (" " * indent) + s

span match {
case Span.Text(value) => render(value.replace("\n", "<br/>"))
case Span.Code(value) => render(s"<code>${value.trim.replace("\n", "<br/>")}</code>")
case Span.Link(value, text) => s"""<a href="${value}">${text.getOrElse(value)}</a>"""
case Span.Bold(value) =>
s"${"<b>"}${renderSpan(value, indent).trim}${"</b>"}"
case Span.Italic(value) =>
s"${"<i>"}${renderSpan(value, indent).trim}${"</i>"}"
case Span.Error(value) =>
s"${s"""<span style="color:red">"""}${render(value).replace("\n", "<br/>")}${"</span>"}"
case Span.Sequence(left, right) =>
renderSpan(left, indent)
renderSpan(right, indent)
}
}

def render(doc: Doc, indent: Int = 0): Unit = {
def append(s: String, indent: Int = indent): Unit = {
writer.append(" " * indent).append(s)
()
}
def newLine(): Unit = append("\n", 0)

doc match {

case Doc.Empty => ()

case Doc.Header(value, level) =>
append(s"<h$level>$value</h$level>\n\n")

case Doc.Paragraph(value) =>
newLine()
append(s"<p>")
newLine()
append(renderSpan(value, 0), indent + 1)
newLine()
append("</p>")
newLine()
def toHtml: html.Html = {
import html._

case Doc.DescriptionList(definitions) =>
append("<dl>")
definitions.foreach { case (span, helpDoc) =>
newLine()
append("<dt>", indent + 1)
newLine()
append(renderSpan(span, indent + 2))
newLine()
append("</dt>", indent + 1)
newLine()
append("<dd>", indent + 1)
render(helpDoc, indent + 2)
append("</dd>", indent + 1)
newLine()
}
append("</dl>")
newLine()
newLine()

case Doc.Listing(elements, listingType) =>
if (listingType == ListingType.Ordered) append("<ol>") else append("<ul>")
newLine()
elements.foreach { doc =>
append("<li>", indent + 1)
render(doc, indent + 2)
append("</li>", indent + 1)
newLine()
self match {
case Doc.Empty =>
Html.Empty
case Header(value, level) =>
level match {
case 1 => h1(value)
case 2 => h2(value)
case 3 => h3(value)
case 4 => h4(value)
case 5 => h5(value)
case 6 => h6(value)
case _ => throw new IllegalArgumentException(s"Invalid header level: $level")
}
case Paragraph(value) =>
p(value.toHtml)
case DescriptionList(definitions) =>
dl(
definitions.flatMap { case (span, helpDoc) =>
Seq(
dt(span.toHtml),
dd(helpDoc.toHtml),
)
},
)
case Sequence(left, right) =>
left.toHtml ++ right.toHtml
case Listing(elements, listingType) =>
val elementsHtml =
elements.map { doc =>
li(doc.toHtml)
}
if (listingType == ListingType.Ordered) append("</ol>") else append("</ul>")
newLine()

case Doc.Sequence(left, right) =>
render(left, indent)
render(right, indent)

}
listingType match {
case ListingType.Unordered => ul(elementsHtml)
case ListingType.Ordered => ol(elementsHtml)
}
}

render(this)
writer.toString()
}

def toHtmlSnippet: String =
toHtml.encode(2).toString

def toPlaintext(columnWidth: Int = 100, color: Boolean = true): String = {
val _ = color

Expand Down Expand Up @@ -385,6 +344,21 @@ object Doc {
case Span.Link(value, _) => value.toString.length
case Span.Sequence(left, right) => left.size + right.size
}

def toHtml: html.Html = {
import html._

self match {
case Span.Text(value) => value
case Span.Code(value) => code(value)
case Span.Error(value) => span(styleAttr := ("color", "red") :: Nil, value)
case Span.Bold(value) => b(value.toHtml)
case Span.Italic(value) => i(value.toHtml)
case Span.Link(value, text) =>
a(href := value.toASCIIString, Html.fromString(text.getOrElse(value.toASCIIString)))
case Span.Sequence(left, right) => left.toHtml ++ right.toHtml
}
}
}
object Span {
final case class Text(value: String) extends Span
Expand Down
31 changes: 24 additions & 7 deletions zio-http/src/main/scala/zio/http/html/Dom.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,40 @@ import zio.stacktracer.TracingImplicits.disableAutoTrace
* elements.
*/
sealed trait Dom { self =>
def encode: CharSequence = self match {
def encode: CharSequence =
encode(EncodingState.NoIndentation)

def encode(spaces: Int): CharSequence =
encode(EncodingState.Indentation(0, spaces))

private[html] def encode(state: EncodingState): CharSequence = self match {
case Dom.Element(name, children) =>
val attributes = children.collect { case self: Dom.Attribute => self.encode }

val elements = children.collect {
case self: Dom.Element => self.encode
case self: Dom.Text => self.encode
val innerState = state.inner
val elements = children.collect {
case self: Dom.Element => self
case self: Dom.Text => 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)
case _ =>
s"${innerState.nextElemSeparator}${elements.map(_.encode(innerState)).mkString(innerState.nextElemSeparator)}${state.nextElemSeparator}"
}

if (noElements && noAttributes && isVoid) s"<$name/>"
else if (noElements && isVoid) s"<$name ${attributes.mkString(" ")}/>"
else if (noAttributes) s"<$name>${elements.mkString("")}</$name>"
else s"<$name ${attributes.mkString(" ")}>${elements.mkString}</$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) => data
case Dom.Attribute(name, value) => s"""$name="$value""""
Expand Down
18 changes: 18 additions & 0 deletions zio-http/src/main/scala/zio/http/html/EncodingState.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package zio.http.html

private[html] sealed trait EncodingState {
def nextElemSeparator: String
def inner: EncodingState
}

object EncodingState {
case object NoIndentation extends EncodingState {
val nextElemSeparator: String = ""
def inner: EncodingState = NoIndentation
}

final case class Indentation(current: Int, spaces: Int) extends EncodingState {
lazy val nextElemSeparator: String = "\n" + (" " * (current * spaces))
def inner: EncodingState = Indentation(current + 1, spaces)
}
}
22 changes: 19 additions & 3 deletions zio-http/src/main/scala/zio/http/html/Html.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,29 @@ import zio.stacktracer.TracingImplicits.disableAutoTrace
* A view is a domain that used generate HTML.
*/
sealed trait Html { self =>
def encode: CharSequence = {
def encode: CharSequence =
encode(EncodingState.NoIndentation)

def encode(spaces: Int): CharSequence =
encode(EncodingState.Indentation(0, spaces))

private[html] def encode(state: EncodingState): CharSequence = {
self match {
case Html.Empty => ""
case Html.Single(element) => element.encode
case Html.Multiple(elements: Seq[Dom]) => elements.map(_.encode).mkString("")
case Html.Single(element) => element.encode(state)
case Html.Multiple(elements: Seq[Dom]) => elements.map(_.encode(state)).mkString(state.nextElemSeparator)
}
}

def ++(that: Html): Html =
(self, that) match {
case (l, Html.Empty) => l
case (Html.Empty, r) => r
case (Html.Single(l), Html.Single(r)) => Html.Multiple(Seq(l, r))
case (Html.Multiple(l), Html.Single(r)) => Html.Multiple(l :+ r)
case (Html.Single(l), Html.Multiple(r)) => Html.Multiple(l +: r)
case (Html.Multiple(l), Html.Multiple(r)) => Html.Multiple(l ++ r)
}
}

object Html {
Expand Down
51 changes: 10 additions & 41 deletions zio-http/src/test/scala/zio/http/endpoint/DocSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,79 +104,48 @@ object DocSpec extends ZIOSpecDefault {
)
).toHtmlSnippet
val expected = """|<h1>Awesome Test!</h1>
|
|
|<p>
| This is a test
|</p>
|<p>This is a test</p>
|<h2>Subsection</h2>
|
|
|<p>
| This is a subsection
|</p>
|<p>This is a subsection</p>
|<h3>Subsubsection</h3>
|
|
|<p>
| This is a subsubsection
|</p>
|
|<p>This is a subsubsection</p>
|<p>
| <a href="https://www.google.com">https://www.google.com</a>
|</p>
|
|<p>
| <span style="color:red">This is an error</span>
|</p>
|
|<p>
| <code>ZIO.succeed(1)</code>
|</p>
|
|<p>
| <b>This is strong</b>
|</p>
|
|<p>
| <i>This is italic</i>
|</p>
|<dl>
| <dt>
| This is a description list item
| </dt>
| <dt>This is a description list item</dt>
| <dd>
| <p>
| This is the description
| </p>
| <p>This is the description</p>
| </dd>
|</dl>
|
|<ol>
| <li>
| <p>
| This is an enumeration item
| </p>
| <p>This is an enumeration item</p>
| </li>
| <li>
| <p>
| This is another enumeration item
| </p>
| <p>This is another enumeration item</p>
| <ul>
| <li>
| <p>
| This is a nested enumeration item
| </p>
| <p>This is a nested enumeration item</p>
| </li>
| <li>
| <p>
| This is another nested enumeration item
| </p>
| <p>This is another nested enumeration item</p>
| </li>
| </ul>
| </li>
|</ol>
|""".stripMargin
|</ol>""".stripMargin
assertTrue(complexDoc == expected)
},
test("plain text rendering") {
Expand Down

0 comments on commit 65e3054

Please sign in to comment.