scalaparsers.Document.scala Maven / Gradle / Ivy
package scalaparsers
import java.io.Writer
import java.io.StringWriter
import cats.{Foldable, Semigroup}
import cats.implicits._
final case class Death(error: Document, base: Exception = null) extends Exception(error.toString, base)
abstract class DocException(msg: String, val doc: Document) extends Exception(msg)
final case object DocNil extends Document
final case object DocNewline extends Document
class DocText(val txt: String) extends Document
object DocText{
def apply(txt:String):DocText = new DocText(if(txt == null) "(null)" else txt)
def unapply(d: DocText) : Option[String] = Some(d.txt)
}
final case class DocBreak(hard: Boolean = true) extends Document
final case class LazyDocText(txtFn: () => String) extends Document
final case class DocGroup(doc: Document) extends Document
final case class DocNest(indent: Int, doc: Document) extends Document
final case class DocCons(hd: Document, tl: Document) extends Document
final case class DocColumn(f: Int => Document) extends Document
final case class DocNesting(f: Int => Document) extends Document
abstract class Document {
import Document._
def ::(hd: Document): Document = DocCons(hd, this)
def ::(hd: String): Document = DocCons(DocText(hd), this)
def :/:(hd: Document): Document = hd :: DocBreak(true) :: this
def :/:(hd: String): Document = hd :: DocBreak(true) :: this
def :/+:(hd: Document): Document = hd :: DocGroup(DocBreak(true)) :: this
def :/+:(hd: String): Document = hd :: DocGroup(DocBreak(true)) :: this
def :\:(hd: Document): Document = hd :: DocBreak(false) :: this
def :\:(hd: String): Document = hd :: DocBreak(false) :: this
def :\+:(hd: Document): Document = hd :: DocGroup(DocBreak(false)) :: this
def :\+:(hd: String): Document = hd :: DocGroup(DocBreak(false)) :: this
def :+:(hd: Document): Document = hd :: DocText(" ") :: this
def :+:(hd: String): Document = hd :: DocText(" ") :: this
def above(xs: Document) = this :: DocNewline :: xs
def align: Document = column(k => nesting (i => nest (k - i, this)))
type FmtState = (Int, Boolean, Document)
/**
* Format this document on writer
and try to set line
* breaks so that the result fits in width
columns.
*
* @param width ...
* @param writer ...
*/
def format(width: Int, writer: Writer): Unit = {
def fits(w: Int, state: List[FmtState]): Boolean = state match {
case _ if w < 0 => false
case List() => true
case (_, _, DocNil) :: z => fits(w, z)
case (i, b, DocCons(h, t)) :: z => fits(w, (i,b,h) :: (i,b,t) :: z)
case (_, _, DocText(t)) :: z => if (t == null) fits(w, z) else fits(w - t.length(), z)
case (_, _, LazyDocText(t)) :: z => if (t == null) fits(w, z) else fits(w - t().length(), z)
case (i, b, DocNest(ii, d)) :: z => fits(w, (i + ii, b, d) :: z)
case (_, false, DocBreak(true)) :: z => fits(w - 1, z)
case (_, false, DocBreak(false)) :: z => fits(w, z)
case (_, true, DocBreak(_)) :: z => true
case (_, _, DocNewline) :: z => true
case (i, true, DocGroup(d)) :: z => fits(w, (i, false, d) :: z) || fits(w, (i, true, d) :: z)
case (i, false, DocGroup(d)) :: z => fits(w, (i, false, d) :: z)
case (i, b, DocColumn(f)) :: z => fits(w, (i, b, f(width - w)) :: z)
case (i, b, DocNesting(f)) :: z => fits(w, (i, b, f(i)) :: z)
}
def fmt(k: Int, state: List[FmtState]): Unit = state match {
case List() => ()
case (_, _, DocNil) :: z => fmt(k, z)
case (i, b, DocCons(h, t)) :: z =>
fmt(k, (i, b, h) :: (i, b, t) :: z)
case (i, _, DocText(t)) :: z =>
writer write t
if (t == null) fmt(k, z) else fmt(k + t.length(), z)
case (i, _, LazyDocText(t)) :: z =>
writer write t()
fmt(k + t().length(), z)
case (i, b, DocNest(ii, d)) :: z =>
fmt(k, (i + ii, b, d) :: z)
case (i, true, DocBreak(_)) :: z =>
writer write "\n"
spaces(writer, i);
fmt(i, z)
case (i, false, DocBreak(true)) :: z =>
writer write " "
fmt(k + 1, z)
case (i, false, DocBreak(false)) :: z => fmt(k, z)
case (i, _, DocNewline) :: z =>
writer write "\n"
spaces(writer, i)
fmt(i, z)
case (i, b, DocGroup(d)) :: z =>
val fitsFlat = fits(width - k, (i, false, d) :: z)
fmt(k, (i, !fitsFlat, d) :: z)
case (i, b, DocColumn(f)) :: z => fmt(k, (i, b, f(k)) :: z)
case (i, b, DocNesting(f)) :: z => fmt(k, (i, b, f(i)) :: z)
}
fmt(0, (0, false, DocGroup(this)) :: Nil)
}
override def toString = {
val w = new StringWriter()
format(80, w)
w.toString
}
}
object Document {
/** The empty document */
def empty = DocNil
def space = DocText(" ")
/** A document which always turns into a new line */
def hardline = DocNewline
/** A break, which will either be turned into a line break or an empty document */
def break = DocBreak(false)
/** A break, which will either be turned into a line break or a space */
def line = DocBreak(true)
def column(f: Int => Document): Document = DocColumn(f)
def nesting(f: Int => Document): Document = DocNesting(f)
/** A document consisting of some text literal */
implicit def text(s: String): Document = DocText(s)
def lazyText(textFn: (() => String)): Document = LazyDocText(textFn)
/**
* A group, whose components will either be printed with all breaks
* rendered as spaces, or with all breaks rendered as line breaks.
*/
def group(d: Document): Document = DocGroup(d)
/** A nested document, which will be indented as specified. */
def nest(i: Int, d: Document): Document = DocNest(i, d)
def ordinal(n: Int, singular: Document, plural: Document) = n match {
case 0 => "no" :+: plural
case 1 => "one" :+: singular
case 2 => "two" :+: plural
case 3 => "three" :+: plural
case 4 => "four" :+: plural
case 5 => "five" :+: plural
case 6 => "six" :+: plural
case n => text(n.toString) :+: plural
}
def Ordinal(n: Int, singular: Document, plural: Document) = n match {
case 0 => "No" :+: plural
case 1 => "One" :+: singular
case 2 => "Two" :+: plural
case 3 => "Three" :+: plural
case 4 => "Four" :+: plural
case 5 => "Five" :+: plural
case 6 => "Six" :+: plural
case n => text(n.toString) :+: plural
}
// separate a list, using an oxford or serial comma if needed
def oxford(andOr : Document, xs: List[Document]) = xs match {
case List() => empty
case List(a) => a
case List(a,b) => a :+: andOr :+: b
case _ => fillSep(punctuate(",", xs.init)) :: "," :+: andOr :+: xs.last
}
def spaces(writer: Writer, n: Int): Unit = {
var rem = n
while (rem >= 16) { writer write " "; rem -= 16 }
if (rem >= 8) { writer write " "; rem -= 8 }
if (rem >= 4) { writer write " "; rem -= 4 }
if (rem >= 2) { writer write " "; rem -= 2}
if (rem == 1) { writer write " " }
}
def fold[F[_]: Foldable](f: (Document, Document) => Document)(z: F[Document]): Document = {
implicit val S = new Semigroup[Document] {
def combine(x: Document, y: Document): Document = f(x, y)
}
z.foldMap(d => Some(d): Option[Document]).getOrElse(empty)
}
def foldl[F[_]: Foldable](f: (Document, Document) => Document)(z: F[Document]): Document =
z.foldLeft(none[Document])((od, d) => od map (f(_, d)) orElse Some(d))
.getOrElse(empty)
def fillSep[F[_]: Foldable](z: F[Document]): Document = fold(_ :/+: _)(z) // fold (>)
def fillSepl[F[_]: Foldable](z: F[Document]): Document = foldl((a,b) => b :: group(line) :: a)(z) // fold (>)
def hsep[F[_]: Foldable](z: F[Document]): Document = fold(_ :+: _)(z) // fold (<+>)
def vsep[F[_]: Foldable](z: F[Document]): Document = fold(_ above _)(z) // fold (<$>)
def vcat[F[_]: Foldable](z: F[Document]): Document = fold(_ :/: _)(z) // fold (<$$>)
def cat(xs: List[Document]) = group(vcat(xs))
def fillCat[F[_]: Foldable](z: F[Document]): Document = fold(_ :/+: _)(z) // fold (/>)
def softline = group(line)
def softbreak = group(break)
def punctuate(sep: Document, xs: List[Document]) : List[Document] = xs match {
case List() => List()
case List(d) => List(d)
case d::ds => (d :: sep) :: punctuate(sep, ds)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy