diff --git a/src/jsutils/__tests__/dedent-test.js b/src/jsutils/__tests__/dedent-test.js new file mode 100644 index 0000000000..b1b93e42f5 --- /dev/null +++ b/src/jsutils/__tests__/dedent-test.js @@ -0,0 +1,146 @@ +/** + * 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. + */ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import dedent from '../dedent'; + +describe('dedent', () => { + it('removes indentation in typical usage', () => { + const output = dedent` + type Query { + me: User + } + + type User { + id: ID + name: String + } + `; + expect(output).to.equal( + [ + 'type Query {', + ' me: User', + '}', + '', + 'type User {', + ' id: ID', + ' name: String', + '}', + '', + ].join('\n'), + ); + }); + + it('removes only the first level of indentation', () => { + const output = dedent` + qux + quux + quuux + quuuux + `; + expect(output).to.equal( + ['qux', ' quux', ' quuux', ' quuuux', ''].join('\n'), + ); + }); + + it('does not escape special characters', () => { + const output = dedent` + type Root { + field(arg: String = "wi\th de\fault"): String + } + `; + expect(output).to.equal( + [ + 'type Root {', + ' field(arg: String = "wi\th de\fault"): String', + '}', + '', + ].join('\n'), + ); + }); + + it('also works as an ordinary function on strings', () => { + const output = dedent(` + type Query { + me: User + } + `); + expect(output).to.equal(['type Query {', ' me: User', '}', ''].join('\n')); + }); + + it('also removes indentation using tabs', () => { + const output = dedent` + \t\t type Query { + \t\t me: User + \t\t } + `; + expect(output).to.equal(['type Query {', ' me: User', '}', ''].join('\n')); + }); + + it('removes leading newlines', () => { + const output = dedent` + + + type Query { + me: User + }`; + expect(output).to.equal(['type Query {', ' me: User', '}'].join('\n')); + }); + + it('does not remove trailing newlines', () => { + const output = dedent` + type Query { + me: User + } + + `; + expect(output).to.equal( + ['type Query {', ' me: User', '}', '', ''].join('\n'), + ); + }); + + it('removes all trailing spaces and tabs', () => { + const output = dedent` + type Query { + me: User + } + \t\t \t `; + expect(output).to.equal(['type Query {', ' me: User', '}', ''].join('\n')); + }); + + it('works on text without leading newline', () => { + const output = dedent` type Query { + me: User + }`; + expect(output).to.equal(['type Query {', ' me: User', '}'].join('\n')); + }); + + it('supports expression interpolation', () => { + const name = 'Luke Skywalker'; + const age = 42; + const output = dedent` + { + "me": { + "name": "${name}" + "age": ${age} + } + } + `; + expect(output).to.equal( + [ + '{', + ' "me": {', + ' "name": "Luke Skywalker"', + ' "age": 42', + ' }', + '}', + '', + ].join('\n'), + ); + }); +}); diff --git a/src/jsutils/dedent.js b/src/jsutils/dedent.js index 720e522e1d..af33a6391a 100644 --- a/src/jsutils/dedent.js +++ b/src/jsutils/dedent.js @@ -8,19 +8,19 @@ */ /** - * fixes indentation by removing leading spaces from each line + * fixes indentation by removing leading spaces and tabs from each line */ function fixIndent(str: string): string { - const indent = /^\n?( *)/.exec(str)[1]; // figure out indent - return str - .replace(RegExp('^' + indent, 'mg'), '') // remove indent + const trimmedStr = str .replace(/^\n*/m, '') // remove leading newline - .replace(/ *$/, ''); // remove trailing spaces + .replace(/[ \t]*$/, ''); // remove trailing spaces and tabs + const indent = /^[ \t]*/.exec(trimmedStr)[0]; // figure out indent + return trimmedStr.replace(RegExp('^' + indent, 'mg'), ''); // remove indent } /** * An ES6 string tag that fixes indentation. Also removes leading newlines - * but keeps trailing ones + * and trailing spaces and tabs, but keeps trailing newlines. * * Example usage: * const str = dedent` @@ -31,19 +31,20 @@ function fixIndent(str: string): string { * str === "{\n test\n}\n"; */ export default function dedent( - strings: string | { raw: [string] }, + strings: string | Array, ...values: Array -) { - const raw = typeof strings === 'string' ? [strings] : strings.raw; - let res = ''; - // interpolation - for (let i = 0; i < raw.length; i++) { - res += raw[i].replace(/\\`/g, '`'); // handle escaped backticks +): string { + // when used as an ordinary function, allow passing a singleton string + const strArray = typeof strings === 'string' ? [strings] : strings; + const numValues = values.length; - if (i < values.length) { - res += values[i]; + const str = strArray.reduce((prev, cur, index) => { + let next = prev + cur; + if (index < numValues) { + next += values[index]; // interpolation } - } + return next; + }, ''); - return fixIndent(res); + return fixIndent(str); } diff --git a/src/language/__tests__/printer-test.js b/src/language/__tests__/printer-test.js index 14b75e32ea..d782554e66 100644 --- a/src/language/__tests__/printer-test.js +++ b/src/language/__tests__/printer-test.js @@ -145,7 +145,8 @@ describe('Printer: Query document', () => { const printed = print(ast); - expect(printed).to.equal(dedent` + expect(printed).to.equal( + dedent(String.raw` query queryName($foo: ComplexType, $site: Site = MOBILE) { whoever123is: node(id: [123, 456]) { id @@ -198,6 +199,7 @@ describe('Printer: Query document', () => { unnamed(truthy: true, falsey: false, nullish: null) query } - `); + `), + ); }); }); diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 262d1b983a..bb4f518d13 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -206,7 +206,8 @@ describe('Type System Printer', () => { type: GraphQLString, args: { argOne: { type: GraphQLString, defaultValue: 'tes\t de\fault' } }, }); - expect(output).to.equal(dedent` + expect(output).to.equal( + dedent(String.raw` schema { query: Root } @@ -214,7 +215,8 @@ describe('Type System Printer', () => { type Root { singleField(argOne: String = "tes\t de\fault"): String } - `); + `), + ); }); it('Prints String Field With Int Arg With Default Null', () => {