Skip to content

Commit a1871c4

Browse files
fix: Fixes description printing
1 parent b716c73 commit a1871c4

File tree

6 files changed

+268
-2
lines changed

6 files changed

+268
-2
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Foundation
2+
3+
/**
4+
* Print a block string in the indented block form by adding a leading and
5+
* trailing blank line. However, if a block string starts with whitespace and is
6+
* a single-line, adding a leading blank line would strip that whitespace.
7+
*
8+
* @internal
9+
*/
10+
func printBlockString(
11+
_ value: String,
12+
minimize: Bool = false
13+
) -> String {
14+
let escapedValue = value.replacingOccurrences(of: "\"\"\"", with: "\\\"\"\"")
15+
16+
// Expand a block string's raw value into independent lines.
17+
let lines = splitLines(string: escapedValue)
18+
let isSingleLine = lines.count == 1
19+
20+
// If common indentation is found we can fix some of those cases by adding leading new line
21+
let forceLeadingNewLine =
22+
lines.count > 1 &&
23+
lines[1 ... (lines.count - 1)].allSatisfy { line in
24+
line.count == 0 || isWhiteSpace(line.charCode(at: 0))
25+
}
26+
27+
// Trailing triple quotes just looks confusing but doesn't force trailing new line
28+
let hasTrailingTripleQuotes = escapedValue.hasSuffix("\\\"\"\"")
29+
30+
// Trailing quote (single or double) or slash forces trailing new line
31+
let hasTrailingQuote = value.hasSuffix("\"") && !hasTrailingTripleQuotes
32+
let hasTrailingSlash = value.hasSuffix("\\")
33+
let forceTrailingNewline = hasTrailingQuote || hasTrailingSlash
34+
35+
let printAsMultipleLines =
36+
!minimize &&
37+
// add leading and trailing new lines only if it improves readability
38+
(
39+
!isSingleLine ||
40+
value.count > 70 ||
41+
forceTrailingNewline ||
42+
forceLeadingNewLine ||
43+
hasTrailingTripleQuotes
44+
)
45+
46+
var result = ""
47+
48+
// Format a multi-line block quote to account for leading space.
49+
let skipLeadingNewLine = isSingleLine && isWhiteSpace(value.charCode(at: 0))
50+
if (printAsMultipleLines && !skipLeadingNewLine) || forceLeadingNewLine {
51+
result += "\n"
52+
}
53+
54+
result += escapedValue
55+
if printAsMultipleLines || forceTrailingNewline {
56+
result += "\n"
57+
}
58+
59+
return "\"\"\"" + result + "\"\"\""
60+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* ```
3+
* WhiteSpace ::
4+
* - "Horizontal Tab (U+0009)"
5+
* - "Space (U+0020)"
6+
* ```
7+
* @internal
8+
*/
9+
func isWhiteSpace(_ code: UInt8?) -> Bool {
10+
guard let code = code else {
11+
return false
12+
}
13+
return code == 0x0009 || code == 0x0020
14+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
3+
/**
4+
* Prints a string as a GraphQL StringValue literal. Replaces control characters
5+
* and excluded characters (" U+0022 and \\ U+005C) with escape sequences.
6+
*/
7+
func printString(_ str: String) -> String {
8+
let replacedString = str.unicodeScalars.map { char in
9+
if
10+
char.value <= 0x1F || // \x00-\x1f
11+
char.value == 0x22 || // \x22
12+
char.value == 0x5C || // \x5c
13+
(char.value >= 0x7F && char.value <= 0x9F) // \x7f-\x9f
14+
{
15+
return escapeSequences[Int(char.value)]
16+
}
17+
return String(char)
18+
}.joined()
19+
return "\"\(replacedString)\""
20+
}
21+
22+
let escapeSequences = [
23+
"\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004", "\\u0005", "\\u0006", "\\u0007",
24+
"\\b", "\\t", "\\n", "\\u000B", "\\f", "\\r", "\\u000E", "\\u000F",
25+
"\\u0010", "\\u0011", "\\u0012", "\\u0013", "\\u0014", "\\u0015", "\\u0016", "\\u0017",
26+
"\\u0018", "\\u0019", "\\u001A", "\\u001B", "\\u001C", "\\u001D", "\\u001E", "\\u001F",
27+
"", "", "\\\"", "", "", "", "", "",
28+
"", "", "", "", "", "", "", "", // 2F
29+
"", "", "", "", "", "", "", "",
30+
"", "", "", "", "", "", "", "", // 3F
31+
"", "", "", "", "", "", "", "",
32+
"", "", "", "", "", "", "", "", // 4F
33+
"", "", "", "", "", "", "", "",
34+
"", "", "", "", "\\\\", "", "", "", // 5F
35+
"", "", "", "", "", "", "", "",
36+
"", "", "", "", "", "", "", "", // 6F
37+
"", "", "", "", "", "", "", "",
38+
"", "", "", "", "", "", "", "\\u007F",
39+
"\\u0080", "\\u0081", "\\u0082", "\\u0083", "\\u0084", "\\u0085", "\\u0086", "\\u0087",
40+
"\\u0088", "\\u0089", "\\u008A", "\\u008B", "\\u008C", "\\u008D", "\\u008E", "\\u008F",
41+
"\\u0090", "\\u0091", "\\u0092", "\\u0093", "\\u0094", "\\u0095", "\\u0096", "\\u0097",
42+
"\\u0098", "\\u0099", "\\u009A", "\\u009B", "\\u009C", "\\u009D", "\\u009E", "\\u009F",
43+
]

Sources/GraphQL/Language/Printer.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,7 @@ extension FloatValue: Printable {
141141

142142
extension StringValue: Printable {
143143
var printed: String {
144-
block == true ? value : "\"\(value)\""
145-
// TODO: isBlockString === true ? printBlockString(value) : printString(value),
144+
block == true ? printBlockString(value) : printString(value)
146145
}
147146
}
148147

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
@testable import GraphQL
2+
import XCTest
3+
4+
class PrintBlockStringTests: XCTestCase {
5+
func testDoesNotEscapeCharacters() {
6+
let str = "\" \\ / \n \r \t"
7+
XCTAssertEqual(printBlockString(str), "\"\"\"\n" + str + "\n\"\"\"")
8+
XCTAssertEqual(printBlockString(str, minimize: true), "\"\"\"\n" + str + "\"\"\"")
9+
}
10+
11+
func testByDefaultPrintBlockStringsAsSingleLine() {
12+
XCTAssertEqual(printBlockString("one liner"), "\"\"\"one liner\"\"\"")
13+
}
14+
15+
func testByDefaultPrintBlockStringsEndingWithTripleQuotationAsMultiLine() {
16+
let str = "triple quotation \"\"\""
17+
XCTAssertEqual(printBlockString(str), "\"\"\"\ntriple quotation \\\"\"\"\n\"\"\"")
18+
XCTAssertEqual(
19+
printBlockString(str, minimize: true),
20+
"\"\"\"triple quotation \\\"\"\"\"\"\""
21+
)
22+
}
23+
24+
func testCorrectlyPrintsSingleLineWithLeadingSpace() {
25+
XCTAssertEqual(
26+
printBlockString(" space-led value \"quoted string\""),
27+
"\"\"\" space-led value \"quoted string\"\n\"\"\""
28+
)
29+
}
30+
31+
func testCorrectlyPrintsSingleLineWithTrailingBackslash() {
32+
let str = "backslash \\"
33+
XCTAssertEqual(printBlockString(str), "\"\"\"\nbackslash \\\n\"\"\"")
34+
XCTAssertEqual(printBlockString(str, minimize: true), "\"\"\"backslash \\\n\"\"\"")
35+
}
36+
37+
func testCorrectlyPrintsMultiLineWithInternalIndent() {
38+
let str = "no indent\n with indent"
39+
XCTAssertEqual(printBlockString(str), "\"\"\"\nno indent\n with indent\n\"\"\"")
40+
XCTAssertEqual(
41+
printBlockString(str, minimize: true),
42+
"\"\"\"\nno indent\n with indent\"\"\""
43+
)
44+
}
45+
46+
func testCorrectlyPrintsStringWithAFirstLineIndentation() {
47+
let str = [
48+
" first ",
49+
" line ",
50+
"indentation",
51+
" string",
52+
].joined(separator: "\n")
53+
54+
XCTAssertEqual(
55+
printBlockString(str),
56+
[
57+
"\"\"\"",
58+
" first ",
59+
" line ",
60+
"indentation",
61+
" string",
62+
"\"\"\"",
63+
].joined(separator: "\n")
64+
)
65+
XCTAssertEqual(
66+
printBlockString(str, minimize: true),
67+
[
68+
"\"\"\" first ",
69+
" line ",
70+
"indentation",
71+
" string\"\"\"",
72+
].joined(separator: "\n")
73+
)
74+
}
75+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
@testable import GraphQL
2+
import XCTest
3+
4+
class PrintStringTests: XCTestCase {
5+
func testPrintsASimpleString() {
6+
XCTAssertEqual(printString("hello world"), "\"hello world\"")
7+
}
8+
9+
func testEscapesQutoes() {
10+
XCTAssertEqual(printString("\"hello world\""), "\"\\\"hello world\\\"\"")
11+
}
12+
13+
func testDoesNotEscapeSingleQuote() {
14+
XCTAssertEqual(printString("who's test"), "\"who's test\"")
15+
}
16+
17+
func testEscapesBackslashes() {
18+
XCTAssertEqual(printString("escape: \\"), "\"escape: \\\\\"")
19+
}
20+
21+
func testEscapesWellKnownControlChars() {
22+
XCTAssertEqual(printString("\n\r\t"), "\"\\n\\r\\t\"")
23+
}
24+
25+
func testEscapesZeroByte() {
26+
XCTAssertEqual(printString("\u{0000}"), "\"\\u0000\"")
27+
}
28+
29+
func testDoesNotEscapeSpace() {
30+
XCTAssertEqual(printString(" "), "\" \"")
31+
}
32+
33+
// TODO: We only support UTF8
34+
func testDoesNotEscapeSupplementaryCharacter() {
35+
XCTAssertEqual(printString("\u{1f600}"), "\"\u{1f600}\"")
36+
}
37+
38+
func testEscapesAllControlChars() {
39+
XCTAssertEqual(
40+
printString(
41+
"\u{0000}\u{0001}\u{0002}\u{0003}\u{0004}\u{0005}\u{0006}\u{0007}" +
42+
"\u{0008}\u{0009}\u{000A}\u{000B}\u{000C}\u{000D}\u{000E}\u{000F}" +
43+
"\u{0010}\u{0011}\u{0012}\u{0013}\u{0014}\u{0015}\u{0016}\u{0017}" +
44+
"\u{0018}\u{0019}\u{001A}\u{001B}\u{001C}\u{001D}\u{001E}\u{001F}" +
45+
"\u{0020}\u{0021}\u{0022}\u{0023}\u{0024}\u{0025}\u{0026}\u{0027}" +
46+
"\u{0028}\u{0029}\u{002A}\u{002B}\u{002C}\u{002D}\u{002E}\u{002F}" +
47+
"\u{0030}\u{0031}\u{0032}\u{0033}\u{0034}\u{0035}\u{0036}\u{0037}" +
48+
"\u{0038}\u{0039}\u{003A}\u{003B}\u{003C}\u{003D}\u{003E}\u{003F}" +
49+
"\u{0040}\u{0041}\u{0042}\u{0043}\u{0044}\u{0045}\u{0046}\u{0047}" +
50+
"\u{0048}\u{0049}\u{004A}\u{004B}\u{004C}\u{004D}\u{004E}\u{004F}" +
51+
"\u{0050}\u{0051}\u{0052}\u{0053}\u{0054}\u{0055}\u{0056}\u{0057}" +
52+
"\u{0058}\u{0059}\u{005A}\u{005B}\u{005C}\u{005D}\u{005E}\u{005F}" +
53+
"\u{0060}\u{0061}\u{0062}\u{0063}\u{0064}\u{0065}\u{0066}\u{0067}" +
54+
"\u{0068}\u{0069}\u{006A}\u{006B}\u{006C}\u{006D}\u{006E}\u{006F}" +
55+
"\u{0070}\u{0071}\u{0072}\u{0073}\u{0074}\u{0075}\u{0076}\u{0077}" +
56+
"\u{0078}\u{0079}\u{007A}\u{007B}\u{007C}\u{007D}\u{007E}\u{007F}" +
57+
"\u{0080}\u{0081}\u{0082}\u{0083}\u{0084}\u{0085}\u{0086}\u{0087}" +
58+
"\u{0088}\u{0089}\u{008A}\u{008B}\u{008C}\u{008D}\u{008E}\u{008F}" +
59+
"\u{0090}\u{0091}\u{0092}\u{0093}\u{0094}\u{0095}\u{0096}\u{0097}" +
60+
"\u{0098}\u{0099}\u{009A}\u{009B}\u{009C}\u{009D}\u{009E}\u{009F}"
61+
),
62+
"\"\\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007" +
63+
"\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F" +
64+
"\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017" +
65+
"\\u0018\\u0019\\u001A\\u001B\\u001C\\u001D\\u001E\\u001F" +
66+
" !\\\"#$%&\'()*+,-./0123456789:;<=>?" +
67+
"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_" +
68+
"`abcdefghijklmnopqrstuvwxyz{|}~\\u007F" +
69+
"\\u0080\\u0081\\u0082\\u0083\\u0084\\u0085\\u0086\\u0087" +
70+
"\\u0088\\u0089\\u008A\\u008B\\u008C\\u008D\\u008E\\u008F" +
71+
"\\u0090\\u0091\\u0092\\u0093\\u0094\\u0095\\u0096\\u0097" +
72+
"\\u0098\\u0099\\u009A\\u009B\\u009C\\u009D\\u009E\\u009F\""
73+
)
74+
}
75+
}

0 commit comments

Comments
 (0)