Add lexer
This commit is contained in:
parent
d72b431453
commit
736ec0f40d
@ -1 +1,4 @@
|
||||
console.log("It works!");
|
||||
import { Lexer } from './parser/Lexer'
|
||||
|
||||
const tokens = new Lexer("10 / 2").process().getTokens();
|
||||
console.log(tokens);
|
243
src/parser/Lexer.ts
Normal file
243
src/parser/Lexer.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import { Token } from "./Token";
|
||||
import { TokenType } from "./TokenType";
|
||||
|
||||
export class Lexer {
|
||||
private static readonly KEYWORDS = {
|
||||
"play": TokenType.PLAY,
|
||||
"queue": TokenType.QUEUE,
|
||||
"stop": TokenType.STOP,
|
||||
"music": TokenType.MUSIC,
|
||||
"ambience": TokenType.AMBIENCE,
|
||||
"sound": TokenType.SOUND,
|
||||
"sound_loop": TokenType.SOUNDLOOP,
|
||||
"fadein": TokenType.FADEIN,
|
||||
"fadeout": TokenType.FADEOUT,
|
||||
|
||||
"scene": TokenType.SCENE,
|
||||
"anim": TokenType.ANIM,
|
||||
"bg": TokenType.BG,
|
||||
"cg": TokenType.CG,
|
||||
"at": TokenType.AT,
|
||||
"as": TokenType.AS,
|
||||
"define": TokenType.DEFINE,
|
||||
"window": TokenType.WINDOW,
|
||||
"hide": TokenType.HIDE,
|
||||
"show": TokenType.SHOW,
|
||||
"with": TokenType.WITH,
|
||||
|
||||
"return": TokenType.RETURN,
|
||||
"menu": TokenType.MENU,
|
||||
"endmenu": TokenType.ENDMENU,
|
||||
"jump": TokenType.JUMP,
|
||||
"label": TokenType.LABEL,
|
||||
|
||||
"if": TokenType.IF,
|
||||
"else": TokenType.ELSE,
|
||||
"endif": TokenType.ENDIF,
|
||||
"or": TokenType.OR,
|
||||
"and": TokenType.AND,
|
||||
"not": TokenType.NOT,
|
||||
|
||||
"renpy.pause": TokenType.RENPY_PAUSE,
|
||||
"renpy.say": TokenType.RENPY_SAY,
|
||||
"persistent.sprite_time": TokenType.PERSISTENT_SPRITE_TIME,
|
||||
"prolog_time": TokenType.PROLOG_TIME,
|
||||
"day_time": TokenType.DAY_TIME,
|
||||
"sunset_time": TokenType.SUNSET_TIME,
|
||||
"night_time": TokenType.NIGHT_TIME,
|
||||
"make_names_known": TokenType.MAKE_NAMES_KNOWN,
|
||||
"make_names_unknown": TokenType.MAKE_NAMES_UNKNOWN,
|
||||
"set_name": TokenType.SET_NAME,
|
||||
"meet": TokenType.SET_NAME,
|
||||
"disable_all_zones": TokenType.DISABLE_ALL_ZONES,
|
||||
"disable_current_zone": TokenType.DISABLE_CURRENT_ZONE,
|
||||
"reset_zone": TokenType.RESET_ZONE,
|
||||
"set_zone": TokenType.SET_ZONE,
|
||||
"show_map": TokenType.SHOW_MAP
|
||||
};
|
||||
|
||||
private static readonly OPERATOR_CHARS = "=+-<>()[]!$:";
|
||||
private static readonly OPERATOR_TYPES = [
|
||||
TokenType.EQ,
|
||||
TokenType.PLUS, TokenType.MINUS,
|
||||
TokenType.LT, TokenType.GT,
|
||||
TokenType.LPAREN, TokenType.RPAREN, TokenType.LBRACKET, TokenType.RBRACKET,
|
||||
TokenType.EXCL, TokenType.COMMAND, TokenType.COLON
|
||||
];
|
||||
|
||||
|
||||
private tokens: Array<Token>
|
||||
private length: number
|
||||
private pos: number
|
||||
private buffer: string
|
||||
|
||||
constructor(
|
||||
private readonly input: string
|
||||
) {
|
||||
this.input = input;
|
||||
this.tokens = [];
|
||||
this.length = input.length;
|
||||
this.pos = 0;
|
||||
this.buffer = "";
|
||||
}
|
||||
|
||||
public getTokens(): Array<Token> { return this.tokens; }
|
||||
|
||||
public process(): Lexer {
|
||||
this.pos = 0;
|
||||
while (this.pos < this.length) {
|
||||
this.tokenize();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private tokenize(): void {
|
||||
this.skipWhitespaces();
|
||||
const ch = this.peek(0);
|
||||
if (ch.match(/[a-z]/i)) {
|
||||
// Keyword/command
|
||||
this.tokenizeWord();
|
||||
} else if (ch.match(/[0-9]/i)) {
|
||||
this.tokenizeNumber();
|
||||
} else if (ch === '"' || ch === '\'') {
|
||||
// Text in " '
|
||||
this.tokenizeText(ch);
|
||||
} else if (ch === '#') {
|
||||
this.tokenizeComment();
|
||||
} else {
|
||||
// Operators/special symbols
|
||||
this.tokenizeOperator();
|
||||
}
|
||||
}
|
||||
|
||||
private tokenizeWord(): void {
|
||||
let ch = this.peek(0);
|
||||
// Unicode u"text" or u'text'
|
||||
if (ch === 'u') {
|
||||
let textStartChar = this.peek(1);
|
||||
if (textStartChar === '"' || textStartChar === '\'') {
|
||||
this.next(); // u
|
||||
this.tokenizeText(textStartChar);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.clearBuffer();
|
||||
while (ch.match(/[a-z0-9_\.]/i)) {
|
||||
this.buffer += (ch);
|
||||
ch = this.next();
|
||||
}
|
||||
|
||||
let word = this.buffer;
|
||||
let key = word.toLowerCase();
|
||||
if (key in Lexer.KEYWORDS) {
|
||||
this.addToken(Lexer.KEYWORDS[key]);
|
||||
} else {
|
||||
this.addToken(TokenType.WORD, word);
|
||||
}
|
||||
}
|
||||
|
||||
private tokenizeNumber(): void {
|
||||
let ch = this.peek(0);
|
||||
this.clearBuffer();
|
||||
let decimal = false;
|
||||
while (true) {
|
||||
// Integer or decimal
|
||||
if (ch === '.') {
|
||||
// Skip floating point if more then 1 present
|
||||
if (!decimal) this.buffer += (ch);
|
||||
decimal = true;
|
||||
ch = this.next();
|
||||
continue;
|
||||
} else if (!ch.match(/[0-9]/i)) {
|
||||
break;
|
||||
}
|
||||
this.buffer += (ch);
|
||||
ch = this.next();
|
||||
}
|
||||
this.addToken(TokenType.NUMBER, this.buffer);
|
||||
}
|
||||
|
||||
private tokenizeOperator(): void {
|
||||
let ch = this.peek(0);
|
||||
var index = Lexer.OPERATOR_CHARS.indexOf(ch);
|
||||
if (index !== -1) {
|
||||
this.addToken(Lexer.OPERATOR_TYPES[index]);
|
||||
}
|
||||
this.next();
|
||||
}
|
||||
|
||||
private tokenizeText(textStartChar: string): void {
|
||||
this.clearBuffer();
|
||||
let ch = this.next(); // skip open "
|
||||
while (true) {
|
||||
if (ch === textStartChar) break;
|
||||
if (ch === '\0') break; // " is not closed, but we'll add what's left
|
||||
if (ch === '\\') {
|
||||
ch = this.next();
|
||||
switch (ch) { // TODO fix escaping
|
||||
case 'n': ch = this.next(); this.buffer += ('\n'); continue;
|
||||
case 't': ch = this.next(); this.buffer += ('\t'); continue;
|
||||
default:
|
||||
if (ch === textStartChar) {
|
||||
ch = this.next();
|
||||
this.buffer += ('"');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.buffer += ('\\');
|
||||
continue;
|
||||
}
|
||||
this.buffer += (ch);
|
||||
ch = this.next();
|
||||
}
|
||||
this.next(); // skip closing "
|
||||
this.addToken(TokenType.TEXT, this.buffer);
|
||||
}
|
||||
|
||||
private tokenizeComment(): void {
|
||||
let ch = this.peek(0);
|
||||
while ("\n\r\0".indexOf(ch) === -1) {
|
||||
ch = this.next();
|
||||
}
|
||||
}
|
||||
|
||||
private skipWhitespaces(): void {
|
||||
let ch = this.peek(0);
|
||||
while (ch !== '\0' && ch.match(/\s/)) {
|
||||
ch = this.next();
|
||||
}
|
||||
}
|
||||
|
||||
private addToken(type: TokenType, text = ""): void {
|
||||
this.tokens.push(new Token(text, type));
|
||||
}
|
||||
|
||||
private clearBuffer(): void {
|
||||
this.buffer = "";
|
||||
}
|
||||
|
||||
private next(): string {
|
||||
this.pos++;
|
||||
if (this.pos >= this.length) return '\0';
|
||||
return this.input.charAt(this.pos);
|
||||
}
|
||||
|
||||
private peek(relativePosition: number): string {
|
||||
const tempPos = this.pos + relativePosition;
|
||||
if (tempPos >= this.length) return '\0';
|
||||
return this.input.charAt(tempPos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
20
src/parser/Token.ts
Normal file
20
src/parser/Token.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { TokenType } from "./TokenType";
|
||||
|
||||
export class Token {
|
||||
constructor(
|
||||
private readonly text: string,
|
||||
private readonly type: TokenType
|
||||
) {}
|
||||
|
||||
getText(): string {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getType(): TokenType {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.type} ${this.text}`;
|
||||
}
|
||||
}
|
76
src/parser/TokenType.ts
Normal file
76
src/parser/TokenType.ts
Normal file
@ -0,0 +1,76 @@
|
||||
export enum TokenType {
|
||||
COMMAND, // starts with $
|
||||
WORD,
|
||||
TEXT,
|
||||
NUMBER,
|
||||
|
||||
// Operators and pecial symbols
|
||||
EQ,
|
||||
PLUS,
|
||||
MINUS,
|
||||
LT,
|
||||
GT,
|
||||
LPAREN,
|
||||
RPAREN,
|
||||
LBRACKET,
|
||||
RBRACKET,
|
||||
EXCL,
|
||||
COLON,
|
||||
|
||||
// Keywords
|
||||
PLAY,
|
||||
QUEUE,
|
||||
STOP,
|
||||
MUSIC,
|
||||
AMBIENCE,
|
||||
SOUND,
|
||||
SOUNDLOOP,
|
||||
FADEIN,
|
||||
FADEOUT,
|
||||
|
||||
SCENE,
|
||||
ANIM,
|
||||
BG,
|
||||
CG,
|
||||
|
||||
WINDOW,
|
||||
HIDE,
|
||||
SHOW,
|
||||
|
||||
AT,
|
||||
AS,
|
||||
WITH,
|
||||
DEFINE,
|
||||
|
||||
MENU,
|
||||
ENDMENU,
|
||||
JUMP,
|
||||
LABEL,
|
||||
RETURN,
|
||||
|
||||
IF,
|
||||
ELSE,
|
||||
ENDIF,
|
||||
OR,
|
||||
AND,
|
||||
NOT,
|
||||
|
||||
// Commands
|
||||
RENPY_PAUSE,
|
||||
RENPY_SAY,
|
||||
PERSISTENT_SPRITE_TIME,
|
||||
PROLOG_TIME,
|
||||
DAY_TIME,
|
||||
SUNSET_TIME,
|
||||
NIGHT_TIME,
|
||||
MAKE_NAMES_KNOWN,
|
||||
MAKE_NAMES_UNKNOWN,
|
||||
SET_NAME,
|
||||
DISABLE_ALL_ZONES,
|
||||
DISABLE_CURRENT_ZONE,
|
||||
RESET_ZONE,
|
||||
SET_ZONE,
|
||||
SHOW_MAP,
|
||||
|
||||
EOF
|
||||
};
|
Loading…
Reference in New Issue
Block a user