graphql-language-service
Version:
The official, runtime independent Language Service for GraphQL
171 lines (143 loc) • 4.48 kB
text/typescript
/**
* Copyright (c) 2021 GraphQL Contributors
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* CharacterStream implements a stream of character tokens given a source text.
* The API design follows that of CodeMirror.StringStream.
*
* Required:
*
* sourceText: (string), A raw GraphQL source text. Works best if a line
* is supplied.
*
*/
import { TokenPattern, CharacterStreamInterface } from './types';
export default class CharacterStream implements CharacterStreamInterface {
private _start = 0;
private _pos = 0;
private _sourceText: string;
constructor(sourceText: string) {
this._sourceText = sourceText;
}
public getStartOfToken = (): number => this._start;
public getCurrentPosition = (): number => this._pos;
private _testNextCharacter(pattern: TokenPattern): boolean {
const character = this._sourceText.charAt(this._pos);
let isMatched = false;
if (typeof pattern === 'string') {
isMatched = character === pattern;
} else {
isMatched =
pattern instanceof RegExp
? pattern.test(character)
: pattern(character);
}
return isMatched;
}
public eol = (): boolean => this._sourceText.length === this._pos;
public sol = (): boolean => this._pos === 0;
public peek = (): string | null => {
return this._sourceText.charAt(this._pos) || null;
};
public next = (): string => {
const char = this._sourceText.charAt(this._pos);
this._pos++;
return char;
};
public eat = (pattern: TokenPattern): string | undefined => {
const isMatched = this._testNextCharacter(pattern);
if (isMatched) {
this._start = this._pos;
this._pos++;
return this._sourceText.charAt(this._pos - 1);
}
return undefined;
};
public eatWhile = (match: TokenPattern): boolean => {
let isMatched = this._testNextCharacter(match);
let didEat = false;
// If a match, treat the total upcoming matches as one token
if (isMatched) {
didEat = isMatched;
this._start = this._pos;
}
while (isMatched) {
this._pos++;
isMatched = this._testNextCharacter(match);
didEat = true;
}
return didEat;
};
public eatSpace = (): boolean => this.eatWhile(/[\s\u00a0]/);
public skipToEnd = (): void => {
this._pos = this._sourceText.length;
};
public skipTo = (position: number): void => {
this._pos = position;
};
public match = (
pattern: TokenPattern,
consume: boolean | null | undefined = true,
caseFold: boolean | null | undefined = false,
): Array<string> | boolean => {
let token = null;
let match = null;
if (typeof pattern === 'string') {
const regex = new RegExp(pattern, caseFold ? 'i' : 'g');
match = regex.test(
this._sourceText.slice(this._pos, this._pos + pattern.length),
);
token = pattern;
} else if (pattern instanceof RegExp) {
match = this._sourceText.slice(this._pos).match(pattern);
token = match?.[0];
}
if (
match != null &&
(typeof pattern === 'string' ||
(match instanceof Array &&
// String.match returns 'index' property, which flow fails to detect
// for some reason. The below is a workaround, but an easier solution
// is just checking if `match.index === 0`
this._sourceText.startsWith(match[0], this._pos)))
) {
if (consume) {
this._start = this._pos;
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- otherwise has type issue
if (token && token.length) {
this._pos += token.length;
}
}
return match;
}
// No match available.
return false;
};
public backUp = (num: number): void => {
this._pos -= num;
};
public column = (): number => this._pos;
public indentation = (): number => {
const match = this._sourceText.match(/\s*/);
let indent = 0;
if (match && match.length !== 0) {
const whiteSpaces = match[0];
let pos = 0;
while (whiteSpaces.length > pos) {
if (whiteSpaces.charCodeAt(pos) === 9) {
indent += 2;
} else {
indent++;
}
pos++;
}
}
return indent;
};
public current = (): string => this._sourceText.slice(this._start, this._pos);
}