Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add streaming pretty printing utilities #567

Merged
merged 11 commits into from
Mar 22, 2024
3 changes: 3 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ lazy val text = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.settings(
name := "fs2-data-text",
description := "Utilities for textual data format",
libraryDependencies ++= List(
"org.typelevel" %%% "cats-collections-core" % "0.9.8"
),
mimaBinaryIssueFilters ++= List(
// private class
ProblemFilters.exclude[IncompatibleResultTypeProblem]("fs2.data.text.CharLikeCharChunks.create"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ object JqLike extends CommandIOApp(name = "fs2-jq", header = "A streaming implem
// execute the compiled query on the input
.through(compiled)
// render the query result
.through(render.pretty())
.through(render.prettyPrint())
// encode the result
.through(fs2.text.utf8.encode[IO])
// and save it to the output
Expand Down
50 changes: 1 addition & 49 deletions json/src/main/scala/fs2/data/json/internal/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@ package data
package json
package internals

import scala.annotation.{switch, tailrec}

private[json] class Renderer(pretty: Boolean, resetOnChunk: Boolean, indent: String)
extends Collector.Builder[Token, String] {

import Renderer._

private val builder = new StringBuilder

private var level = 0
Expand All @@ -41,51 +37,9 @@ private[json] class Renderer(pretty: Boolean, resetOnChunk: Boolean, indent: Str
}

private def jsonString(s: String, key: Boolean): Unit = {
@tailrec
def loop(idx: Int): Unit =
if (idx < s.length) {
val nextEscape = s.indexWhere(c => c > 127 || Character.isISOControl(c) || "\\/\b\f\n\r\t\"".contains(c), idx)
if (nextEscape >= 0) {
if (nextEscape > 0) {
builder.append(s.substring(idx, nextEscape))
}
val c = s(nextEscape)
(c: @switch) match {
case '\\' =>
builder.append("\\\\")
case '/' =>
builder.append("\\/")
case '\b' =>
builder.append("\\b")
case '\f' =>
builder.append("\\f")
case '\n' =>
builder.append("\\n")
case '\r' =>
builder.append("\\r")
case '\t' =>
builder.append("\\t")
case '"' =>
builder.append("\\\"")
case _ =>
// escape non ascii or control characters
builder
.append("\\u")
.append(hex((c >> 12) & 0x0f))
.append(hex((c >> 8) & 0x0f))
.append(hex((c >> 4) & 0x0f))
.append(hex(c & 0x0f))
}
loop(nextEscape + 1)
} else {
// append the rest of the string and we are done
builder.append(s.substring(idx))
}
}

prefixValue()
builder.append('"')
loop(0)
Token.renderString(s, 0, builder)
builder.append('"')

if (key) {
Expand Down Expand Up @@ -172,8 +126,6 @@ private[json] class Renderer(pretty: Boolean, resetOnChunk: Boolean, indent: Str

private[json] object Renderer {

private val hex = "0123456789abcdef"

def pipe[F[_]](pretty: Boolean, indent: String): Pipe[F, Token, String] =
in =>
Stream.suspend(Stream.emit(new Renderer(pretty, true, indent))).flatMap { builder =>
Expand Down
22 changes: 21 additions & 1 deletion json/src/main/scala/fs2/data/json/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import json.ast._
import json.internals._

import cats._
import fs2.data.text.render.Renderable

/** Handles stream parsing and traversing of json documents.
*/
Expand Down Expand Up @@ -153,7 +154,7 @@ package object json {
* You can use this to write the Json stream to a file.
*/
def compact[F[_]]: Pipe[F, Token, String] =
Renderer.pipe[F](false, "")
_.through(fs2.data.text.render.pretty(width = Int.MaxValue)(Token.compact))

/** Renders a pretty-printed representation of the token stream with the given
* indentation size.
Expand All @@ -163,9 +164,26 @@ package object json {
*
* You can use this to write the Json stream to a file.
*/
@deprecated(message = "Consider using `fs2.data.json.render.prettyPrint` instead.", since = "fs2-data 1.11.0")
def pretty[F[_]](indent: String = " "): Pipe[F, Token, String] =
Renderer.pipe[F](true, indent)

/** Renders a pretty-printed representation of the token stream with the given
* indentation size and page width.
*
* Chunks can be concatenated to render all values in the stream,
* separated by new lines.
*
* You can use this to write the Json stream to a file.
*
* You can configure how the stream is rendered by providing an instance of
* [[fs2.data.text.render.Renderable Renderable]] in scope. A default one is automatically
* provided if you do not need specific formatting.
*/
def prettyPrint[F[_]](width: Int = 100, indent: Int = 2)(implicit
renderable: Renderable[Token]): Pipe[F, Token, String] =
_.through(fs2.data.text.render.pretty(width = width, indent = indent))

}

/** Json Token stream collectors. */
Expand All @@ -187,6 +205,8 @@ package object json {
*
* Top-level values are separated by new lines.
*/
@deprecated(message = "Consider using `.through(fs2.data.json.render.prettyPrint()).compile.string` instead.",
since = "fs2-data 1.11.0")
def pretty(indent: String = " "): Collector.Aux[Token, String] =
new Collector[Token] {
type Out = String
Expand Down
Loading
Loading