Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Block String #926

Merged
merged 3 commits into from
Nov 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions src/language/__tests__/blockStringValue-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* 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 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'));
});

});
6 changes: 5 additions & 1 deletion src/language/__tests__/kitchen-sink.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) {
}

fragment frag on Friend {
foo(size: $size, bar: $b, obj: {key: "value"})
foo(size: $size, bar: $b, obj: {key: "value", block: """

block string uses \"""

"""})
}

{
Expand Down
115 changes: 115 additions & 0 deletions src/language/__tests__/lexer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,121 @@ describe('Lexer', () => {
);
});

it('lexes block strings', () => {

expect(
lexOne('"""simple"""')
).to.containSubset({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 12,
value: 'simple'
});

expect(
lexOne('" white space "')
).to.containSubset({
kind: TokenKind.STRING,
start: 0,
end: 15,
value: ' white space '
});

expect(
lexOne('"""contains " quote"""')
).to.containSubset({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 22,
value: 'contains " quote'
});

expect(
lexOne('"""contains \\""" triplequote"""')
).to.containSubset({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 31,
value: 'contains """ triplequote'
});

expect(
lexOne('"""multi\nline"""')
).to.containSubset({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 16,
value: 'multi\nline'
});

expect(
lexOne('"""multi\rline\r\nnormalized"""')
).to.containSubset({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 28,
value: 'multi\nline\nnormalized'
});

expect(
lexOne('"""unescaped \\n\\r\\b\\t\\f\\u1234"""')
).to.containSubset({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 32,
value: 'unescaped \\n\\r\\b\\t\\f\\u1234'
});

expect(
lexOne('"""slashes \\\\ \\/"""')
).to.containSubset({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 19,
value: 'slashes \\\\ \\/'
});

expect(
lexOne(`"""

spans
multiple
lines

"""`)
).to.containSubset({
kind: TokenKind.BLOCK_STRING,
start: 0,
end: 68,
value: 'spans\n multiple\n lines'
});

});

it('lex reports useful block string errors', () => {

expect(
() => lexOne('"""')
).to.throw('Syntax Error GraphQL request (1:4) Unterminated string.');

expect(
() => lexOne('"""no end quote')
).to.throw('Syntax Error GraphQL request (1:16) Unterminated string.');

expect(
() => lexOne('"""contains unescaped \u0007 control char"""')
).to.throw(
'Syntax Error GraphQL request (1:23) Invalid character within String: "\\u0007".'
);

expect(
() => lexOne('"""null-byte is not \u0000 end of file"""')
).to.throw(
'Syntax Error GraphQL request (1:21) Invalid character within String: "\\u0000".'
);

});

it('lexes numbers', () => {

expect(
Expand Down
16 changes: 16 additions & 0 deletions src/language/__tests__/parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,22 @@ describe('Parser', () => {
});
});

it('parses block strings', () => {
expect(parseValue('["""long""" "short"]')).to.containSubset({
kind: Kind.LIST,
loc: { start: 0, end: 20 },
values: [
{ kind: Kind.STRING,
loc: { start: 1, end: 11},
value: 'long',
block: true },
{ kind: Kind.STRING,
loc: { start: 12, end: 19},
value: 'short',
block: false } ]
});
});

});

describe('parseType', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/language/__tests__/printer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ describe('Printer', () => {
}

fragment frag on Friend {
foo(size: $size, bar: $b, obj: {key: "value"})
foo(size: $size, bar: $b, obj: {key: "value", block: """
block string uses \"""
"""})
}

{
Expand Down
6 changes: 6 additions & 0 deletions src/language/__tests__/visitor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,12 @@ describe('Visitor', () => {
[ 'enter', 'StringValue', 'value', 'ObjectField' ],
[ 'leave', 'StringValue', 'value', 'ObjectField' ],
[ 'leave', 'ObjectField', 0, undefined ],
[ 'enter', 'ObjectField', 1, undefined ],
[ 'enter', 'Name', 'name', 'ObjectField' ],
[ 'leave', 'Name', 'name', 'ObjectField' ],
[ 'enter', 'StringValue', 'value', 'ObjectField' ],
[ 'leave', 'StringValue', 'value', 'ObjectField' ],
[ 'leave', 'ObjectField', 1, undefined ],
[ 'leave', 'ObjectValue', 'value', 'Argument' ],
[ 'leave', 'Argument', 2, undefined ],
[ 'leave', 'Field', 0, undefined ],
Expand Down
2 changes: 2 additions & 0 deletions src/language/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type TokenKind = '<SOF>'
| 'Int'
| 'Float'
| 'String'
| 'BlockString'
| 'Comment';

/**
Expand Down Expand Up @@ -288,6 +289,7 @@ export type StringValueNode = {
kind: 'StringValue';
loc?: Location;
value: string;
block?: boolean;
};

export type BooleanValueNode = {
Expand Down
64 changes: 64 additions & 0 deletions src/language/blockStringValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* 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
*/

/**
* 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.
*
* This implements the GraphQL spec's BlockStringValue() static algorithm.
*/
export default function blockStringValue(rawString: string): string {
// Expand a block string's raw value into independent lines.
const lines = rawString.split(/\r\n|[\n\r]/g);

// Remove common indentation from all lines but first.
let commonIndent = null;
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
const indent = leadingWhitespace(line);
if (
indent < line.length &&
(commonIndent === null || indent < commonIndent)
) {
commonIndent = indent;
if (commonIndent === 0) {
break;
}
}
}

if (commonIndent) {
for (let i = 1; i < lines.length; i++) {
lines[i] = lines[i].slice(commonIndent);
}
}

// Remove leading and trailing blank lines.
while (lines.length > 0 && isBlank(lines[0])) {
lines.shift();
}
while (lines.length > 0 && isBlank(lines[lines.length - 1])) {
lines.pop();
}

// Return a string of the lines joined with U+000A.
return lines.join('\n');
}

function leadingWhitespace(str) {
let i = 0;
while (i < str.length && (str[i] === ' ' || str[i] === '\t')) {
i++;
}
return i;
}

function isBlank(str) {
return leadingWhitespace(str) === str.length;
}
Loading