Skip to content

Commit ce2d30a

Browse files
committed
Add RemoveIndentation() to the lexer for multi-line strings.
1 parent 3fa0624 commit ce2d30a

File tree

4 files changed

+91
-18
lines changed

4 files changed

+91
-18
lines changed

src/jsutils/dedent.js

+2-11
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,7 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

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

2213
/**
2314
* An ES6 string tag that fixes identation. Also removes leading newlines
@@ -46,5 +37,5 @@ export default function dedent(
4637
}
4738
}
4839

49-
return fixIdent(res);
40+
return removeIndentation(res) + '\n';
5041
}

src/jsutils/removeIndentation.js

+66
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

+18-3
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,11 @@ describe('Lexer', () => {
270270
});
271271

272272
expect(
273-
lexOne('""" white space """')
273+
lexOne('" white space "')
274274
).to.containSubset({
275-
kind: TokenKind.MULTI_LINE_STRING,
275+
kind: TokenKind.STRING,
276276
start: 0,
277-
end: 19,
277+
end: 15,
278278
value: ' white space '
279279
});
280280

@@ -332,6 +332,21 @@ describe('Lexer', () => {
332332
value: 'slashes \\\\ \\/'
333333
});
334334

335+
expect(
336+
lexOne(`"""
337+
338+
spans
339+
multiple
340+
lines
341+
342+
"""`)
343+
).to.containSubset({
344+
kind: TokenKind.MULTI_LINE_STRING,
345+
start: 0,
346+
end: 68,
347+
value: 'spans\n multiple\n lines'
348+
});
349+
335350
});
336351

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

src/language/lexer.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import type { Token } from './ast';
1212
import type { Source } from './source';
1313
import { syntaxError } from '../error';
14+
import removeIndentation from '../jsutils/removeIndentation';
1415

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

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

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

0 commit comments

Comments
 (0)