diff --git a/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala b/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala index 65b06429..9967daed 100644 --- a/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala +++ b/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala @@ -1,8 +1,7 @@ package sangria.renderer -import sangria.ast.AstLocation -import sangria.util.StringUtil.{escapeBlockString, escapeString} import sangria.ast._ +import sangria.util.StringUtil.{escapeBlockString, escapeString, linesIterator} object QueryRenderer { val Pretty: QueryRendererConfig = QueryRendererConfig( @@ -477,7 +476,7 @@ object QueryRenderer { extraIndent: Boolean = true): String = if (node.value.trim.nonEmpty) { val ind = if (extraIndent) indent.incForce.str else indent.strForce - val lines = escapeBlockString(node.value).split("\n").iterator.map { line => + val lines = linesIterator(escapeBlockString(node.value)).map { line => if (line.isEmpty) line // do not output lines with only whitespaces inside else ind + line } diff --git a/modules/core/src/main/scala/sangria/util/StringUtil.scala b/modules/core/src/main/scala/sangria/util/StringUtil.scala index 626b894f..2c6040b6 100644 --- a/modules/core/src/main/scala/sangria/util/StringUtil.scala +++ b/modules/core/src/main/scala/sangria/util/StringUtil.scala @@ -3,6 +3,7 @@ package sangria.util import sangria.since3_0_0 import java.util.Locale +import scala.collection.AbstractIterator import scala.collection.mutable.ListBuffer object StringUtil { @@ -110,6 +111,30 @@ object StringUtil { def charHex(ch: Char): String = Integer.toHexString(ch).toUpperCase(Locale.ENGLISH) + // Redefine `linesIterator`, since the implementation provided by Scala is not consistent for our + // cross-compiled versions + def linesIterator(string: String): Iterator[String] = new AbstractIterator[String] { + def hasNext: Boolean = !done + def next(): String = if (done) Iterator.empty.next() else advance() + + private def isLineBreak(c: Char) = c == '\r' || c == '\n' + private def isWindowsLineBreak(c1: Char, c2: Char) = c1 == '\r' && c2 == '\n' + private[this] val len = string.length + private[this] var index = 0 + @`inline` private def done: Boolean = index >= len + private def advance(): String = { + val start = index + while (!done && !isLineBreak(string.charAt(index))) index += 1 + val end = index + if (!done) { + val c = string.charAt(index) + index += 1 + if (!done && isWindowsLineBreak(c, string.charAt(index))) index += 1 + } + string.substring(start, end) + } + } + /** Produces the value of a block string from its parsed raw value, similar to Coffeescript's * block string, Python's docstring trim or Ruby's strip_heredoc. * diff --git a/modules/core/src/test/scala/sangria/renderer/QueryRendererSpec.scala b/modules/core/src/test/scala/sangria/renderer/QueryRendererSpec.scala index 7d234a7c..198d560e 100644 --- a/modules/core/src/test/scala/sangria/renderer/QueryRendererSpec.scala +++ b/modules/core/src/test/scala/sangria/renderer/QueryRendererSpec.scala @@ -987,6 +987,53 @@ class QueryRendererSpec extends AnyWordSpec with Matchers with StringMatchers { | # se |}""".stripMargin)(after.being(strippedOfCarriageReturns)) } + + "renders comments with various line separators correctly" in { + val simpleType = sangria.schema.ObjectType[Unit, Unit]( + name = "SimpleType", + description = "A simple object type.", + fields = sangria.schema.fields[Unit, Unit]( + sangria.schema.Field( + name = "value", + description = Some( + "A field with a multi-line description that contains different line breaks.\n" + + "LF:\n\n" + + "CRLF:\r\n\r\n" + + "CR:\r\r" + + "Empty lines above should have no indentation when rendered."), + fieldType = sangria.schema.StringType, + resolve = _ => "theValue" + ) + ) + ) + + val compactRendered = + QueryRenderer.render(simpleType.toAst, QueryRenderer.Compact) + val prettyRendered = + QueryRenderer.render(simpleType.toAst, QueryRenderer.Pretty) + + compactRendered should equal( + """"A simple object type." type SimpleType {"A field with a multi-line description that contains different line breaks.\nLF:\n\nCRLF:\r\n\r\nCR:\r\rEmpty lines above should have no indentation when rendered." value:String!}""") + + prettyRendered.replace(" ", "_") should equal( + Seq( + "\"A simple object type.\"", + "type SimpleType {", + " \"\"\"", + " A field with a multi-line description that contains different line breaks.", + " LF:", + "", + " CRLF:", + "", + " CR:", + "", + " Empty lines above should have no indentation when rendered.", + " \"\"\"", + " value: String!", + "}" + ).mkString("\n").replace(" ", "_") + )(after.being(strippedOfCarriageReturns)) + } } } }