Add parser
This commit is contained in:
parent
92c483fc2f
commit
3eac6863d6
750
src/parser/Parser.ts
Normal file
750
src/parser/Parser.ts
Normal file
@ -0,0 +1,750 @@
|
|||||||
|
import { TextUtils } from "../utils/TextUtils";
|
||||||
|
import { Variables } from "../runtime/Variables";
|
||||||
|
import { FadeInfo } from "../view/model/FadeInfo";
|
||||||
|
import { Menu } from "../view/model/Menu";
|
||||||
|
import { ViewModel } from "../view/model/ViewModel";
|
||||||
|
import { Token } from "./Token";
|
||||||
|
import { TokenType } from "./TokenType";
|
||||||
|
import {
|
||||||
|
Expression,
|
||||||
|
ValueExpression,
|
||||||
|
VariableExpression,
|
||||||
|
BinaryExpression,
|
||||||
|
Operator
|
||||||
|
} from "./ast";
|
||||||
|
|
||||||
|
export class Parser {
|
||||||
|
private readonly EOF = new Token("", TokenType.EOF);
|
||||||
|
|
||||||
|
private tokensCount: number
|
||||||
|
private position: number
|
||||||
|
private lastPosition: number
|
||||||
|
private labels: object
|
||||||
|
private hasEndMenu: boolean
|
||||||
|
private hasEndIf: boolean
|
||||||
|
private vm: ViewModel
|
||||||
|
|
||||||
|
constructor(private readonly tokens: Array<Token>) {
|
||||||
|
this.tokens = tokens;
|
||||||
|
this.tokensCount = tokens.length;
|
||||||
|
this.position = 0;
|
||||||
|
this.lastPosition = 0;
|
||||||
|
this.labels = {};
|
||||||
|
/** Optimization, if there no endmenu/endif => don't search them */
|
||||||
|
this.hasEndMenu = false;
|
||||||
|
this.hasEndIf = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getInstance(): Parser {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setView(view: ViewModel): void {
|
||||||
|
this.vm = view
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPosition(position): void {
|
||||||
|
this.position = position;
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public jumpLabel(label): void {
|
||||||
|
if (label in this.labels) {
|
||||||
|
this.position = this.labels[label];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public next(): void {
|
||||||
|
this.lastPosition = this.position;
|
||||||
|
|
||||||
|
let terminal = false;
|
||||||
|
let counter = 0;
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
terminal = this.statement();
|
||||||
|
} catch (re) {
|
||||||
|
console.log("Parser.next() " + re);
|
||||||
|
if (this.tokens.length === 0) return;
|
||||||
|
}
|
||||||
|
// to prevent infinite loop
|
||||||
|
counter++;
|
||||||
|
if (counter >= 1000) {
|
||||||
|
this.position++;
|
||||||
|
counter = 0;
|
||||||
|
}
|
||||||
|
} while (!terminal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public prevScene(): void {
|
||||||
|
// To jump to previous scene search SCENE token twice
|
||||||
|
// as the first occurence will only find a current scene
|
||||||
|
const currentScene = this.find(TokenType.SCENE, -1);
|
||||||
|
const pos = this.findFrom(currentScene, TokenType.SCENE, -1);
|
||||||
|
// Jump to start if no scenes found
|
||||||
|
if (pos == this.position) this.position = 0;
|
||||||
|
else this.position = pos;
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextScene(): void {
|
||||||
|
this.position = this.find(TokenType.SCENE, 1);
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private find(which: TokenType, step: number): number {
|
||||||
|
return this.findFrom(this.position, which, step);
|
||||||
|
}
|
||||||
|
|
||||||
|
private findFrom(from: number, which: TokenType, step: number) {
|
||||||
|
let pos = from;
|
||||||
|
while (true) {
|
||||||
|
pos += step;
|
||||||
|
if (pos < 0 || pos >= this.tokensCount) break;
|
||||||
|
|
||||||
|
const token = this.tokens[pos];
|
||||||
|
if (which === token.getType()) return pos;
|
||||||
|
}
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
|
private statement(): boolean {
|
||||||
|
const token = this.get(0);
|
||||||
|
if (this.match(token, TokenType.COMMAND)) return this.command();
|
||||||
|
if (this.match(token, TokenType.SCENE)) return this.scene();
|
||||||
|
if (this.match(token, TokenType.PLAY)) return this.play();
|
||||||
|
if (this.match(token, TokenType.QUEUE)) return this.queue();
|
||||||
|
if (this.match(token, TokenType.STOP)) return this.stop();
|
||||||
|
if (this.match(token, TokenType.SHOW)) return this.show();
|
||||||
|
if (this.match(token, TokenType.HIDE)) return this.hide();
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.JUMP)) return this.jump();
|
||||||
|
if (this.match(token, TokenType.IF)) return this.ifStatement();
|
||||||
|
|
||||||
|
if (this.lookMatch(1, TokenType.COLON)) {
|
||||||
|
// menu:
|
||||||
|
if (this.match(token, TokenType.MENU)) return this.menu();
|
||||||
|
|
||||||
|
// The rest of menu choice. Skip to ENDMENU.
|
||||||
|
if (this.match(token, TokenType.TEXT)) {
|
||||||
|
if (this.hasEndMenu) this.position += this.skipMenu();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rest of if statement. Skip to ENDIF.
|
||||||
|
if (this.match(token, TokenType.ELSE)) {
|
||||||
|
if (this.hasEndIf) this.position += this.skipIf();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text with author name.
|
||||||
|
if (this.lookMatch(1, TokenType.TEXT) && this.match(token, TokenType.WORD)) {
|
||||||
|
const whoid = token.getText();
|
||||||
|
this.vm.textW(whoid, this.consume(TokenType.TEXT).getText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular text
|
||||||
|
if (this.match(token, TokenType.TEXT)) {
|
||||||
|
this.vm.text(token.getText());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.EOF)) {
|
||||||
|
this.vm.finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.WINDOW)) {
|
||||||
|
if (this.match0(TokenType.SHOW))
|
||||||
|
return this.vm.windowShow(this.matchWithEffect());
|
||||||
|
else if (this.match0(TokenType.HIDE))
|
||||||
|
return this.vm.windowHide(this.matchWithEffect());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(this.matchWithEffect())) return false;
|
||||||
|
|
||||||
|
this.position++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private command(): boolean {
|
||||||
|
const token = this.get(0);
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.RENPY_PAUSE)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
const pause = this.consumeDouble();
|
||||||
|
let hard = false;
|
||||||
|
if (!this.lookMatch(0, TokenType.RPAREN)) {
|
||||||
|
this.consume(TokenType.WORD); // hard
|
||||||
|
this.consume(TokenType.EQ);
|
||||||
|
hard = this.consumeBoolean();
|
||||||
|
}
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
this.vm.pause(Math.floor(1000 * pause), hard);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.RENPY_SAY)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
const whoid = this.consume(TokenType.WORD).getText();
|
||||||
|
// TODO: this.consume(TokenType.COMMA)
|
||||||
|
const text = this.consume(TokenType.TEXT).getText();
|
||||||
|
// TODO: this.consume(TokenType.COMMA)
|
||||||
|
this.consume(TokenType.WORD); // interact
|
||||||
|
this.consume(TokenType.EQ);
|
||||||
|
const interact = this.consumeBoolean();
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
this.vm.textW(whoid, text);
|
||||||
|
return interact;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.PERSISTENT_SPRITE_TIME)) {
|
||||||
|
this.consume(TokenType.EQ);
|
||||||
|
this.consume(TokenType.TEXT);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.PROLOG_TIME) ||
|
||||||
|
this.match(token, TokenType.DAY_TIME) ||
|
||||||
|
this.match(token, TokenType.SUNSET_TIME) ||
|
||||||
|
this.match(token, TokenType.NIGHT_TIME)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.MAKE_NAMES_KNOWN) ||
|
||||||
|
this.match(token, TokenType.MAKE_NAMES_UNKNOWN)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
if (token.getType() === TokenType.MAKE_NAMES_KNOWN) {
|
||||||
|
this.vm.makeNamesKnown();
|
||||||
|
} else {
|
||||||
|
this.vm.makeNamesUnknown();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.SET_NAME)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
const whoid = this.consume(TokenType.TEXT).getText();
|
||||||
|
// TODO: this.consume(TokenType.COMMA)
|
||||||
|
const name = this.consume(TokenType.TEXT).getText();
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
this.vm.meet(whoid, name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map
|
||||||
|
if (this.match(token, TokenType.DISABLE_ALL_ZONES)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
this.vm.disableAllZones();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.match(token, TokenType.DISABLE_CURRENT_ZONE)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
this.vm.disableCurrentZone();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.match(token, TokenType.RESET_ZONE)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
const zone = this.consume(TokenType.TEXT).getText();
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
this.vm.resetZone(zone);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.match(token, TokenType.SET_ZONE)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
const zone = this.consume(TokenType.TEXT).getText();
|
||||||
|
// TODO: this.consume(TokenType.COMMA)
|
||||||
|
const label = this.consume(TokenType.TEXT).getText();
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
this.vm.setZone(zone, label);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.match(token, TokenType.SHOW_MAP)) {
|
||||||
|
this.consume(TokenType.LPAREN);
|
||||||
|
this.consume(TokenType.RPAREN);
|
||||||
|
this.vm.showMap();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.match(token, TokenType.WORD)) {
|
||||||
|
if (this.match0(TokenType.EQ)) {
|
||||||
|
// variable = expression
|
||||||
|
Variables.setVariable(token.getText(), this.expression().eval());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.lookMatch(1, TokenType.EQ) && this.match0(TokenType.PLUS)) {
|
||||||
|
// variable += expression
|
||||||
|
this.consume(TokenType.EQ);
|
||||||
|
const varValue = Variables.getVariable(token.getText());
|
||||||
|
Variables.setVariable(token.getText(), varValue + this.expression().eval());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.lookMatch(1, TokenType.EQ) && this.match0(TokenType.MINUS)) {
|
||||||
|
// variable -= expression
|
||||||
|
this.consume(TokenType.EQ);
|
||||||
|
const varValue = Variables.getVariable(token.getText());
|
||||||
|
Variables.setVariable(token.getText(), varValue - this.expression().eval());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scene(): boolean {
|
||||||
|
let type = "";
|
||||||
|
if (this.match0(TokenType.BG)) type = "bg";
|
||||||
|
else if (this.match0(TokenType.CG)) type = "cg";
|
||||||
|
else if (this.match0(TokenType.ANIM)) type = "anim";
|
||||||
|
|
||||||
|
const name = this.consume(TokenType.WORD).getText();
|
||||||
|
const effect = this.matchWithEffect();
|
||||||
|
this.vm.background(type, name, effect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private show(): boolean {
|
||||||
|
const whoid = this.consume(TokenType.WORD).getText();
|
||||||
|
let params = ""; // emotion? cloth? distance?
|
||||||
|
while (this.lookMatch(0, TokenType.WORD)) {
|
||||||
|
let text = this.consume(TokenType.WORD).getText();
|
||||||
|
params += text;
|
||||||
|
if (text == "close" || text == "far") break;
|
||||||
|
}
|
||||||
|
// Position (left, cleft, ...)
|
||||||
|
let position = "";
|
||||||
|
if (this.match0(TokenType.AT)) {
|
||||||
|
position = this.consume(TokenType.WORD).getText();
|
||||||
|
}
|
||||||
|
// Alias (to show same character in different positions)
|
||||||
|
let alias = "";
|
||||||
|
if (this.match0(TokenType.AS)) {
|
||||||
|
alias = this.consume(TokenType.WORD).getText();
|
||||||
|
}
|
||||||
|
const effect = this.matchWithEffect();
|
||||||
|
this.vm.sprite(whoid, params, position, alias, effect);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hide(): boolean {
|
||||||
|
const whoid = this.consume(TokenType.WORD).getText();
|
||||||
|
const effect = this.matchWithEffect();
|
||||||
|
this.vm.hideSprite(whoid, effect);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private play(): boolean {
|
||||||
|
if (this.match0(TokenType.MUSIC)) return this.playMusic();
|
||||||
|
if (this.match0(TokenType.AMBIENCE)) return this.playAmbience();
|
||||||
|
if (this.match0(TokenType.SOUND)) return this.playSound();
|
||||||
|
if (this.match0(TokenType.SOUNDLOOP)) return this.playSoundLoop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private playMusic(): boolean {
|
||||||
|
const name = this.consumeMusicName();
|
||||||
|
const fade = this.matchFade();
|
||||||
|
this.vm.music(name, fade);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private playSound(): boolean {
|
||||||
|
const name = this.consumeMusicName();
|
||||||
|
const fade = this.matchFade();
|
||||||
|
this.vm.sound(name, fade);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private playSoundLoop(): boolean {
|
||||||
|
const name = this.consumeMusicName();
|
||||||
|
const fade = this.matchFade();
|
||||||
|
this.vm.soundLoop(name, fade);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private playAmbience(): boolean {
|
||||||
|
const name = this.consumeMusicName();
|
||||||
|
const fade = this.matchFade();
|
||||||
|
this.vm.ambience(name, fade);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private queue(): boolean {
|
||||||
|
if (this.match0(TokenType.MUSIC)) return this.queueMusic();
|
||||||
|
if (this.match0(TokenType.SOUND)) return this.queueSound();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueMusic(): boolean {
|
||||||
|
const name = this.consumeMusicName();
|
||||||
|
this.vm.addMusicToQueue(name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueSound(): boolean {
|
||||||
|
const name = this.consumeMusicName();
|
||||||
|
this.vm.addSoundToQueue(name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private stop(): boolean {
|
||||||
|
if (this.match0(TokenType.MUSIC)) {
|
||||||
|
const fade = this.matchFade();
|
||||||
|
this.vm.stopMusic(fade);
|
||||||
|
}
|
||||||
|
if (this.match0(TokenType.SOUND)) {
|
||||||
|
const fade = this.matchFade();
|
||||||
|
this.vm.stopSound(fade);
|
||||||
|
}
|
||||||
|
if (this.match0(TokenType.SOUNDLOOP)) {
|
||||||
|
const fade = this.matchFade();
|
||||||
|
this.vm.stopSoundLoop(fade);
|
||||||
|
}
|
||||||
|
if (this.match0(TokenType.AMBIENCE)) {
|
||||||
|
const fade = this.matchFade();
|
||||||
|
this.vm.stopAmbience(fade);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private menu(): boolean {
|
||||||
|
// menu: title?
|
||||||
|
this.consume(TokenType.COLON);
|
||||||
|
|
||||||
|
let title = "";
|
||||||
|
if (this.lookMatch(0, TokenType.TEXT) && !this.lookMatch(1, TokenType.COLON)) {
|
||||||
|
title = this.consume(TokenType.TEXT).getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hasEndMenu) return false;
|
||||||
|
// Search choice elements
|
||||||
|
const menu = new Menu(title);
|
||||||
|
let pos = 0;
|
||||||
|
let level = 1; // menu nesting level
|
||||||
|
while (true) {
|
||||||
|
if (this.lookMatch(pos, TokenType.MENU) && this.lookMatch(pos + 1, TokenType.COLON)) {
|
||||||
|
level++;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (this.lookMatch(pos, TokenType.ENDMENU)) {
|
||||||
|
level--;
|
||||||
|
// Break on reaching 1-level ENDMENU
|
||||||
|
if (level <= 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level == 1) {
|
||||||
|
// Add elements from 1-level menu
|
||||||
|
if (this.lookMatch(pos, TokenType.TEXT) && this.lookMatch(pos + 1, TokenType.COLON)) {
|
||||||
|
menu.addItem(this.get(pos).getText(), this.position + pos + 2);
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lookMatch(pos, TokenType.EOF)) return false;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.vm.menu(menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private jump(): boolean {
|
||||||
|
const labelName = this.consume(TokenType.WORD).getText();
|
||||||
|
this.jumpLabel(labelName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ifStatement(): boolean {
|
||||||
|
const condition = this.expression();
|
||||||
|
this.consume(TokenType.COLON);
|
||||||
|
|
||||||
|
if (!this.hasEndIf) return false;
|
||||||
|
if (condition.eval() == 0) {
|
||||||
|
// If false, skip to next ENDIF
|
||||||
|
let pos = 0;
|
||||||
|
let level = 1; // block nesting level
|
||||||
|
while (true) {
|
||||||
|
if (this.lookMatch(pos, TokenType.IF)) {
|
||||||
|
level++;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (this.lookMatch(pos, TokenType.ELSE)) {
|
||||||
|
level--;
|
||||||
|
pos += 2; // skip ELSE and COLON
|
||||||
|
|
||||||
|
// Break on 1-level ELSE
|
||||||
|
if (level <= 0) break;
|
||||||
|
}
|
||||||
|
if (this.lookMatch(pos, TokenType.ENDIF)) {
|
||||||
|
level--;
|
||||||
|
// Break on 1-level ENDIF
|
||||||
|
if (level <= 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lookMatch(pos, TokenType.EOF)) return false;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
this.position += pos;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private expression(): Expression {
|
||||||
|
return this.orTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
private orTest(): Expression {
|
||||||
|
let expression = this.andTest();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (this.match0(TokenType.OR)) {
|
||||||
|
expression = new BinaryExpression(Operator.BOOLEAN_OR, expression, this.andTest());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
private andTest(): Expression {
|
||||||
|
let expression = this.notTest();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (this.match0(TokenType.AND)) {
|
||||||
|
expression = new BinaryExpression(Operator.BOOLEAN_AND, expression, this.notTest());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
private notTest(): Expression {
|
||||||
|
if (this.match0(TokenType.NOT)) {
|
||||||
|
return new ValueExpression(this.notTest().eval() != 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
return this.comparison();
|
||||||
|
}
|
||||||
|
|
||||||
|
private comparison(): Expression {
|
||||||
|
let expression = this.additive();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (this.lookMatch(1, TokenType.EQ)) {
|
||||||
|
if (this.match0(TokenType.EQ)) {
|
||||||
|
// ==
|
||||||
|
this.consume(TokenType.EQ);
|
||||||
|
expression = new BinaryExpression(Operator.EQUALS, expression, this.additive());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.match0(TokenType.GT)) {
|
||||||
|
// >=
|
||||||
|
this.consume(TokenType.EQ);
|
||||||
|
expression = new BinaryExpression(Operator.GTEQ, expression, this.additive());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.match0(TokenType.LT)) {
|
||||||
|
// <=
|
||||||
|
this.consume(TokenType.EQ);
|
||||||
|
expression = new BinaryExpression(Operator.LTEQ, expression, this.additive());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.match0(TokenType.EXCL)) {
|
||||||
|
// !=
|
||||||
|
this.consume(TokenType.EQ);
|
||||||
|
expression = new BinaryExpression(Operator.NOTEQUALS, expression, this.additive());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.match0(TokenType.LT)) {
|
||||||
|
expression = new BinaryExpression(Operator.LT, expression, this.additive());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.match0(TokenType.GT)) {
|
||||||
|
expression = new BinaryExpression(Operator.GT, expression, this.additive());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
private additive(): Expression {
|
||||||
|
let expression = this.unary();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (this.match0(TokenType.PLUS)) {
|
||||||
|
expression = new BinaryExpression(Operator.ADD, expression, this.unary());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.match0(TokenType.MINUS)) {
|
||||||
|
expression = new BinaryExpression(Operator.SUBTRACT, expression, this.unary());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unary(): Expression {
|
||||||
|
if (this.match0(TokenType.MINUS)) {
|
||||||
|
return new ValueExpression(-this.primary().eval());
|
||||||
|
}
|
||||||
|
if (this.match0(TokenType.PLUS)) {
|
||||||
|
return this.primary();
|
||||||
|
}
|
||||||
|
return this.primary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private primary(): Expression {
|
||||||
|
const current = this.get(0);
|
||||||
|
if (this.match(current, TokenType.NUMBER)) {
|
||||||
|
return new ValueExpression(parseFloat(current.getText()));
|
||||||
|
}
|
||||||
|
if (this.match(current, TokenType.WORD)) {
|
||||||
|
return new VariableExpression(current.getText());
|
||||||
|
}
|
||||||
|
if (this.match(current, TokenType.LPAREN)) {
|
||||||
|
const expr = this.expression();
|
||||||
|
this.match0(TokenType.RPAREN);
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
throw "Unknown expression";
|
||||||
|
}
|
||||||
|
|
||||||
|
private preScan(): void {
|
||||||
|
// Pre-scan all labels for quick jumping
|
||||||
|
for (let i = 0; i < this.tokensCount; i++) {
|
||||||
|
const current = this.tokens[i].getType();
|
||||||
|
if (current === TokenType.ENDMENU) {
|
||||||
|
this.hasEndMenu = true;
|
||||||
|
} else if (current === TokenType.ENDIF) {
|
||||||
|
this.hasEndIf = true;
|
||||||
|
} else if ((current === TokenType.LABEL) &&
|
||||||
|
((i + 2) < this.tokensCount) &&
|
||||||
|
(this.tokens[i + 2].getType() === TokenType.COLON)) {
|
||||||
|
// label word :
|
||||||
|
const token = this.tokens[i + 1];
|
||||||
|
if (token.getType() === TokenType.WORD) {
|
||||||
|
// add next command position
|
||||||
|
this.labels[token.getText()] = i + 3;
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private skipMenu(): number {
|
||||||
|
let pos = 0;
|
||||||
|
let level = 1; // menu nesting level
|
||||||
|
while (true) {
|
||||||
|
if (this.lookMatch(pos, TokenType.MENU) && this.lookMatch(pos + 1, TokenType.COLON)) {
|
||||||
|
level++;
|
||||||
|
pos += 2;
|
||||||
|
}
|
||||||
|
if (this.lookMatch(pos, TokenType.ENDMENU)) {
|
||||||
|
pos++;
|
||||||
|
level--;
|
||||||
|
// Break on reaching 1-level ENDMENU
|
||||||
|
if (level <= 0) break;
|
||||||
|
}
|
||||||
|
if (this.lookMatch(pos, TokenType.EOF)) return 0;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private skipIf(): number {
|
||||||
|
let pos = 0;
|
||||||
|
let level = 1;
|
||||||
|
while (true) {
|
||||||
|
if (this.lookMatch(pos, TokenType.IF)) level++;
|
||||||
|
else if (this.lookMatch(pos, TokenType.ENDIF)) {
|
||||||
|
pos++;
|
||||||
|
level--;
|
||||||
|
if (level <= 0) break;
|
||||||
|
} else if (this.lookMatch(pos, TokenType.EOF)) return 0;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private consumeMusicName(): string {
|
||||||
|
let name = "";
|
||||||
|
if (this.lookMatch(1, TokenType.LBRACKET)) {
|
||||||
|
// music_list["music"]
|
||||||
|
this.consume(TokenType.WORD);
|
||||||
|
this.consume(TokenType.LBRACKET);
|
||||||
|
name = this.consume(TokenType.TEXT).getText();
|
||||||
|
this.consume(TokenType.RBRACKET);
|
||||||
|
} else if (this.lookMatch(0, TokenType.TEXT)) {
|
||||||
|
name = this.consume(TokenType.TEXT).getText();
|
||||||
|
} else {
|
||||||
|
name = this.consume(TokenType.WORD).getText();
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private matchFade(): FadeInfo {
|
||||||
|
const result = new FadeInfo();
|
||||||
|
if (this.match0(TokenType.FADEIN)) {
|
||||||
|
result.fadeIn = true;
|
||||||
|
result.duration = this.consumeDouble();
|
||||||
|
} else if (this.match0(TokenType.FADEOUT)) {
|
||||||
|
result.fadeOut = true;
|
||||||
|
result.duration = this.consumeDouble();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private matchWithEffect(): string {
|
||||||
|
if (this.match0(TokenType.WITH)) {
|
||||||
|
return this.consume(TokenType.WORD).getText();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private consumeDouble(): number {
|
||||||
|
return parseFloat(this.consume(TokenType.NUMBER).getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
private consumeBoolean(): boolean {
|
||||||
|
return "true" === (this.consume(TokenType.WORD).getText().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
private match0(type: TokenType): boolean {
|
||||||
|
return this.match(this.get(0), type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private match(token: Token, type: TokenType): boolean {
|
||||||
|
if (type !== token.getType()) return false;
|
||||||
|
this.position++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private consume(type: TokenType): Token {
|
||||||
|
if (this.get(0).getType() !== type) throw "Expected token type " + type + ", but found " + this.get(0);
|
||||||
|
return this.tokens[this.position++];
|
||||||
|
}
|
||||||
|
|
||||||
|
private lookMatch(pos: number, type: TokenType): boolean {
|
||||||
|
return (type === this.get(pos).getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private get(offset: number): Token {
|
||||||
|
if (this.position + offset >= this.tokensCount) return this.EOF;
|
||||||
|
return this.tokens[this.position + offset];
|
||||||
|
}
|
||||||
|
};
|
13
src/parser/ast/index.ts
Normal file
13
src/parser/ast/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { BinaryExpression } from "./BinaryExpression";
|
||||||
|
import { Expression } from "./Expression";
|
||||||
|
import { Operator } from "./Operator";
|
||||||
|
import { ValueExpression } from "./ValueExpression";
|
||||||
|
import { VariableExpression } from "./VariableExpression";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Expression,
|
||||||
|
ValueExpression,
|
||||||
|
VariableExpression,
|
||||||
|
BinaryExpression,
|
||||||
|
Operator
|
||||||
|
};
|
7
src/utils/TextUtils.ts
Normal file
7
src/utils/TextUtils.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export class TextUtils {
|
||||||
|
public static isEmpty(str: string|null) {
|
||||||
|
if (typeof str === 'undefined') return true;
|
||||||
|
if (str === undefined) return true;
|
||||||
|
return str.length === 0;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user