UNPKG

@fmlang/tokenizer

Version:

Tokenizer for Forms Markup Language (FML)

320 lines (315 loc) 7.54 kB
// @bun // src/Result.ts class Result { value; error; constructor(value, error) { this.value = value; this.error = error; } IsError() { return this.error != null || this.value == null; } HasValue() { return this.value != null; } } // src/Token.ts class Token { token_type; lexeme; literal; line; constructor(token_type, lexeme, literal, line) { this.token_type = token_type; this.lexeme = lexeme; this.literal = literal; this.line = line; } } // src/TokenType.ts var TokenType = { IDENTIFIER: 0, STRING: 1, NUMBER: 2, MARKDOWN: 3, ASTERISK: 4, COMMA: 5, DOT: 6, MINUS: 7, PLUS: 8, SEMICOLON: 9, FORWARD_SLASH: 10, BACK_SLASH: 11, POUND: 12, LEFT_BRACE: 13, RIGHT_BRACE: 14, LEFT_BRACKET: 15, RIGHT_BRACKET: 16, LEFT_PARENTHESIS: 17, RIGHT_PARENTHESIS: 18, EXCLAMATION_POINT: 19, EXCLAMATION_POINT_EQUAL: 20, EQUAL: 21, EQUAL_EQUAL: 22, GREATER_THAN: 23, GREATER_THAN_EQUAL: 24, LESS_THAN: 25, LESS_THAN_EQUAL: 26, AND: 27, ELSE: 28, FALSE: 29, LOOP: 30, IF: 31, OR: 32, PRINT: 33, RETURN: 34, TRUE: 35, LET: 36, CONST: 37, EOL: 99, EOF: 100 }; // src/KeywordMap.ts var KeywordMap = { and: TokenType.AND, else: TokenType.ELSE, false: TokenType.FALSE, loop: TokenType.LOOP, if: TokenType.IF, or: TokenType.OR, print: TokenType.PRINT, return: TokenType.RETURN, true: TokenType.TRUE, let: TokenType.LET, const: TokenType.CONST }; // src/Tokenizer.ts class Tokenizer { tokens = []; start = 0; current = 0; line = 1; source = ""; constructor(fml) { this.source = fml; this.start = 0; this.tokens = []; this.current = 0; this.line = 1; } Tokenize() { while (!this.IsAtEnd()) { this.start = this.current; this.ScanToken(); } this.tokens.push(new Token(TokenType.EOF, "", null, this.line)); return new Result(this.tokens, null); } IsAtEnd() { return this.current >= this.source.length; } ScanToken() { let c = this.Advance(); switch (c) { case "(": this.AddToken(TokenType.LEFT_PARENTHESIS); break; case ")": this.AddToken(TokenType.RIGHT_PARENTHESIS); break; case "{": this.AddToken(TokenType.LEFT_BRACE); break; case "}": this.AddToken(TokenType.RIGHT_BRACE); break; case "[": this.AddToken(TokenType.LEFT_BRACKET); break; case "]": this.AddToken(TokenType.RIGHT_BRACKET); break; case ",": this.AddToken(TokenType.COMMA); break; case ".": this.AddToken(TokenType.DOT); break; case "-": this.AddToken(TokenType.MINUS); break; case "+": this.AddToken(TokenType.PLUS); break; case "*": this.AddToken(TokenType.ASTERISK); break; case "": this.AddToken(TokenType.SEMICOLON); break; case "!": this.AddToken(this.MatchNext("=") ? TokenType.EXCLAMATION_POINT_EQUAL : TokenType.EXCLAMATION_POINT); break; case "=": this.AddToken(this.MatchNext("=") ? TokenType.EQUAL_EQUAL : TokenType.EQUAL); break; case "<": this.AddToken(this.MatchNext("=") ? TokenType.LESS_THAN_EQUAL : TokenType.LESS_THAN); break; case ">": this.AddToken(this.MatchNext("=") ? TokenType.GREATER_THAN_EQUAL : TokenType.GREATER_THAN); break; case "/": { if (this.MatchNext("/")) { while (this.PeekNext() != ` ` && !this.IsAtEnd()) { this.Advance(); } } else { this.AddToken(TokenType.FORWARD_SLASH); } break; } case "#": { if (this.MatchNext("{")) { this.ReadMarkdown(); } this.AddToken(TokenType.POUND); break; } case '"': { this.ReadString(); break; } case " ": case "\r": case "\t": break; case ` `: this.line++; break; default: if (this.IsDigit(c)) { this.ReadNumber(); } else if (this.IsAlpha(c)) { this.ReadIdentifier(); } else { throw new Error(`Unexpected character ${c}`); } break; } } Advance() { this.current++; return this.source[this.current - 1]; } MatchNext(expected) { if (this.IsAtEnd()) return false; if (this.source[this.current] != expected) return false; this.current++; return true; } IsDigit(lexeme) { return lexeme >= "0" && lexeme <= "9"; } ReadNumber() { while (this.IsDigit(this.PeekNext())) { this.Advance(); } if (this.PeekNext() == "." && this.IsDigit(this.PeekNext(1))) { this.Advance(); while (this.IsDigit(this.PeekNext())) { this.Advance(); } } let num = parseFloat(this.source.substring(this.start, this.current)); this.AddToken(TokenType.NUMBER, num); } PeekNext(offset = 0) { if (this.IsAtEnd()) return "\x00"; return this.source[this.current + offset]; } ReadString() { while (this.PeekNext() != '"' && !this.IsAtEnd()) { let next = this.PeekNext(); if (this.PeekNext() == "\\" && this.PeekNext(1) == '"') { this.Advance(); } if (this.PeekNext() == ` `) { this.line++; } this.Advance(); } if (this.IsAtEnd()) { throw new Error("Unterminated string on line " + this.line); } this.Advance(); let value = this.source.substring(this.start + 1, this.current - 1); value = value.replace(/\\"/g, '"'); this.AddToken(TokenType.STRING, value); } IsAlpha(c) { return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c == "_"; } IsAlphaNumeric(c) { return this.IsAlpha(c) || this.IsDigit(c); } ReadIdentifier() { while (this.IsAlphaNumeric(this.PeekNext())) { this.Advance(); } let text = this.source.substring(this.start, this.current); let token_type = KeywordMap[text] || TokenType.IDENTIFIER; this.AddToken(token_type); } AddToken(token_type, literal = null) { let text = this.source.substring(this.start, this.current); this.tokens.push(new Token(token_type, text, literal, this.line)); } ReadMarkdown() { while (!this.IsAtEnd()) { if (this.PeekNext() != "}") { this.Advance(); } else { if (this.PeekNext(1) == "#") { break; } else { if (this.IsAtEnd()) { throw new Error(`Unterminated markdown on line ${this.line}: ${this.source.substring(this.start)}`); } this.Advance(); } } } if (this.IsAtEnd()) { throw new Error(`Unterminated markdown on line ${this.line}: ${this.source.substring(this.start)}`); } if (!this.MatchNext("}")) { throw new Error(`Unterminated markdown on line ${this.line}: ${this.source.substring(this.start)}`); } if (!this.MatchNext("#")) { throw new Error(`Unterminated markdown on line ${this.line}: ${this.source.substring(this.start)}`); } let value = this.source.substring(this.start + 2, this.current - 2); value = value.trim().split(` `).map((line) => line.trim()).join(` `); this.AddToken(TokenType.MARKDOWN, value); this.line += value.split(` `).length + 1; this.start = this.current; } } export { Tokenizer, TokenType, Token, Result, KeywordMap };