diff --git a/src/language/__tests__/blockString-test.js b/src/language/__tests__/blockString-test.js new file mode 100644 index 0000000000..69d1530807 --- /dev/null +++ b/src/language/__tests__/blockString-test.js @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { dedentBlockStringValue, printBlockString } from '../blockString'; + +function joinLines(...args) { + return args.join('\n'); +} + +describe('dedentBlockStringValue', () => { + it('removes uniform indentation from a string', () => { + const rawValue = joinLines( + '', + ' Hello,', + ' World!', + '', + ' Yours,', + ' GraphQL.', + ); + expect(dedentBlockStringValue(rawValue)).to.equal( + joinLines('Hello,', ' World!', '', 'Yours,', ' GraphQL.'), + ); + }); + + it('removes empty leading and trailing lines', () => { + const rawValue = joinLines( + '', + '', + ' Hello,', + ' World!', + '', + ' Yours,', + ' GraphQL.', + '', + '', + ); + expect(dedentBlockStringValue(rawValue)).to.equal( + joinLines('Hello,', ' World!', '', 'Yours,', ' GraphQL.'), + ); + }); + + it('removes blank leading and trailing lines', () => { + const rawValue = joinLines( + ' ', + ' ', + ' Hello,', + ' World!', + '', + ' Yours,', + ' GraphQL.', + ' ', + ' ', + ); + expect(dedentBlockStringValue(rawValue)).to.equal( + joinLines('Hello,', ' World!', '', 'Yours,', ' GraphQL.'), + ); + }); + + it('retains indentation from first line', () => { + const rawValue = joinLines( + ' Hello,', + ' World!', + '', + ' Yours,', + ' GraphQL.', + ); + expect(dedentBlockStringValue(rawValue)).to.equal( + joinLines(' Hello,', ' World!', '', 'Yours,', ' GraphQL.'), + ); + }); + + it('does not alter trailing spaces', () => { + const rawValue = joinLines( + ' ', + ' Hello, ', + ' World! ', + ' ', + ' Yours, ', + ' GraphQL. ', + ' ', + ); + expect(dedentBlockStringValue(rawValue)).to.equal( + joinLines( + 'Hello, ', + ' World! ', + ' ', + 'Yours, ', + ' GraphQL. ', + ), + ); + }); +}); + +describe('printBlockString', () => { + it('by default print block strings as single line', () => { + const str = 'one liner'; + expect(printBlockString(str)).to.equal('"""one liner"""'); + expect(printBlockString(str, '', true)).to.equal('"""\none liner\n"""'); + }); + + it('correctly prints single-line with leading space', () => { + const str = ' space-led string'; + expect(printBlockString(str)).to.equal('""" space-led string"""'); + expect(printBlockString(str, '', true)).to.equal( + '""" space-led string\n"""', + ); + }); + + it('correctly prints single-line with leading space and quotation', () => { + const str = ' space-led value "quoted string"'; + + expect(printBlockString(str)).to.equal( + '""" space-led value "quoted string"\n"""', + ); + + expect(printBlockString(str, '', true)).to.equal( + '""" space-led value "quoted string"\n"""', + ); + }); + + it('correctly prints string with a first line indentation', () => { + const str = joinLines( + ' first ', + ' line ', + 'indentation', + ' string', + ); + + expect(printBlockString(str)).to.equal( + joinLines( + '"""', + ' first ', + ' line ', + 'indentation', + ' string', + '"""', + ), + ); + }); +}); diff --git a/src/language/__tests__/blockStringValue-test.js b/src/language/__tests__/blockStringValue-test.js deleted file mode 100644 index 90a6d7fdf1..0000000000 --- a/src/language/__tests__/blockStringValue-test.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict - */ - -import { expect } from 'chai'; -import { describe, it } from 'mocha'; -import blockStringValue from '../blockStringValue'; - -describe('blockStringValue', () => { - it('removes uniform indentation from a string', () => { - const rawValue = [ - '', - ' Hello,', - ' World!', - '', - ' Yours,', - ' GraphQL.', - ].join('\n'); - expect(blockStringValue(rawValue)).to.equal( - ['Hello,', ' World!', '', 'Yours,', ' GraphQL.'].join('\n'), - ); - }); - - it('removes empty leading and trailing lines', () => { - const rawValue = [ - '', - '', - ' Hello,', - ' World!', - '', - ' Yours,', - ' GraphQL.', - '', - '', - ].join('\n'); - expect(blockStringValue(rawValue)).to.equal( - ['Hello,', ' World!', '', 'Yours,', ' GraphQL.'].join('\n'), - ); - }); - - it('removes blank leading and trailing lines', () => { - const rawValue = [ - ' ', - ' ', - ' Hello,', - ' World!', - '', - ' Yours,', - ' GraphQL.', - ' ', - ' ', - ].join('\n'); - expect(blockStringValue(rawValue)).to.equal( - ['Hello,', ' World!', '', 'Yours,', ' GraphQL.'].join('\n'), - ); - }); - - it('retains indentation from first line', () => { - const rawValue = [ - ' Hello,', - ' World!', - '', - ' Yours,', - ' GraphQL.', - ].join('\n'); - expect(blockStringValue(rawValue)).to.equal( - [' Hello,', ' World!', '', 'Yours,', ' GraphQL.'].join('\n'), - ); - }); - - it('does not alter trailing spaces', () => { - const rawValue = [ - ' ', - ' Hello, ', - ' World! ', - ' ', - ' Yours, ', - ' GraphQL. ', - ' ', - ].join('\n'); - expect(blockStringValue(rawValue)).to.equal( - [ - 'Hello, ', - ' World! ', - ' ', - 'Yours, ', - ' GraphQL. ', - ].join('\n'), - ); - }); -}); diff --git a/src/language/__tests__/printer-test.js b/src/language/__tests__/printer-test.js index 0bd029fe88..d4ec81f78a 100644 --- a/src/language/__tests__/printer-test.js +++ b/src/language/__tests__/printer-test.js @@ -98,55 +98,6 @@ describe('Printer: Query document', () => { `); }); - describe('block string', () => { - it('correctly prints single-line with leading space', () => { - const mutationASTWithArtifacts = parse( - '{ field(arg: """ space-led value""") }', - ); - expect(print(mutationASTWithArtifacts)).to.equal(dedent` - { - field(arg: """ space-led value""") - } - `); - }); - - it('correctly prints string with a first line indentation', () => { - const mutationASTWithArtifacts = parse(` - { - field(arg: """ - first - line - indentation - """) - } - `); - expect(print(mutationASTWithArtifacts)).to.equal(dedent` - { - field(arg: """ - first - line - indentation - """) - } - `); - }); - - it('correctly prints single-line with leading space and quotation', () => { - const mutationASTWithArtifacts = parse(` - { - field(arg: """ space-led value "quoted string" - """) - } - `); - expect(print(mutationASTWithArtifacts)).to.equal(dedent` - { - field(arg: """ space-led value "quoted string" - """) - } - `); - }); - }); - it('Experimental: correctly prints fragment defined variables', () => { const fragmentWithVariable = parse( ` diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index 1036b9dea9..4a41b7fc87 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -54,18 +54,12 @@ describe('Printer: SDL document', () => { type Foo implements Bar & Baz { "Description of the \`one\` field." one: Type - """ - This is a description of the \`two\` field. - """ + """This is a description of the \`two\` field.""" two( - """ - This is a description of the \`argument\` argument. - """ + """This is a description of the \`argument\` argument.""" argument: InputType! ): Type - """ - This is a description of the \`three\` field. - """ + """This is a description of the \`three\` field.""" three(argument: InputType, other: String): Int four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String @@ -121,13 +115,9 @@ describe('Printer: SDL document', () => { extend scalar CustomScalar @onScalar enum Site { - """ - This is a description of the \`DESKTOP\` value - """ + """This is a description of the \`DESKTOP\` value""" DESKTOP - """ - This is a description of the \`MOBILE\` value - """ + """This is a description of the \`MOBILE\` value""" MOBILE "This is a description of the \`WEB\` value" WEB @@ -163,9 +153,7 @@ describe('Printer: SDL document', () => { extend input InputType @onInputObject - """ - This is a description of the \`@skip\` directive - """ + """This is a description of the \`@skip\` directive""" directive @skip(if: Boolean! @onArgumentDefinition) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/src/language/blockStringValue.js b/src/language/blockString.js similarity index 59% rename from src/language/blockStringValue.js rename to src/language/blockString.js index 5b90bc0531..c5ef1a92a7 100644 --- a/src/language/blockStringValue.js +++ b/src/language/blockString.js @@ -13,7 +13,7 @@ * * This implements the GraphQL spec's BlockStringValue() static algorithm. */ -export default function blockStringValue(rawString: string): string { +export function dedentBlockStringValue(rawString: string): string { // Expand a block string's raw value into independent lines. const lines = rawString.split(/\r\n|[\n\r]/g); @@ -62,3 +62,32 @@ function leadingWhitespace(str) { function isBlank(str) { return leadingWhitespace(str) === str.length; } + +/** + * Print a block string in the indented block form by adding a leading and + * trailing blank line. However, if a block string starts with whitespace and is + * a single-line, adding a leading blank line would strip that whitespace. + */ +export function printBlockString( + value: string, + indentation?: string = '', + preferMultipleLines?: ?boolean = false, +): string { + const isSingleLine = value.indexOf('\n') === -1; + const hasLeadingSpace = value[0] === ' ' || value[0] === '\t'; + const hasTrailingQuote = value[value.length - 1] === '"'; + const printAsMultipleLines = + !isSingleLine || hasTrailingQuote || preferMultipleLines; + + let result = ''; + // Format a multi-line block quote to account for leading space. + if (printAsMultipleLines && !(isSingleLine && hasLeadingSpace)) { + result += '\n' + indentation; + } + result += indentation ? value.replace(/\n/g, '\n' + indentation) : value; + if (printAsMultipleLines) { + result += '\n'; + } + + return '"""' + result.replace(/"""/g, '\\"""') + '"""'; +} diff --git a/src/language/lexer.js b/src/language/lexer.js index 9d6c2044c1..fbc288c856 100644 --- a/src/language/lexer.js +++ b/src/language/lexer.js @@ -11,7 +11,7 @@ import defineToJSON from '../jsutils/defineToJSON'; import type { Token } from './ast'; import type { Source } from './source'; import { syntaxError } from '../error'; -import blockStringValue from './blockStringValue'; +import { dedentBlockStringValue } from './blockString'; /** * Given a Source object, this returns a Lexer for that source. @@ -650,7 +650,7 @@ function readBlockString(source, start, line, col, prev, lexer): Token { line, col, prev, - blockStringValue(rawValue), + dedentBlockStringValue(rawValue), ); } diff --git a/src/language/printer.js b/src/language/printer.js index f3c1fc5ae6..bda4d0c310 100644 --- a/src/language/printer.js +++ b/src/language/printer.js @@ -9,6 +9,7 @@ import type { ASTNode } from './ast'; import { visit } from './visitor'; +import { printBlockString } from './blockString'; /** * Converts an AST into a string, using one set of reasonable @@ -90,7 +91,7 @@ const printDocASTReducer: any = { FloatValue: ({ value }) => value, StringValue: ({ value, block: isBlockString }, key) => isBlockString - ? printBlockString(value, key === 'description') + ? printBlockString(value, key === 'description' ? '' : ' ') : JSON.stringify(value), BooleanValue: ({ value }) => (value ? 'true' : 'false'), NullValue: () => 'null', @@ -273,15 +274,3 @@ function isMultiline(string) { function hasMultilineItems(maybeArray) { return maybeArray && maybeArray.some(isMultiline); } - -/** - * Print a block string in the indented block form by adding a leading and - * trailing blank line. However, if a block string starts with whitespace and is - * a single-line, adding a leading blank line would strip that whitespace. - */ -function printBlockString(value, isDescription) { - const escaped = value.replace(/"""/g, '\\"""'); - return isMultiline(value) || (value[0] !== ' ' && value[0] !== '\t') - ? `"""\n${isDescription ? escaped : indent(escaped)}\n"""` - : `"""${escaped.replace(/"$/, '"\n')}"""`; -} diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 371a858f96..eb450c81ca 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -494,45 +494,6 @@ describe('Type System Printer', () => { expect(recreatedField).to.include({ description }); }); - it('Does not one-line print a description that ends with a quote', () => { - const description = 'This field is "awesome"'; - const output = printSingleFieldSchema({ - type: GraphQLString, - description, - }); - expect(output).to.equal(dedent` - type Query { - """ - This field is "awesome" - """ - singleField: String - } - `); - const schema = buildSchema(output); - const recreatedRoot = assertObjectType(schema.getTypeMap().Query); - const recreatedField = recreatedRoot.getFields().singleField; - expect(recreatedField).to.include({ description }); - }); - - it('Preserves leading spaces when printing a description', () => { - const description = ' This field is "awesome"'; - const output = printSingleFieldSchema({ - type: GraphQLString, - description, - }); - expect(output).to.equal(dedent` - type Query { - """ This field is "awesome" - """ - singleField: String - } - `); - const schema = buildSchema(output); - const recreatedRoot = assertObjectType(schema.getTypeMap().Query); - const recreatedField = recreatedRoot.getFields().singleField; - expect(recreatedField).to.include({ description }); - }); - it('Print Introspection Schema', () => { const Schema = new GraphQLSchema({}); const output = printIntrospectionSchema(Schema); diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 73f83cf203..9b336756f7 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -15,7 +15,7 @@ import keyValMap from '../jsutils/keyValMap'; import type { ObjMap } from '../jsutils/ObjMap'; import { valueFromAST } from './valueFromAST'; import { assertValidSDL } from '../validation/validate'; -import blockStringValue from '../language/blockStringValue'; +import { dedentBlockStringValue } from '../language/blockString'; import { TokenKind } from '../language/lexer'; import { parse } from '../language/parser'; import type { ParseOptions } from '../language/parser'; @@ -457,7 +457,7 @@ export function getDescription( if (options && options.commentDescriptions) { const rawValue = getLeadingCommentBlock(node); if (rawValue !== undefined) { - return blockStringValue('\n' + rawValue); + return dedentBlockStringValue('\n' + rawValue); } } } diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index 8f32e896f5..835940fce7 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -12,6 +12,7 @@ import objectValues from '../polyfills/objectValues'; import inspect from '../jsutils/inspect'; import { astFromValue } from '../utilities/astFromValue'; import { print } from '../language/printer'; +import { printBlockString } from '../language/blockString'; import type { GraphQLSchema } from '../type/schema'; import { isScalarType, @@ -329,37 +330,13 @@ function printDescription( return printDescriptionWithComments(lines, indentation, firstInBlock); } - let description = - indentation && !firstInBlock - ? '\n' + indentation + '"""' - : indentation + '"""'; - - // In some circumstances, a single line can be used for the description. - if ( - lines.length === 1 && - lines[0].length < 70 && - lines[0][lines[0].length - 1] !== '"' - ) { - return description + escapeQuote(lines[0]) + '"""\n'; - } - - // Format a multi-line block quote to account for leading space. - const hasLeadingSpace = lines[0][0] === ' ' || lines[0][0] === '\t'; - if (!hasLeadingSpace) { - description += '\n'; - } - for (let i = 0; i < lines.length; i++) { - if (i !== 0 || !hasLeadingSpace) { - description += indentation; - } - description += escapeQuote(lines[i]) + '\n'; - } - description += indentation + '"""\n'; - return description; -} + const text = lines.join('\n'); + const preferMultipleLines = text.length > 70; + const blockString = printBlockString(text, '', preferMultipleLines); + const prefix = + indentation && !firstInBlock ? '\n' + indentation : indentation; -function escapeQuote(line) { - return line.replace(/"""/g, '\\"""'); + return prefix + blockString.replace(/\n/g, '\n' + indentation) + '\n'; } function printDescriptionWithComments(lines, indentation, firstInBlock) {