From bfa1689b45ad7f6d03bb7eff69f11cc7b09667c6 Mon Sep 17 00:00:00 2001 From: Armin Wiebigke Date: Wed, 4 Sep 2024 20:45:08 +0200 Subject: [PATCH 1/3] Add test for rendering comments with line breaks See https://github.com/sangria-graphql/sangria/issues/1125 --- .../sangria/renderer/QueryRendererSpec.scala | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) 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)) + } } } } From daa7781e4e62cba416dba5b7442f0e30d0ca2c8d Mon Sep 17 00:00:00 2001 From: Armin Wiebigke Date: Wed, 4 Sep 2024 20:45:40 +0200 Subject: [PATCH 2/3] Correctly handle line breaks in QueryRenderer --- .../core/src/main/scala/sangria/renderer/QueryRenderer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala b/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala index 65b06429..b12e86e7 100644 --- a/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala +++ b/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala @@ -477,7 +477,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 = escapeBlockString(node.value).linesIterator.map { line => if (line.isEmpty) line // do not output lines with only whitespaces inside else ind + line } From 24be4fba1e18a68d3c66ec83a3c9a3ec6d08b928 Mon Sep 17 00:00:00 2001 From: Armin Wiebigke Date: Wed, 4 Sep 2024 20:46:46 +0200 Subject: [PATCH 3/3] Add linesIterator implementation `linesIterator` from Scala is not consistent for our cross-compiled versions, so we provide an explicit implementation (copied from Scala 2.13.14 StringOps.scala). --- .../sangria/renderer/QueryRenderer.scala | 5 ++-- .../main/scala/sangria/util/StringUtil.scala | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala b/modules/core/src/main/scala/sangria/renderer/QueryRenderer.scala index b12e86e7..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).linesIterator.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. *