diff --git a/src/main/scala/astrac/boxtables/Config.scala b/src/main/scala/astrac/boxtables/Config.scala index fb70e6e..91303ce 100644 --- a/src/main/scala/astrac/boxtables/Config.scala +++ b/src/main/scala/astrac/boxtables/Config.scala @@ -1,8 +1,6 @@ package astrac.boxtables -case class RowsConfig[Primitive](theme: Theme[Primitive], - sizing: Sizing, - formatter: GenericFormatter[Primitive]) +case class RowsConfig[Primitive](theme: Theme[Primitive], sizing: Sizing) case class TableConfig[Primitive]( main: RowsConfig[Primitive], diff --git a/src/main/scala/astrac/boxtables/GenericAutoRow.scala b/src/main/scala/astrac/boxtables/GenericAutoRow.scala index ba89c46..134178f 100644 --- a/src/main/scala/astrac/boxtables/GenericAutoRow.scala +++ b/src/main/scala/astrac/boxtables/GenericAutoRow.scala @@ -57,4 +57,8 @@ object GenericAutoRow { def derive[Primitive, Model](implicit r: GenericAutoRow[Primitive, Model]) : GenericRow[Primitive, Model] = apply + + def formatted[Primitive, Model](f: GenericFormatter[Primitive])( + implicit r: GenericAutoRow[Primitive, Model]) + : GenericRow[Primitive, Model] = derive[Primitive, Model].format(f) } diff --git a/src/main/scala/astrac/boxtables/GenericCell.scala b/src/main/scala/astrac/boxtables/GenericCell.scala index cd7e429..ea14e4a 100644 --- a/src/main/scala/astrac/boxtables/GenericCell.scala +++ b/src/main/scala/astrac/boxtables/GenericCell.scala @@ -4,6 +4,10 @@ import cats.Contravariant trait GenericCell[Primitive, Model] { def content(a: Model): Primitive + def formatter: GenericFormatter[Primitive] + + def format(f: GenericFormatter[Primitive]): GenericCell[Primitive, Model] = + GenericCell.instance(content, f) } object GenericCell { @@ -12,9 +16,11 @@ object GenericCell { : GenericCell[Primitive, Model] = c def instance[Primitive, Model]( - f: Model => Primitive): GenericCell[Primitive, Model] = + f: Model => Primitive, + fmt: GenericFormatter[Primitive]): GenericCell[Primitive, Model] = new GenericCell[Primitive, Model] { - def content(a: Model) = f(a) + override def content(a: Model) = f(a) + override lazy val formatter = fmt } implicit def contravariant[Primitive] @@ -22,6 +28,6 @@ object GenericCell { new Contravariant[GenericCell[Primitive, ?]] { override def contramap[A, B](fa: GenericCell[Primitive, A])( f: B => A): GenericCell[Primitive, B] = - GenericCell.instance(b => fa.content(f(b))) + GenericCell.instance(b => fa.content(f(b)), fa.formatter) } } diff --git a/src/main/scala/astrac/boxtables/GenericFormatter.scala b/src/main/scala/astrac/boxtables/GenericFormatter.scala index 082f3e0..a37abc7 100644 --- a/src/main/scala/astrac/boxtables/GenericFormatter.scala +++ b/src/main/scala/astrac/boxtables/GenericFormatter.scala @@ -1,6 +1,5 @@ package astrac.boxtables trait GenericFormatter[Primitive] { - def space: Primitive def apply(s: Primitive)(w: Int): List[Primitive] } diff --git a/src/main/scala/astrac/boxtables/GenericRow.scala b/src/main/scala/astrac/boxtables/GenericRow.scala index 30cd3f4..070ad4f 100644 --- a/src/main/scala/astrac/boxtables/GenericRow.scala +++ b/src/main/scala/astrac/boxtables/GenericRow.scala @@ -5,8 +5,14 @@ import cats.syntax.contravariant._ sealed trait GenericRow[Primitive, Model] { def cells: List[GenericCell[Primitive, Model]] - def contents(model: Model): List[Primitive] = cells.map(_.content(model)) lazy val columns: Int = cells.size + + def format(f: GenericFormatter[Primitive]): GenericRow[Primitive, Model] = + GenericRow.instance(cells.map(_.format(f))) + + def formatByIndex( + f: Int => GenericFormatter[Primitive]): GenericRow[Primitive, Model] = + GenericRow.instance(cells.zipWithIndex.map(ci => ci._1.format(f(ci._2)))) } object GenericRow { diff --git a/src/main/scala/astrac/boxtables/algebra/Line.scala b/src/main/scala/astrac/boxtables/algebra/Line.scala index 4943838..79d0ddd 100644 --- a/src/main/scala/astrac/boxtables/algebra/Line.scala +++ b/src/main/scala/astrac/boxtables/algebra/Line.scala @@ -10,24 +10,25 @@ import cats.syntax.traverse._ import Sizing._ trait Line[Primitive, Model] { + implicit val PrimitiveSupport: PrimitiveSupport[Primitive] implicit def Primitive: Monoid[Primitive] implicit def Model: GenericRow[Primitive, Model] - val formatter: Rows[Primitive, GenericFormatter[Primitive]] = Rows.formatter + import PrimitiveSupport._ + val sizing: Rows[Primitive, Sizing] = Rows.sizing val theme: Rows[Primitive, Theme[Primitive]] = Rows.theme def transpose( - ls: List[List[Primitive]]): Rows[Primitive, List[List[Primitive]]] = - formatter.flatMap { f => - val w = ls.map(_.size).max - ls.zipWithIndex - .traverse { - case (l, i) => - cellWidth(i).map(cw => l.padTo(w, Primitive.combineN(f.space, cw))) - } - .map(_.transpose) - } + ls: List[List[Primitive]]): Rows[Primitive, List[List[Primitive]]] = { + val w = ls.map(_.size).max + ls.zipWithIndex + .traverse { + case (l, i) => + cellWidth(i).map(cw => l.padTo(w, Primitive.combineN(space, cw))) + } + .map(_.transpose) + } def boundedSpace(w: Int): Rows[Primitive, Int] = theme.map { t => math.max(0, @@ -118,29 +119,26 @@ trait Line[Primitive, Model] { .flatMap(identity) def contentLine(contents: List[Primitive]): Rows[Primitive, Primitive] = - for { - t <- theme - f <- formatter - l <- line(t.margins.fill.l, - t.borders.l, - t.padding.fill.l, - contents, - t.dividers.v.getOrElse(f.space), - t.padding.fill.r, - t.borders.r, - t.margins.fill.r) - } yield l + theme.flatMap { t => + line(t.margins.fill.l, + t.borders.l, + t.padding.fill.l, + contents, + t.dividers.v.getOrElse(space), + t.padding.fill.r, + t.borders.r, + t.margins.fill.r) + } def paddingLine(p: Primitive): Rows[Primitive, Primitive] = for { t <- theme cs <- fillCells(p) - f <- formatter l <- line(t.margins.fill.l, t.borders.l, p, cs, - t.dividers.v.getOrElse(f.space), + t.dividers.v.getOrElse(space), p, t.borders.r, t.margins.fill.r) diff --git a/src/main/scala/astrac/boxtables/algebra/PrimitiveSupport.scala b/src/main/scala/astrac/boxtables/algebra/PrimitiveSupport.scala new file mode 100644 index 0000000..40fd822 --- /dev/null +++ b/src/main/scala/astrac/boxtables/algebra/PrimitiveSupport.scala @@ -0,0 +1,16 @@ +package astrac.boxtables +package algebra + +trait PrimitiveSupport[Primitive] { + def space: Primitive +} + +object PrimitiveSupport { + def instance[Primitive](s: Primitive): PrimitiveSupport[Primitive] = + new PrimitiveSupport[Primitive] { + override lazy val space = s + } + + implicit val stringSupport: PrimitiveSupport[String] = + instance(" ") +} diff --git a/src/main/scala/astrac/boxtables/algebra/Rows.scala b/src/main/scala/astrac/boxtables/algebra/Rows.scala index 21cec56..d9ad241 100644 --- a/src/main/scala/astrac/boxtables/algebra/Rows.scala +++ b/src/main/scala/astrac/boxtables/algebra/Rows.scala @@ -4,9 +4,6 @@ package algebra import cats.data.Reader object Rows { - def formatter[Primitive]: Rows[Primitive, GenericFormatter[Primitive]] = - Reader(_.formatter) - def sizing[Primitive]: Rows[Primitive, Sizing] = Reader(_.sizing) diff --git a/src/main/scala/astrac/boxtables/algebra/Table.scala b/src/main/scala/astrac/boxtables/algebra/Table.scala index ddba963..37a3048 100644 --- a/src/main/scala/astrac/boxtables/algebra/Table.scala +++ b/src/main/scala/astrac/boxtables/algebra/Table.scala @@ -45,18 +45,15 @@ trait Table[Primitive, Model] extends Line[Primitive, Model] { List.fill(t.padding.space.b)(paddingLine(t.padding.fill.b)).sequence } - def row(model: Model): Rows[Primitive, List[Primitive]] = formatter.flatMap { - f => - val contents = Model.contents(model) - - val contentLines = contents.zipWithIndex - .traverse { - case (c, i) => cellWidth(i).map(f(c)) - } - .flatMap(transpose) - .flatMap(_.traverse(contentLine)) - - paddingTop |+| contentLines |+| paddingBottom + def row(model: Model): Rows[Primitive, List[Primitive]] = { + val contentLines = Model.cells.zipWithIndex + .traverse { + case (c, i) => cellWidth(i).map(c.formatter(c.content(model))) + } + .flatMap(transpose) + .flatMap(_.traverse(contentLine)) + + paddingTop |+| contentLines |+| paddingBottom } lazy val tableStart = topMargin |+| topBorder @@ -78,8 +75,10 @@ trait Table[Primitive, Model] extends Line[Primitive, Model] { object Table { implicit def instance[Primitive, Model]( implicit monoid: Monoid[Primitive], - modelRow: GenericRow[Primitive, Model]): Table[Primitive, Model] = + modelRow: GenericRow[Primitive, Model], + support: PrimitiveSupport[Primitive]): Table[Primitive, Model] = new Table[Primitive, Model] { + implicit val PrimitiveSupport = support implicit val Primitive = monoid implicit val Model = modelRow } diff --git a/src/main/scala/astrac/boxtables/string/Formatter.scala b/src/main/scala/astrac/boxtables/string/Formatter.scala index d6aa77c..fbefc53 100644 --- a/src/main/scala/astrac/boxtables/string/Formatter.scala +++ b/src/main/scala/astrac/boxtables/string/Formatter.scala @@ -5,8 +5,6 @@ trait Formatter extends GenericFormatter[String] object Formatter { val withWordBoundaries: Formatter = new Formatter { - override val space = " " - private def breakLine(w: Int)(s: String) = { val (lines, last) = s .split(" ") @@ -34,7 +32,6 @@ object Formatter { } val basic: Formatter = new Formatter { - override val space = " " override def apply(s: String)(w: Int): List[String] = if (w == 0) Nil else diff --git a/src/main/scala/astrac/boxtables/string/Specialisations.scala b/src/main/scala/astrac/boxtables/string/Specialisations.scala index 12726ad..77fe786 100644 --- a/src/main/scala/astrac/boxtables/string/Specialisations.scala +++ b/src/main/scala/astrac/boxtables/string/Specialisations.scala @@ -3,12 +3,16 @@ package string object Cell { def apply[Model: Cell]: Cell[Model] = implicitly - def instance[Model](f: Model => String): Cell[Model] = GenericCell.instance(f) + def instance[Model](f: Model => String, + formatter: Formatter = Formatter.basic): Cell[Model] = + GenericCell.instance(f, formatter) } object AutoRow { def apply[Model: AutoRow]: Row[Model] = implicitly[AutoRow[Model]].row def derive[Model: AutoRow]: Row[Model] = AutoRow[Model] + def formatted[Model: AutoRow](f: Formatter): Row[Model] = + AutoRow[Model].format(f) } object Row { diff --git a/src/main/scala/astrac/boxtables/string/Tables.scala b/src/main/scala/astrac/boxtables/string/Tables.scala index 852369c..9ed2c6f 100644 --- a/src/main/scala/astrac/boxtables/string/Tables.scala +++ b/src/main/scala/astrac/boxtables/string/Tables.scala @@ -8,13 +8,12 @@ object Tables { def simple[F[_]: Foldable, Model](data: F[Model], sizing: Sizing, - theme: Theme[String], - formatter: Formatter = Formatter.basic)( + theme: Theme[String])( implicit algebra: Table[String, Model] ): String = Templates .simple(data) - .run(TableConfig(RowsConfig(theme, sizing, formatter))) + .run(TableConfig(RowsConfig(theme, sizing))) .mkString("\n") def withHeader[F[_]: Foldable, Header, Model]( @@ -64,9 +63,8 @@ object Tables { withHeader( h, as, - TableConfig[String]( - main = RowsConfig(Themes.markdownMain, sizing, Formatter.basic), - header = - Some(RowsConfig(Themes.markdownHeader, sizing, Formatter.basic))) + TableConfig[String](main = RowsConfig(Themes.markdownMain, sizing), + header = + Some(RowsConfig(Themes.markdownHeader, sizing))) ) } diff --git a/src/test/scala/astrac/boxtables/RowSpec.scala b/src/test/scala/astrac/boxtables/RowSpec.scala index cd34d88..0eedc7f 100644 --- a/src/test/scala/astrac/boxtables/RowSpec.scala +++ b/src/test/scala/astrac/boxtables/RowSpec.scala @@ -20,7 +20,9 @@ class RowSpec extends CatsSuite { case Some(v) => v } .take(equalitySamples) - .forall(e => a.contents(e) == b.contents(e)) + .forall(e => + a.cells.map(_.content(e)) == b.cells.map(_.content(e)) && + a.cells.map(_.formatter) == b.cells.map(_.formatter)) } checkAll("Row.ControvariantMonoidalLaws", diff --git a/src/test/scala/astrac/boxtables/string/TablesSpec.scala b/src/test/scala/astrac/boxtables/string/TablesSpec.scala index d840baa..5e259bd 100644 --- a/src/test/scala/astrac/boxtables/string/TablesSpec.scala +++ b/src/test/scala/astrac/boxtables/string/TablesSpec.scala @@ -140,20 +140,20 @@ class TablesSpec extends Properties("Tables") { User("Billy Pilgrim", 20, false, Counters(5, 7)), User("Mandarax", 3, true, Counters(10, 0)) )) - .run(RowsConfig(testTheme, Sizing.Equal(w), Formatter.basic)) + .run(RowsConfig(testTheme, Sizing.Equal(w))) lines.forall(_.size == w) } property("TableWithWordBoundariesFormatter") = { - implicit val tupleRow: Row[(String, String, String)] = AutoRow.derive + implicit val tupleRow: Row[(String, String, String)] = + AutoRow.formatted(Formatter.withWordBoundaries) val tbl = Tables.simple( List.fill(2)( (Examples.loremIpsumShort, Examples.loremIpsum, Examples.loremIpsum)), Sizing.Weighted(60, List(2, 3, 4)), - Themes.singleLineAscii, - Formatter.withWordBoundaries + Themes.singleLineAscii ) tbl == Examples.tableWordBoundariesFormatter