Skip to content

Commit 7c00820

Browse files
committed
Add RemoveIndentation() to the lexer for multi-line strings.
1 parent b33378d commit 7c00820

File tree

5 files changed

+110
-24
lines changed

5 files changed

+110
-24
lines changed

src/jsutils/dedent.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,7 @@
77
* @flow
88
*/
99

10-
/**
11-
* fixes identation by removing leading spaces from each line
12-
*/
13-
function fixIdent(str: string): string {
14-
const indent = /^\n?( *)/.exec(str)[1]; // figure out ident
15-
return str
16-
.replace(RegExp('^' + indent, 'mg'), '') // remove ident
17-
.replace(/^\n*/m, '') // remove leading newline
18-
.replace(/ *$/, ''); // remove trailing spaces
19-
}
10+
import removeIndentation from './removeIndentation';
2011

2112
/**
2213
* An ES6 string tag that fixes identation. Also removes leading newlines
@@ -45,5 +36,5 @@ export default function dedent(
4536
}
4637
}
4738

48-
return fixIdent(res);
39+
return removeIndentation(res) + '\n';
4940
}

src/jsutils/removeIndentation.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* @flow */
2+
/**
3+
* Copyright (c) 2017, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*/
10+
11+
/**
12+
* Removes leading identation from each line in a multi-line string.
13+
*
14+
* This implements RemoveIndentation() algorithm in the GraphQL spec.
15+
*
16+
* Note: this is similar to Python's docstring "trim" operation.
17+
* https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
18+
*/
19+
export default function removeIndentation(rawString: string): string {
20+
// Expand a multi-line string into independent lines.
21+
const lines = rawString.split(/\r\n|[\n\r]/g);
22+
23+
// Determine minimum indentation, not including the first line.
24+
let minIndent;
25+
for (let i = 1; i < lines.length; i++) {
26+
const line = lines[i];
27+
const lineIndent = leadingWhitespace(line);
28+
if (
29+
lineIndent < line.length &&
30+
(minIndent === undefined || lineIndent < minIndent)
31+
) {
32+
minIndent = lineIndent;
33+
if (minIndent === 0) {
34+
break;
35+
}
36+
}
37+
}
38+
39+
// Remove indentation, not including the first line.
40+
if (minIndent) {
41+
for (let i = 1; i < lines.length; i++) {
42+
lines[i] = lines[i].slice(minIndent);
43+
}
44+
}
45+
46+
// Remove leading and trailing empty lines.
47+
while (lines.length > 0 && lines[0].length === 0) {
48+
lines.shift();
49+
}
50+
while (lines.length > 0 && lines[lines.length - 1].length === 0) {
51+
lines.pop();
52+
}
53+
54+
// Return a multi-line string joined with U+000A.
55+
return lines.join('\n');
56+
}
57+
58+
function leadingWhitespace(str) {
59+
let i = 0;
60+
for (; i < str.length; i++) {
61+
if (str[i] !== ' ' && str[i] !== '\t') {
62+
break;
63+
}
64+
}
65+
return i;
66+
}

src/language/__tests__/lexer-test.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -301,11 +301,11 @@ describe('Lexer', () => {
301301
});
302302

303303
expect(
304-
lexOne('""" white space """')
304+
lexOne('" white space "')
305305
).to.containSubset({
306-
kind: TokenKind.MULTI_LINE_STRING,
306+
kind: TokenKind.STRING,
307307
start: 0,
308-
end: 19,
308+
end: 15,
309309
value: ' white space '
310310
});
311311

@@ -337,12 +337,12 @@ describe('Lexer', () => {
337337
});
338338

339339
expect(
340-
lexOne('"""multi\rline"""')
340+
lexOne('"""multi\rline\r\nnormalized"""')
341341
).to.containSubset({
342342
kind: TokenKind.MULTI_LINE_STRING,
343343
start: 0,
344-
end: 16,
345-
value: 'multi\rline'
344+
end: 28,
345+
value: 'multi\nline\nnormalized'
346346
});
347347

348348
expect(
@@ -363,6 +363,21 @@ describe('Lexer', () => {
363363
value: 'slashes \\\\ \\/'
364364
});
365365

366+
expect(
367+
lexOne(`"""
368+
369+
spans
370+
multiple
371+
lines
372+
373+
"""`)
374+
).to.containSubset({
375+
kind: TokenKind.MULTI_LINE_STRING,
376+
start: 0,
377+
end: 68,
378+
value: 'spans\n multiple\n lines'
379+
});
380+
366381
});
367382

368383
it('lex reports useful multi-line string errors', () => {

src/language/lexer.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import type { Token } from './ast';
1111
import type { Source } from './source';
1212
import { syntaxError } from '../error';
13+
import removeIndentation from '../jsutils/removeIndentation';
1314

1415
/**
1516
* Given a Source object, this returns a Lexer for that source.
@@ -532,7 +533,7 @@ function readMultiLineString(source, start, line, col, prev): Token {
532533
let position = start + 3;
533534
let chunkStart = position;
534535
let code = 0;
535-
let value = '';
536+
let rawValue = '';
536537

537538
while (
538539
position < body.length &&
@@ -544,15 +545,15 @@ function readMultiLineString(source, start, line, col, prev): Token {
544545
charCodeAt.call(body, position + 1) === 34 &&
545546
charCodeAt.call(body, position + 2) === 34
546547
) {
547-
value += slice.call(body, chunkStart, position);
548+
rawValue += slice.call(body, chunkStart, position);
548549
return new Tok(
549550
MULTI_LINE_STRING,
550551
start,
551552
position + 3,
552553
line,
553554
col,
554555
prev,
555-
value
556+
removeIndentation(rawValue)
556557
);
557558
}
558559

@@ -577,7 +578,7 @@ function readMultiLineString(source, start, line, col, prev): Token {
577578
charCodeAt.call(body, position + 2) === 34 &&
578579
charCodeAt.call(body, position + 3) === 34
579580
) {
580-
value += slice.call(body, chunkStart, position) + '"""';
581+
rawValue += slice.call(body, chunkStart, position) + '"""';
581582
position += 4;
582583
chunkStart = position;
583584
} else {

src/language/printer.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,7 @@ const printDocASTReducer = {
7373
IntValue: ({ value }) => value,
7474
FloatValue: ({ value }) => value,
7575
StringValue: ({ value, multiLine }) =>
76-
multiLine ?
77-
`"""${value.replace(/"""/g, '\\"""')}"""` :
78-
JSON.stringify(value),
76+
multiLine ? printMultiLineString(value) : JSON.stringify(value),
7977
BooleanValue: ({ value }) => JSON.stringify(value),
8078
NullValue: () => 'null',
8179
EnumValue: ({ value }) => value,
@@ -204,3 +202,18 @@ function wrap(start, maybeString, end) {
204202
function indent(maybeString) {
205203
return maybeString && maybeString.replace(/\n/g, '\n ');
206204
}
205+
206+
function printMultiLineString(value) {
207+
const hasLineBreak = value.indexOf('\n') !== -1;
208+
const hasLeadingSpace = value[0] === ' ' || value[0] === '\t';
209+
let printed = '"""';
210+
if (hasLineBreak && !hasLeadingSpace) {
211+
printed += '\n';
212+
}
213+
printed += value.replace(/"""/g, '\\"""');
214+
if (hasLineBreak) {
215+
printed += '\n';
216+
}
217+
printed += '"""';
218+
return printed;
219+
}

0 commit comments

Comments
 (0)