Skip to content

Commit

Permalink
Add md interpolator to create Markdown based docs
Browse files Browse the repository at this point in the history
Also distinguish between inline code and code block docs
  • Loading branch information
987Nabil committed Sep 20, 2023
1 parent 8780eb9 commit 0e3b1c2
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 32 deletions.
26 changes: 26 additions & 0 deletions zio-http/src/main/scala-2/zio/http/MdInterpolator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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

import zio.http.codec.Doc

trait MdInterpolator {

implicit class MdInterpolatorHelper(val sc: StringContext) {
def md(args: Any*): Doc = Doc.fromCommonMark(sc.s(args: _*).stripMargin)
}
}
27 changes: 27 additions & 0 deletions zio-http/src/main/scala-3/zio/http/MdInterpolator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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

import zio.http.codec.Doc

trait MdInterpolator {

extension(inline sc: StringContext) {
inline def md(inline args: Any*): Doc = Doc.fromCommonMark(sc.s(args*).stripMargin)
}

}
94 changes: 69 additions & 25 deletions zio-http/src/main/scala/zio/http/codec/Doc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@

package zio.http.codec

import zio.stacktracer.TracingImplicits.disableAutoTrace

import zio.http.codec.Doc.Span.CodeStyle
import zio.http.template

/**
Expand All @@ -39,6 +38,7 @@ sealed trait Doc { self =>
case Doc.DescriptionList(xs) => xs.forall(_._2.isEmpty)
case Doc.Sequence(left, right) => left.isEmpty && right.isEmpty
case Doc.Listing(xs, _) => xs.forall(_.isEmpty)
case Doc.Raw(value, _) => value.isEmpty
case _ => false
}

Expand All @@ -49,18 +49,20 @@ sealed trait Doc { self =>
def render(s: String): String = " " * indent + s

span match {
case Span.Text(value) => render(value)
case Span.Code(value) => render(s"```${value.trim}```")
case Span.Link(value, text) => render(s"[${text.getOrElse(value)}]($value)")
case Span.Bold(value) =>
case Span.Text(value) => render(value)
case Span.Code(value, CodeStyle.Block) => render(s"```${value.trim}\n```")
case Span.Code(value, CodeStyle.Inline) => render(s"`$value`")
case Span.Link(value, text) => render(s"[${text.getOrElse(value)}]($value)")
case Span.Bold(value) =>
s"${render("**")}${renderSpan(value, indent).trim}${render("**")}"
case Span.Italic(value) =>
case Span.Italic(value) =>
s"${render("*")}${renderSpan(value, indent).trim}${render("*")}"
case Span.Error(value) =>
case Span.Error(value) =>
s"${render(s"""<span style="color:red">""")}${render(value)}${render("</span>")}"
case Span.Sequence(left, right) =>
renderSpan(left, indent)
renderSpan(right, indent)
case Span.Sequence(left, right) =>
val l = renderSpan(left, indent)
val r = renderSpan(right, indent)
s"$l$r"
}
}

Expand All @@ -75,6 +77,7 @@ sealed trait Doc { self =>
case Doc.Empty => ()

case Doc.Header(value, level) =>
if (writer.nonEmpty && writer.last != '\n') append("\n")
append(s"${"#" * level} $value\n\n")

case Doc.Paragraph(value) =>
Expand All @@ -94,8 +97,8 @@ sealed trait Doc { self =>
if (listingType == ListingType.Ordered) append(s"${i + 1}. ") else append("- ")
doc match {
case Listing(_, _) =>
writer.append("\n")
render(doc, indent + 1)
writer.append("\n")
case Sequence(left, right) =>
render(left, indent)
writer.deleteCharAt(writer.length - 1)
Expand All @@ -110,6 +113,12 @@ sealed trait Doc { self =>
render(left, indent)
render(right, indent)

case Doc.Raw(value, RawDocType.CommonMark) =>
writer.append(value)

case Doc.Raw(_, docType) =>
throw new IllegalArgumentException(s"Unsupported raw doc type: $docType")

}
}

Expand Down Expand Up @@ -155,6 +164,11 @@ sealed trait Doc { self =>
case ListingType.Unordered => ul(elementsHtml)
case ListingType.Ordered => ol(elementsHtml)
}

case Raw(value, RawDocType.Html) =>
Html.fromString(value)
case Raw(_, docType) =>
throw new IllegalArgumentException(s"Unsupported raw doc type: $docType")
}
}

Expand Down Expand Up @@ -243,6 +257,12 @@ sealed trait Doc { self =>
case Doc.Sequence(left, right) =>
renderHelpDoc(left)
renderHelpDoc(right)

case Doc.Raw(value, RawDocType.Plain) =>
writer.append(value)

case Doc.Raw(_, docType) =>
throw new IllegalArgumentException(s"Unsupported raw doc type: $docType")
}

def renderSpan(span: Span): Unit = {
Expand All @@ -256,7 +276,7 @@ sealed trait Doc { self =>

writer.append(if (uppercase) value.toUpperCase() else value)

case Span.Code(value) =>
case Span.Code(value, _) =>
setStyle(Console.WHITE)
writer.append(value)
resetStyle()
Expand Down Expand Up @@ -294,7 +314,19 @@ sealed trait Doc { self =>

}
object Doc {

def fromCommonMark(commonMark: String): Doc =
Doc.Raw(commonMark, RawDocType.CommonMark)

private sealed trait RawDocType
private object RawDocType {
case object Plain extends RawDocType
case object CommonMark extends RawDocType
case object Html extends RawDocType
}

case object Empty extends Doc
private final case class Raw(value: String, docType: RawDocType) extends Doc
final case class Header(value: String, level: Int) extends Doc
final case class Paragraph(value: Span) extends Doc
final case class DescriptionList(definitions: List[(Span, Doc)]) extends Doc
Expand Down Expand Up @@ -327,19 +359,23 @@ object Doc {
def h3(t: String): Doc = Header(t, 3)
def h4(t: String): Doc = Header(t, 4)
def h5(t: String): Doc = Header(t, 5)
def h6(t: String): Doc = Header(t, 6)

def p(t: String): Doc = Doc.Paragraph(Span.text(t))
def p(span: Span): Doc = Doc.Paragraph(span)

sealed trait Span { self =>
final def +(that: Span): Span = Span.Sequence(self, that)
final def +(that: Span): Span =
if (self.isEmpty) that
else if (that.isEmpty) self
else Span.Sequence(self, that)

final def isEmpty: Boolean = self.size == 0

final def size: Int =
self match {
case Span.Text(value) => value.length
case Span.Code(value) => value.length
case Span.Code(value, _) => value.length
case Span.Error(value) => value.length
case Span.Bold(value) => value.size
case Span.Italic(value) => value.size
Expand All @@ -351,27 +387,35 @@ object Doc {
import template._

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) =>
case Span.Text(value) => value
case Span.Code(value, CodeStyle.Block) => pre(code(value))
case Span.Code(value, CodeStyle.Inline) => 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
case Span.Sequence(left, right) => left.toHtml ++ right.toHtml
}
}
}
object Span {
final case class Text(value: String) extends Span
final case class Code(value: String) extends Span
final case class Code(value: String, codeStyle: CodeStyle) extends Span
final case class Error(value: String) extends Span
final case class Bold(value: Span) extends Span
final case class Italic(value: Span) extends Span
final case class Link(value: java.net.URI, text: Option[String]) extends Span
final case class Sequence(left: Span, right: Span) extends Span

def code(t: String): Span = Span.Code(t)
sealed trait CodeStyle
object CodeStyle {
case object Inline extends CodeStyle
case object Block extends CodeStyle
}

def code(t: String): Span = Span.Code(t, CodeStyle.Inline)
def codeBlock(t: String): Span = Span.Code(t, CodeStyle.Block)
def empty: Span = Span.text("")
def error(t: String): Span = Span.Error(t)
def bold(span: Span): Span = Span.Bold(span)
Expand All @@ -380,7 +424,7 @@ object Doc {
def italic(t: String): Span = Span.Italic(text(t))
def text(t: String): Span = Span.Text(t)
def link(uri: java.net.URI): Span = Span.Link(uri, None)
def link(uri: java.net.URI, text: String): Span = Span.Link(uri, Some(text))
def link(uri: java.net.URI, text: String): Span = Span.Link(uri, Some(text).filter(_.nonEmpty))
def spans(span: Span, spans0: Span*): Span = spans(span :: spans0.toList)
def spans(spans: Iterable[Span]): Span = spans.toList.foldLeft(empty)(_ + _)

Expand Down
2 changes: 1 addition & 1 deletion zio-http/src/main/scala/zio/http/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import java.util.UUID

import zio.http.codec.PathCodec

package object http extends UrlInterpolator {
package object http extends UrlInterpolator with MdInterpolator {

/**
* A smart constructor that attempts to construct a handler from the specified
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
* limitations under the License.
*/

package zio.http.endpoint
package zio.http.codec

import zio.test._

import zio.http.ZIOHttpSpec
import zio.http.codec.Doc
import zio.http._
import zio.http.codec.Doc._

object DocSpec extends ZIOHttpSpec {
Expand Down Expand Up @@ -65,7 +64,7 @@ object DocSpec extends ZIOHttpSpec {
|
|<span style="color:red">This is an error</span>
|
|```ZIO.succeed(1)```
|`ZIO.succeed(1)`
|
|**This is strong**
|
Expand Down Expand Up @@ -208,5 +207,17 @@ object DocSpec extends ZIOHttpSpec {
|""".stripMargin
assertTrue(complexDoc == expected)
},
test("md interpolator") {

val name = "John"
val age = 42
val doc = md"""# Hello $name!
|
|You are $age years old."""
val expected = """# Hello John!
|
|You are 42 years old.""".stripMargin
assertTrue(doc.toCommonMark == expected)
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ object RichTextCodecSpec extends ZIOHttpSpec {

def textOf(doc: Doc): Option[String] =
doc match {
case Doc.Paragraph(Doc.Span.Code(text)) => Some(text)
case _ => None
case Doc.Paragraph(Doc.Span.Code(text, _)) => Some(text)
case _ => None
}

override def spec = suite("Rich Text Codec Spec")(
Expand Down

0 comments on commit 0e3b1c2

Please sign in to comment.