mirror of
https://github.com/robonen/questlang.git
synced 2026-03-20 02:44:47 +00:00
fix: lint, type errors
test: specify types for path checks in interpreter tests test: add optional chaining in lexer tests to prevent errors test: add optional chaining in parser tests to prevent errors fix: clean up whitespace and formatting in AST and lexer files feat: enhance CLI with improved error handling and user prompts fix: update index and interpreter files for better type handling fix: clean up parser code for better readability and error handling build: enable minification in tsdown configuration
This commit is contained in:
@@ -11,9 +11,5 @@ export default antfu(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
typescript: true,
|
typescript: true,
|
||||||
rules: {
|
|
||||||
'unused-imports/no-unused-imports': 'error',
|
|
||||||
'unused-imports/no-unused-vars': 'error',
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -1,15 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "questlang-interpreter",
|
"name": "questlang-interpreter",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
|
||||||
"description": "TypeScript interpreter for QuestLang programming language",
|
"description": "TypeScript interpreter for QuestLang programming language",
|
||||||
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"questlang",
|
"questlang",
|
||||||
"interpreter",
|
"interpreter",
|
||||||
"typescript",
|
"typescript",
|
||||||
"language"
|
"language"
|
||||||
],
|
],
|
||||||
"type": "module",
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"questlang": "dist/cli.js"
|
"questlang": "dist/cli.js"
|
||||||
},
|
},
|
||||||
@@ -21,19 +25,14 @@
|
|||||||
"lint": "eslint ./src",
|
"lint": "eslint ./src",
|
||||||
"lint:fix": "eslint ./src --fix"
|
"lint:fix": "eslint ./src --fix"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@clack/prompts": "^0.11.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^5.2.1",
|
"@antfu/eslint-config": "^5.2.1",
|
||||||
"@robonen/tsconfig": "^0.0.2",
|
"@robonen/tsconfig": "^0.0.2",
|
||||||
"tsdown": "^0.14.2",
|
"tsdown": "^0.14.2",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@clack/prompts": "^0.11.0"
|
|
||||||
},
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"import": "./dist/index.js"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -18,6 +18,9 @@ importers:
|
|||||||
'@robonen/tsconfig':
|
'@robonen/tsconfig':
|
||||||
specifier: ^0.0.2
|
specifier: ^0.0.2
|
||||||
version: 0.0.2
|
version: 0.0.2
|
||||||
|
eslint:
|
||||||
|
specifier: ^9.34.0
|
||||||
|
version: 9.34.0(jiti@2.5.1)
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.14.2
|
specifier: ^0.14.2
|
||||||
version: 0.14.2(typescript@5.9.2)
|
version: 0.14.2(typescript@5.9.2)
|
||||||
|
|||||||
@@ -138,6 +138,6 @@ describe('questLang Integration', () => {
|
|||||||
interpreter.moveToNode('выбор_пути');
|
interpreter.moveToNode('выбор_пути');
|
||||||
const choices = interpreter.getAvailableChoices();
|
const choices = interpreter.getAvailableChoices();
|
||||||
expect(choices).toHaveLength(3);
|
expect(choices).toHaveLength(3);
|
||||||
expect(choices[0].text).toBe('Пойти в дешёвую шашлычную');
|
expect(choices[0]?.text).toBe('Пойти в дешёвую шашлычную');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ describe('questInterpreter', () => {
|
|||||||
|
|
||||||
expect(paths.length).toBeGreaterThan(0);
|
expect(paths.length).toBeGreaterThan(0);
|
||||||
// Should have at least two paths (налево->победа and направо)
|
// Should have at least two paths (налево->победа and направо)
|
||||||
expect(paths.some(path => path.includes('победа'))).toBe(true);
|
expect(paths.some((path: string) => path.includes('победа'))).toBe(true);
|
||||||
expect(paths.some(path => path.includes('направо'))).toBe(true);
|
expect(paths.some((path: string) => path.includes('направо'))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { Lexer } from '../lexer';
|
|
||||||
import { TokenType } from '..';
|
import { TokenType } from '..';
|
||||||
|
import { Lexer } from '../lexer';
|
||||||
|
|
||||||
describe('lexer', () => {
|
describe('lexer', () => {
|
||||||
it('should tokenize quest keywords', () => {
|
it('should tokenize quest keywords', () => {
|
||||||
@@ -8,19 +8,19 @@ describe('lexer', () => {
|
|||||||
const lexer = new Lexer(source);
|
const lexer = new Lexer(source);
|
||||||
const tokens = lexer.tokenize();
|
const tokens = lexer.tokenize();
|
||||||
|
|
||||||
expect(tokens[0].type).toBe(TokenType.QUEST);
|
expect(tokens[0]?.type).toBe(TokenType.QUEST);
|
||||||
expect(tokens[0].value).toBe('квест');
|
expect(tokens[0]?.value).toBe('квест');
|
||||||
|
|
||||||
expect(tokens[1].type).toBe(TokenType.IDENTIFIER);
|
expect(tokens[1]?.type).toBe(TokenType.IDENTIFIER);
|
||||||
expect(tokens[1].value).toBe('Тест');
|
expect(tokens[1]?.value).toBe('Тест');
|
||||||
|
|
||||||
expect(tokens[2].type).toBe(TokenType.SEMICOLON);
|
expect(tokens[2]?.type).toBe(TokenType.SEMICOLON);
|
||||||
|
|
||||||
expect(tokens[3].type).toBe(TokenType.GOAL);
|
expect(tokens[3]?.type).toBe(TokenType.GOAL);
|
||||||
expect(tokens[3].value).toBe('цель');
|
expect(tokens[3]?.value).toBe('цель');
|
||||||
|
|
||||||
expect(tokens[4].type).toBe(TokenType.STRING);
|
expect(tokens[4]?.type).toBe(TokenType.STRING);
|
||||||
expect(tokens[4].value).toBe('Описание');
|
expect(tokens[4]?.value).toBe('Описание');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tokenize strings correctly', () => {
|
it('should tokenize strings correctly', () => {
|
||||||
@@ -28,8 +28,8 @@ describe('lexer', () => {
|
|||||||
const lexer = new Lexer(source);
|
const lexer = new Lexer(source);
|
||||||
const tokens = lexer.tokenize();
|
const tokens = lexer.tokenize();
|
||||||
|
|
||||||
expect(tokens[0].type).toBe(TokenType.STRING);
|
expect(tokens[0]?.type).toBe(TokenType.STRING);
|
||||||
expect(tokens[0].value).toBe('Тестовая строка с пробелами');
|
expect(tokens[0]?.value).toBe('Тестовая строка с пробелами');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tokenize numbers', () => {
|
it('should tokenize numbers', () => {
|
||||||
@@ -37,11 +37,11 @@ describe('lexer', () => {
|
|||||||
const lexer = new Lexer(source);
|
const lexer = new Lexer(source);
|
||||||
const tokens = lexer.tokenize();
|
const tokens = lexer.tokenize();
|
||||||
|
|
||||||
expect(tokens[0].type).toBe(TokenType.NUMBER);
|
expect(tokens[0]?.type).toBe(TokenType.NUMBER);
|
||||||
expect(tokens[0].value).toBe('42');
|
expect(tokens[0]?.value).toBe('42');
|
||||||
|
|
||||||
expect(tokens[1].type).toBe(TokenType.NUMBER);
|
expect(tokens[1]?.type).toBe(TokenType.NUMBER);
|
||||||
expect(tokens[1].value).toBe('3.14');
|
expect(tokens[1]?.value).toBe('3.14');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle comments', () => {
|
it('should handle comments', () => {
|
||||||
@@ -49,10 +49,10 @@ describe('lexer', () => {
|
|||||||
const lexer = new Lexer(source);
|
const lexer = new Lexer(source);
|
||||||
const tokens = lexer.tokenize();
|
const tokens = lexer.tokenize();
|
||||||
|
|
||||||
expect(tokens[0].type).toBe(TokenType.COMMENT);
|
expect(tokens[0]?.type).toBe(TokenType.COMMENT);
|
||||||
expect(tokens[0].value).toBe('// это комментарий');
|
expect(tokens[0]?.value).toBe('// это комментарий');
|
||||||
|
|
||||||
expect(tokens[1].type).toBe(TokenType.QUEST);
|
expect(tokens[1]?.type).toBe(TokenType.QUEST);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should track line and column numbers', () => {
|
it('should track line and column numbers', () => {
|
||||||
@@ -60,11 +60,11 @@ describe('lexer', () => {
|
|||||||
const lexer = new Lexer(source);
|
const lexer = new Lexer(source);
|
||||||
const tokens = lexer.tokenize();
|
const tokens = lexer.tokenize();
|
||||||
|
|
||||||
expect(tokens[0].line).toBe(1);
|
expect(tokens[0]?.line).toBe(1);
|
||||||
expect(tokens[0].column).toBe(1);
|
expect(tokens[0]?.column).toBe(1);
|
||||||
|
|
||||||
expect(tokens[1].line).toBe(2);
|
expect(tokens[1]?.line).toBe(2);
|
||||||
expect(tokens[1].column).toBe(1);
|
expect(tokens[1]?.column).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle all symbols', () => {
|
it('should handle all symbols', () => {
|
||||||
@@ -86,7 +86,7 @@ describe('lexer', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
expectedTypes.forEach((expectedType, index) => {
|
expectedTypes.forEach((expectedType, index) => {
|
||||||
expect(tokens[index].type).toBe(expectedType);
|
expect(tokens[index]?.type).toBe(expectedType);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ describe('parser', () => {
|
|||||||
|
|
||||||
const nodes = ast.graph.nodes;
|
const nodes = ast.graph.nodes;
|
||||||
expect(Object.keys(nodes)).toHaveLength(4);
|
expect(Object.keys(nodes)).toHaveLength(4);
|
||||||
expect(nodes.старт.nodeType).toBe('начальный');
|
expect(nodes.старт?.nodeType).toBe('начальный');
|
||||||
expect(nodes.действие1.nodeType).toBe('действие');
|
expect(nodes.действие1?.nodeType).toBe('действие');
|
||||||
expect(nodes.конец1.nodeType).toBe('концовка');
|
expect(nodes.конец1?.nodeType).toBe('концовка');
|
||||||
expect(nodes.конец2.nodeType).toBe('концовка');
|
expect(nodes.конец2?.nodeType).toBe('концовка');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse action node with options', () => {
|
it('should parse action node with options', () => {
|
||||||
|
|||||||
10
src/ast.ts
10
src/ast.ts
@@ -6,7 +6,7 @@ export enum TokenType {
|
|||||||
IDENTIFIER = 'IDENTIFIER',
|
IDENTIFIER = 'IDENTIFIER',
|
||||||
STRING = 'STRING',
|
STRING = 'STRING',
|
||||||
NUMBER = 'NUMBER',
|
NUMBER = 'NUMBER',
|
||||||
|
|
||||||
// Keywords
|
// Keywords
|
||||||
QUEST = 'квест',
|
QUEST = 'квест',
|
||||||
GOAL = 'цель',
|
GOAL = 'цель',
|
||||||
@@ -19,12 +19,12 @@ export enum TokenType {
|
|||||||
TRANSITIONS = 'переходы',
|
TRANSITIONS = 'переходы',
|
||||||
OPTIONS = 'варианты',
|
OPTIONS = 'варианты',
|
||||||
TITLE = 'название',
|
TITLE = 'название',
|
||||||
|
|
||||||
// Node types
|
// Node types
|
||||||
INITIAL = 'начальный',
|
INITIAL = 'начальный',
|
||||||
ACTION = 'действие',
|
ACTION = 'действие',
|
||||||
ENDING = 'концовка',
|
ENDING = 'концовка',
|
||||||
|
|
||||||
// Symbols
|
// Symbols
|
||||||
SEMICOLON = ';',
|
SEMICOLON = ';',
|
||||||
COLON = ':',
|
COLON = ':',
|
||||||
@@ -36,12 +36,12 @@ export enum TokenType {
|
|||||||
RIGHT_BRACKET = ']',
|
RIGHT_BRACKET = ']',
|
||||||
LEFT_PAREN = '(',
|
LEFT_PAREN = '(',
|
||||||
RIGHT_PAREN = ')',
|
RIGHT_PAREN = ')',
|
||||||
|
|
||||||
// Special
|
// Special
|
||||||
EOF = 'EOF',
|
EOF = 'EOF',
|
||||||
NEWLINE = 'NEWLINE',
|
NEWLINE = 'NEWLINE',
|
||||||
COMMENT = 'COMMENT',
|
COMMENT = 'COMMENT',
|
||||||
WHITESPACE = 'WHITESPACE'
|
WHITESPACE = 'WHITESPACE',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
130
src/cli.ts
130
src/cli.ts
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import type { QuestInterpreter } from '.';
|
||||||
|
import type { InitialNode, NodeDefinition } from './ast.js';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import process from 'node:process';
|
||||||
import * as p from '@clack/prompts';
|
import * as p from '@clack/prompts';
|
||||||
import { QuestLang, type QuestInterpreter } from '.';
|
import { QuestLang } from '.';
|
||||||
import { type NodeDefinition, type InitialNode } from './ast';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beautiful command-line interface for QuestLang using clack
|
* Beautiful command-line interface for QuestLang using clack
|
||||||
@@ -14,9 +16,9 @@ class ClackCLI {
|
|||||||
*/
|
*/
|
||||||
public async run(): Promise<void> {
|
public async run(): Promise<void> {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
console.clear();
|
console.clear();
|
||||||
|
|
||||||
p.intro('🎮 QuestLang Interpreter');
|
p.intro('🎮 QuestLang Interpreter');
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
@@ -25,32 +27,32 @@ class ClackCLI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const command = args[0];
|
const command = args[0];
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'play':
|
case 'play':
|
||||||
if (args.length < 2) {
|
if (args.length < 2) {
|
||||||
p.outro('❌ Usage: questlang-clack play <file.ql>');
|
p.outro('❌ Usage: questlang-clack play <file.ql>');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
await this.playQuest(args[1]);
|
await this.playQuest(args[1]!);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'validate':
|
case 'validate':
|
||||||
if (args.length < 2) {
|
if (args.length < 2) {
|
||||||
p.outro('❌ Usage: questlang-clack validate <file.ql>');
|
p.outro('❌ Usage: questlang-clack validate <file.ql>');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
await this.validateQuest(args[1]);
|
await this.validateQuest(args[1]!);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'analyze':
|
case 'analyze':
|
||||||
if (args.length < 2) {
|
if (args.length < 2) {
|
||||||
p.outro('❌ Usage: questlang-clack analyze <file.ql>');
|
p.outro('❌ Usage: questlang-clack analyze <file.ql>');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
await this.analyzeQuest(args[1]);
|
await this.analyzeQuest(args[1]!);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
p.outro(`❌ Unknown command: ${command}`);
|
p.outro(`❌ Unknown command: ${command}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -59,7 +61,7 @@ class ClackCLI {
|
|||||||
|
|
||||||
private async showInteractiveMenu(): Promise<void> {
|
private async showInteractiveMenu(): Promise<void> {
|
||||||
const questFiles = this.findQuestFiles();
|
const questFiles = this.findQuestFiles();
|
||||||
|
|
||||||
if (questFiles.length === 0) {
|
if (questFiles.length === 0) {
|
||||||
p.outro('❌ No .ql quest files found in current directory');
|
p.outro('❌ No .ql quest files found in current directory');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -107,25 +109,26 @@ class ClackCLI {
|
|||||||
return fs.readdirSync('.')
|
return fs.readdirSync('.')
|
||||||
.filter(file => file.endsWith('.ql'))
|
.filter(file => file.endsWith('.ql'))
|
||||||
.sort();
|
.sort();
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async playQuest(filename: string): Promise<void> {
|
private async playQuest(filename: string): Promise<void> {
|
||||||
const spinner = p.spinner();
|
const spinner = p.spinner();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
spinner.start('Loading quest...');
|
spinner.start('Loading quest...');
|
||||||
|
|
||||||
const source = this.readFile(filename);
|
const source = this.readFile(filename);
|
||||||
const interpreter = QuestLang.interpret(source);
|
const interpreter = QuestLang.interpret(source);
|
||||||
|
|
||||||
// Validate first
|
// Validate first
|
||||||
const validation = interpreter.validate();
|
const validation = interpreter.validate();
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
spinner.stop('❌ Quest validation failed');
|
spinner.stop('❌ Quest validation failed');
|
||||||
|
|
||||||
p.log.error('Validation errors:');
|
p.log.error('Validation errors:');
|
||||||
validation.errors.forEach(error => p.log.error(` • ${error}`));
|
validation.errors.forEach(error => p.log.error(` • ${error}`));
|
||||||
p.outro('Fix the errors and try again');
|
p.outro('Fix the errors and try again');
|
||||||
@@ -134,12 +137,12 @@ class ClackCLI {
|
|||||||
|
|
||||||
const questInfo = interpreter.getQuestInfo();
|
const questInfo = interpreter.getQuestInfo();
|
||||||
spinner.stop('✅ Quest loaded successfully');
|
spinner.stop('✅ Quest loaded successfully');
|
||||||
|
|
||||||
p.note(`📖 ${questInfo.goal}`, `🎮 ${questInfo.name}`);
|
p.note(`📖 ${questInfo.goal}`, `🎮 ${questInfo.name}`);
|
||||||
|
|
||||||
await this.gameLoop(interpreter);
|
await this.gameLoop(interpreter);
|
||||||
|
}
|
||||||
} catch (error) {
|
catch (error) {
|
||||||
spinner.stop('❌ Error loading quest');
|
spinner.stop('❌ Error loading quest');
|
||||||
p.log.error(error instanceof Error ? error.message : String(error));
|
p.log.error(error instanceof Error ? error.message : String(error));
|
||||||
p.outro('Failed to start quest');
|
p.outro('Failed to start quest');
|
||||||
@@ -150,7 +153,7 @@ class ClackCLI {
|
|||||||
private async gameLoop(interpreter: QuestInterpreter): Promise<void> {
|
private async gameLoop(interpreter: QuestInterpreter): Promise<void> {
|
||||||
while (!interpreter.getState().isComplete) {
|
while (!interpreter.getState().isComplete) {
|
||||||
const currentNode = interpreter.getCurrentNode();
|
const currentNode = interpreter.getCurrentNode();
|
||||||
|
|
||||||
if (!currentNode) {
|
if (!currentNode) {
|
||||||
p.log.error('Current node not found');
|
p.log.error('Current node not found');
|
||||||
break;
|
break;
|
||||||
@@ -161,13 +164,13 @@ class ClackCLI {
|
|||||||
|
|
||||||
if (currentNode.nodeType === 'действие') {
|
if (currentNode.nodeType === 'действие') {
|
||||||
const choices = interpreter.getAvailableChoices();
|
const choices = interpreter.getAvailableChoices();
|
||||||
|
|
||||||
const choice = await p.select({
|
const choice = await p.select({
|
||||||
message: 'What do you want to do?',
|
message: 'What do you want to do?',
|
||||||
options: choices.map((choice, index) => ({
|
options: choices.map((choice, index) => ({
|
||||||
value: index,
|
value: index,
|
||||||
label: choice.text
|
label: choice.text,
|
||||||
}))
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (p.isCancel(choice)) {
|
if (p.isCancel(choice)) {
|
||||||
@@ -176,21 +179,28 @@ class ClackCLI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = interpreter.executeChoice(choice as number);
|
const result = interpreter.executeChoice(choice as number);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
p.log.error(`Error: ${result.error}`);
|
p.log.error(`Error: ${result.error}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (currentNode.nodeType === 'начальный') {
|
}
|
||||||
|
else if (currentNode.nodeType === 'начальный') {
|
||||||
// Auto-advance from initial nodes to first transition
|
// Auto-advance from initial nodes to first transition
|
||||||
const initialNode = currentNode as InitialNode;
|
const initialNode = currentNode as InitialNode;
|
||||||
if (initialNode.transitions && initialNode.transitions.length > 0) {
|
if (initialNode.transitions && initialNode.transitions.length > 0) {
|
||||||
const result = interpreter.moveToNode(initialNode.transitions[0]);
|
const firstTransition = initialNode.transitions[0];
|
||||||
|
if (!firstTransition) {
|
||||||
|
p.log.error('First transition is undefined');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const result = interpreter.moveToNode(firstTransition);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
p.log.error(`Error: ${result.error}`);
|
p.log.error(`Error: ${result.error}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
p.log.error('Initial node has no transitions');
|
p.log.error('Initial node has no transitions');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -203,9 +213,9 @@ class ClackCLI {
|
|||||||
const finalNode = interpreter.getCurrentNode();
|
const finalNode = interpreter.getCurrentNode();
|
||||||
p.note(
|
p.note(
|
||||||
finalNode?.description || 'Quest completed',
|
finalNode?.description || 'Quest completed',
|
||||||
`🏆 ${state.endingTitle}`
|
`🏆 ${state.endingTitle}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const playAgain = await p.confirm({
|
const playAgain = await p.confirm({
|
||||||
message: 'Would you like to play again?',
|
message: 'Would you like to play again?',
|
||||||
});
|
});
|
||||||
@@ -213,7 +223,8 @@ class ClackCLI {
|
|||||||
if (!p.isCancel(playAgain) && playAgain) {
|
if (!p.isCancel(playAgain) && playAgain) {
|
||||||
interpreter.reset();
|
interpreter.reset();
|
||||||
await this.gameLoop(interpreter);
|
await this.gameLoop(interpreter);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
p.outro('Thanks for playing! 🎉');
|
p.outro('Thanks for playing! 🎉');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,26 +232,28 @@ class ClackCLI {
|
|||||||
|
|
||||||
private async validateQuest(filename: string): Promise<void> {
|
private async validateQuest(filename: string): Promise<void> {
|
||||||
const spinner = p.spinner();
|
const spinner = p.spinner();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
spinner.start('Validating quest...');
|
spinner.start('Validating quest...');
|
||||||
|
|
||||||
const source = this.readFile(filename);
|
const source = this.readFile(filename);
|
||||||
const validation = QuestLang.validate(source);
|
const validation = QuestLang.validate(source);
|
||||||
|
|
||||||
if (validation.isValid) {
|
if (validation.isValid) {
|
||||||
spinner.stop('✅ Validation completed');
|
spinner.stop('✅ Validation completed');
|
||||||
p.log.success('Quest is valid!');
|
p.log.success('Quest is valid!');
|
||||||
p.outro('🎉 No issues found');
|
p.outro('🎉 No issues found');
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
spinner.stop('❌ Validation failed');
|
spinner.stop('❌ Validation failed');
|
||||||
|
|
||||||
p.log.error('Validation errors:');
|
p.log.error('Validation errors:');
|
||||||
validation.errors.forEach(error => p.log.error(` • ${error}`));
|
validation.errors.forEach(error => p.log.error(` • ${error}`));
|
||||||
p.outro('Fix the errors and try again');
|
p.outro('Fix the errors and try again');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
spinner.stop('❌ Error during validation');
|
spinner.stop('❌ Error during validation');
|
||||||
p.log.error(error instanceof Error ? error.message : String(error));
|
p.log.error(error instanceof Error ? error.message : String(error));
|
||||||
p.outro('Validation failed');
|
p.outro('Validation failed');
|
||||||
@@ -250,58 +263,59 @@ class ClackCLI {
|
|||||||
|
|
||||||
private async analyzeQuest(filename: string): Promise<void> {
|
private async analyzeQuest(filename: string): Promise<void> {
|
||||||
const spinner = p.spinner();
|
const spinner = p.spinner();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
spinner.start('Analyzing quest...');
|
spinner.start('Analyzing quest...');
|
||||||
|
|
||||||
const source = this.readFile(filename);
|
const source = this.readFile(filename);
|
||||||
const interpreter = QuestLang.interpret(source);
|
const interpreter = QuestLang.interpret(source);
|
||||||
const questInfo = interpreter.getQuestInfo();
|
const questInfo = interpreter.getQuestInfo();
|
||||||
|
|
||||||
spinner.stop('✅ Analysis completed');
|
spinner.stop('✅ Analysis completed');
|
||||||
|
|
||||||
const nodes = Object.values(interpreter.getProgram().graph.nodes) as NodeDefinition[];
|
const nodes = Object.values(interpreter.getProgram().graph.nodes) as NodeDefinition[];
|
||||||
const initialNodes = nodes.filter(n => n.nodeType === 'начальный');
|
const initialNodes = nodes.filter(n => n.nodeType === 'начальный');
|
||||||
const actionNodes = nodes.filter(n => n.nodeType === 'действие');
|
const actionNodes = nodes.filter(n => n.nodeType === 'действие');
|
||||||
const endingNodes = nodes.filter(n => n.nodeType === 'концовка');
|
const endingNodes = nodes.filter(n => n.nodeType === 'концовка');
|
||||||
|
|
||||||
p.note(
|
p.note(
|
||||||
`📊 Quest Analysis Results\n\n` +
|
`📊 Quest Analysis Results\n\n`
|
||||||
`Total nodes: ${nodes.length}\n` +
|
+ `Total nodes: ${nodes.length}\n`
|
||||||
` • Initial nodes: ${initialNodes.length}\n` +
|
+ ` • Initial nodes: ${initialNodes.length}\n`
|
||||||
` • Action nodes: ${actionNodes.length}\n` +
|
+ ` • Action nodes: ${actionNodes.length}\n`
|
||||||
` • Ending nodes: ${endingNodes.length}`,
|
+ ` • Ending nodes: ${endingNodes.length}`,
|
||||||
`📖 ${questInfo.name}`
|
`📖 ${questInfo.name}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Analyze paths
|
// Analyze paths
|
||||||
const paths = interpreter.getAllPaths();
|
const paths = interpreter.getAllPaths();
|
||||||
|
|
||||||
if (paths.length > 0) {
|
if (paths.length > 0) {
|
||||||
const avgPathLength = paths.reduce((sum, path) => sum + path.length, 0) / paths.length;
|
const avgPathLength = paths.reduce((sum, path) => sum + path.length, 0) / paths.length;
|
||||||
const shortestPath = Math.min(...paths.map(path => path.length));
|
const shortestPath = Math.min(...paths.map(path => path.length));
|
||||||
const longestPath = Math.max(...paths.map(path => path.length));
|
const longestPath = Math.max(...paths.map(path => path.length));
|
||||||
|
|
||||||
p.log.info('📈 Path Analysis:');
|
p.log.info('📈 Path Analysis:');
|
||||||
p.log.info(` • Possible paths: ${paths.length}`);
|
p.log.info(` • Possible paths: ${paths.length}`);
|
||||||
p.log.info(` • Average path length: ${avgPathLength.toFixed(1)} steps`);
|
p.log.info(` • Average path length: ${avgPathLength.toFixed(1)} steps`);
|
||||||
p.log.info(` • Shortest path: ${shortestPath} steps`);
|
p.log.info(` • Shortest path: ${shortestPath} steps`);
|
||||||
p.log.info(` • Longest path: ${longestPath} steps`);
|
p.log.info(` • Longest path: ${longestPath} steps`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
const validation = interpreter.validate();
|
const validation = interpreter.validate();
|
||||||
|
|
||||||
if (validation.isValid) {
|
if (validation.isValid) {
|
||||||
p.log.success('✅ Quest structure is valid');
|
p.log.success('✅ Quest structure is valid');
|
||||||
p.outro('🎉 Analysis completed successfully');
|
p.outro('🎉 Analysis completed successfully');
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
p.log.warn('⚠️ Quest has validation issues:');
|
p.log.warn('⚠️ Quest has validation issues:');
|
||||||
validation.errors.forEach(error => p.log.warn(` • ${error}`));
|
validation.errors.forEach(error => p.log.warn(` • ${error}`));
|
||||||
p.outro('Consider fixing these issues');
|
p.outro('Consider fixing these issues');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
catch (error) {
|
||||||
spinner.stop('❌ Error during analysis');
|
spinner.stop('❌ Error during analysis');
|
||||||
p.log.error(error instanceof Error ? error.message : String(error));
|
p.log.error(error instanceof Error ? error.message : String(error));
|
||||||
p.outro('Analysis failed');
|
p.outro('Analysis failed');
|
||||||
@@ -313,14 +327,14 @@ class ClackCLI {
|
|||||||
if (!fs.existsSync(filename)) {
|
if (!fs.existsSync(filename)) {
|
||||||
throw new Error(`File not found: ${filename}`);
|
throw new Error(`File not found: ${filename}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs.readFileSync(filename, 'utf-8');
|
return fs.readFileSync(filename, 'utf-8');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cli = new ClackCLI();
|
const cli = new ClackCLI();
|
||||||
|
|
||||||
cli.run().catch(error => {
|
cli.run().catch((error) => {
|
||||||
p.log.error(`Unexpected error: ${error}`);
|
p.log.error(`Unexpected error: ${error}`);
|
||||||
p.outro('❌ CLI crashed');
|
p.outro('❌ CLI crashed');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
17
src/index.ts
17
src/index.ts
@@ -1,7 +1,7 @@
|
|||||||
|
import type { QuestProgram } from './ast';
|
||||||
|
import { QuestInterpreter } from './interpreter';
|
||||||
import { Lexer } from './lexer';
|
import { Lexer } from './lexer';
|
||||||
import { Parser } from './parser';
|
import { Parser } from './parser';
|
||||||
import { QuestInterpreter } from './interpreter';
|
|
||||||
import type { QuestProgram } from './ast';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main QuestLang processor
|
* Main QuestLang processor
|
||||||
@@ -13,7 +13,7 @@ export class QuestLang {
|
|||||||
public static parse(source: string): QuestProgram {
|
public static parse(source: string): QuestProgram {
|
||||||
const lexer = new Lexer(source);
|
const lexer = new Lexer(source);
|
||||||
const tokens = lexer.tokenize();
|
const tokens = lexer.tokenize();
|
||||||
|
|
||||||
const parser = new Parser(tokens);
|
const parser = new Parser(tokens);
|
||||||
return parser.parse();
|
return parser.parse();
|
||||||
}
|
}
|
||||||
@@ -33,18 +33,17 @@ export class QuestLang {
|
|||||||
try {
|
try {
|
||||||
const interpreter = this.interpret(source);
|
const interpreter = this.interpret(source);
|
||||||
return interpreter.validate();
|
return interpreter.validate();
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errors: [error instanceof Error ? error.message : 'Unknown parsing error']
|
errors: [error instanceof Error ? error.message : 'Unknown parsing error'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export main classes
|
export * from './ast';
|
||||||
|
export { QuestInterpreter } from './interpreter';
|
||||||
export { Lexer } from './lexer';
|
export { Lexer } from './lexer';
|
||||||
export { Parser } from './parser';
|
export { Parser } from './parser';
|
||||||
export { QuestInterpreter } from './interpreter';
|
|
||||||
export * from './ast';
|
|
||||||
export * from './types';
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type {
|
import type {
|
||||||
QuestProgram,
|
|
||||||
GraphNode,
|
|
||||||
NodeDefinition,
|
|
||||||
InitialNode,
|
|
||||||
ActionNode,
|
ActionNode,
|
||||||
EndingNode,
|
EndingNode,
|
||||||
OptionChoice
|
InitialNode,
|
||||||
|
NodeDefinition,
|
||||||
|
OptionChoice,
|
||||||
|
QuestProgram,
|
||||||
} from './ast';
|
} from './ast';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,9 +30,9 @@ export interface ExecutionResult {
|
|||||||
* Visitor interface for quest nodes
|
* Visitor interface for quest nodes
|
||||||
*/
|
*/
|
||||||
export interface QuestVisitor {
|
export interface QuestVisitor {
|
||||||
visitInitialNode(node: InitialNode, state: QuestState): void;
|
visitInitialNode: (node: InitialNode, state: QuestState) => void;
|
||||||
visitActionNode(node: ActionNode, state: QuestState): void;
|
visitActionNode: (node: ActionNode, state: QuestState) => void;
|
||||||
visitEndingNode(node: EndingNode, state: QuestState): void;
|
visitEndingNode: (node: EndingNode, state: QuestState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +47,7 @@ export class QuestInterpreter {
|
|||||||
this.currentState = {
|
this.currentState = {
|
||||||
currentNode: program.graph.start,
|
currentNode: program.graph.start,
|
||||||
history: [],
|
history: [],
|
||||||
isComplete: false
|
isComplete: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ export class QuestInterpreter {
|
|||||||
return {
|
return {
|
||||||
name: this.program.name,
|
name: this.program.name,
|
||||||
goal: this.program.goal,
|
goal: this.program.goal,
|
||||||
isComplete: this.currentState.isComplete
|
isComplete: this.currentState.isComplete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,12 +100,12 @@ export class QuestInterpreter {
|
|||||||
*/
|
*/
|
||||||
public executeChoice(choiceIndex: number): ExecutionResult {
|
public executeChoice(choiceIndex: number): ExecutionResult {
|
||||||
const currentNode = this.getCurrentNode();
|
const currentNode = this.getCurrentNode();
|
||||||
|
|
||||||
if (!currentNode) {
|
if (!currentNode) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
newState: this.currentState,
|
newState: this.currentState,
|
||||||
error: `Current node '${this.currentState.currentNode}' not found`
|
error: `Current node '${this.currentState.currentNode}' not found`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@ export class QuestInterpreter {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
newState: this.currentState,
|
newState: this.currentState,
|
||||||
error: `Cannot execute choice on node type '${currentNode.nodeType}'`
|
error: `Cannot execute choice on node type '${currentNode.nodeType}'`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +124,18 @@ export class QuestInterpreter {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
newState: this.currentState,
|
newState: this.currentState,
|
||||||
error: `Invalid choice index: ${choiceIndex}. Available choices: 0-${choices.length - 1}`
|
error: `Invalid choice index: ${choiceIndex}. Available choices: 0-${choices.length - 1}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const choice = choices[choiceIndex];
|
const choice = choices[choiceIndex];
|
||||||
|
if (!choice) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
newState: this.currentState,
|
||||||
|
error: `Choice at index ${choiceIndex} is undefined`,
|
||||||
|
};
|
||||||
|
}
|
||||||
return this.moveToNode(choice.target);
|
return this.moveToNode(choice.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,12 +144,12 @@ export class QuestInterpreter {
|
|||||||
*/
|
*/
|
||||||
public moveToNode(nodeId: string): ExecutionResult {
|
public moveToNode(nodeId: string): ExecutionResult {
|
||||||
const targetNode = this.program.graph.nodes[nodeId];
|
const targetNode = this.program.graph.nodes[nodeId];
|
||||||
|
|
||||||
if (!targetNode) {
|
if (!targetNode) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
newState: this.currentState,
|
newState: this.currentState,
|
||||||
error: `Target node '${nodeId}' not found`
|
error: `Target node '${nodeId}' not found`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +158,14 @@ export class QuestInterpreter {
|
|||||||
currentNode: nodeId,
|
currentNode: nodeId,
|
||||||
history: [...this.currentState.history, this.currentState.currentNode],
|
history: [...this.currentState.history, this.currentState.currentNode],
|
||||||
isComplete: targetNode.nodeType === 'концовка',
|
isComplete: targetNode.nodeType === 'концовка',
|
||||||
endingTitle: targetNode.nodeType === 'концовка' ? (targetNode as EndingNode).title : undefined
|
endingTitle: targetNode.nodeType === 'концовка' ? (targetNode as EndingNode).title : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.currentState = newState;
|
this.currentState = newState;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
newState: { ...newState }
|
newState: { ...newState },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +176,7 @@ export class QuestInterpreter {
|
|||||||
this.currentState = {
|
this.currentState = {
|
||||||
currentNode: this.program.graph.start,
|
currentNode: this.program.graph.start,
|
||||||
history: [],
|
history: [],
|
||||||
isComplete: false
|
isComplete: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,15 +186,15 @@ export class QuestInterpreter {
|
|||||||
public getAllPaths(): string[][] {
|
public getAllPaths(): string[][] {
|
||||||
const paths: string[][] = [];
|
const paths: string[][] = [];
|
||||||
const visited = new Set<string>();
|
const visited = new Set<string>();
|
||||||
|
|
||||||
this.findPaths(this.currentState.currentNode, [this.currentState.currentNode], paths, visited);
|
this.findPaths(this.currentState.currentNode, [this.currentState.currentNode], paths, visited);
|
||||||
|
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
private findPaths(nodeId: string, currentPath: string[], allPaths: string[][], visited: Set<string>): void {
|
private findPaths(nodeId: string, currentPath: string[], allPaths: string[][], visited: Set<string>): void {
|
||||||
const node = this.program.graph.nodes[nodeId];
|
const node = this.program.graph.nodes[nodeId];
|
||||||
|
|
||||||
if (!node || visited.has(nodeId)) {
|
if (!node || visited.has(nodeId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -205,7 +211,8 @@ export class QuestInterpreter {
|
|||||||
for (const option of actionNode.options) {
|
for (const option of actionNode.options) {
|
||||||
this.findPaths(option.target, [...currentPath, option.target], allPaths, new Set(visited));
|
this.findPaths(option.target, [...currentPath, option.target], allPaths, new Set(visited));
|
||||||
}
|
}
|
||||||
} else if (node.nodeType === 'начальный') {
|
}
|
||||||
|
else if (node.nodeType === 'начальный') {
|
||||||
const initialNode = node as InitialNode;
|
const initialNode = node as InitialNode;
|
||||||
for (const transition of initialNode.transitions) {
|
for (const transition of initialNode.transitions) {
|
||||||
this.findPaths(transition, [...currentPath, transition], allPaths, new Set(visited));
|
this.findPaths(transition, [...currentPath, transition], allPaths, new Set(visited));
|
||||||
@@ -234,7 +241,8 @@ export class QuestInterpreter {
|
|||||||
errors.push(`Node '${nodeId}' references non-existent target '${option.target}'`);
|
errors.push(`Node '${nodeId}' references non-existent target '${option.target}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (node.nodeType === 'начальный') {
|
}
|
||||||
|
else if (node.nodeType === 'начальный') {
|
||||||
const initialNode = node as InitialNode;
|
const initialNode = node as InitialNode;
|
||||||
for (const transition of initialNode.transitions) {
|
for (const transition of initialNode.transitions) {
|
||||||
if (!this.program.graph.nodes[transition]) {
|
if (!this.program.graph.nodes[transition]) {
|
||||||
@@ -247,7 +255,7 @@ export class QuestInterpreter {
|
|||||||
// Check for unreachable nodes
|
// Check for unreachable nodes
|
||||||
const reachable = new Set<string>();
|
const reachable = new Set<string>();
|
||||||
this.findReachableNodes(this.program.graph.start, reachable);
|
this.findReachableNodes(this.program.graph.start, reachable);
|
||||||
|
|
||||||
for (const nodeId of nodeIds) {
|
for (const nodeId of nodeIds) {
|
||||||
if (!reachable.has(nodeId)) {
|
if (!reachable.has(nodeId)) {
|
||||||
errors.push(`Node '${nodeId}' is unreachable`);
|
errors.push(`Node '${nodeId}' is unreachable`);
|
||||||
@@ -256,15 +264,17 @@ export class QuestInterpreter {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: errors.length === 0,
|
isValid: errors.length === 0,
|
||||||
errors
|
errors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private findReachableNodes(nodeId: string, reachable: Set<string>): void {
|
private findReachableNodes(nodeId: string, reachable: Set<string>): void {
|
||||||
if (reachable.has(nodeId)) return;
|
if (reachable.has(nodeId))
|
||||||
|
return;
|
||||||
|
|
||||||
const node = this.program.graph.nodes[nodeId];
|
const node = this.program.graph.nodes[nodeId];
|
||||||
if (!node) return;
|
if (!node)
|
||||||
|
return;
|
||||||
|
|
||||||
reachable.add(nodeId);
|
reachable.add(nodeId);
|
||||||
|
|
||||||
@@ -273,7 +283,8 @@ export class QuestInterpreter {
|
|||||||
for (const option of actionNode.options) {
|
for (const option of actionNode.options) {
|
||||||
this.findReachableNodes(option.target, reachable);
|
this.findReachableNodes(option.target, reachable);
|
||||||
}
|
}
|
||||||
} else if (node.nodeType === 'начальный') {
|
}
|
||||||
|
else if (node.nodeType === 'начальный') {
|
||||||
const initialNode = node as InitialNode;
|
const initialNode = node as InitialNode;
|
||||||
for (const transition of initialNode.transitions) {
|
for (const transition of initialNode.transitions) {
|
||||||
this.findReachableNodes(transition, reachable);
|
this.findReachableNodes(transition, reachable);
|
||||||
|
|||||||
39
src/lexer.ts
39
src/lexer.ts
@@ -1,5 +1,5 @@
|
|||||||
|
import type { Token } from './ast';
|
||||||
import { TokenType } from './ast';
|
import { TokenType } from './ast';
|
||||||
import type { Token, Position } from './ast';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lexical analyzer for QuestLang
|
* Lexical analyzer for QuestLang
|
||||||
@@ -26,7 +26,7 @@ export class Lexer {
|
|||||||
['название', TokenType.TITLE],
|
['название', TokenType.TITLE],
|
||||||
['начальный', TokenType.INITIAL],
|
['начальный', TokenType.INITIAL],
|
||||||
['действие', TokenType.ACTION],
|
['действие', TokenType.ACTION],
|
||||||
['концовка', TokenType.ENDING]
|
['концовка', TokenType.ENDING],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
constructor(source: string) {
|
constructor(source: string) {
|
||||||
@@ -100,7 +100,8 @@ export class Lexer {
|
|||||||
case '/':
|
case '/':
|
||||||
if (this.match('/')) {
|
if (this.match('/')) {
|
||||||
this.scanComment(start, startLine, startColumn);
|
this.scanComment(start, startLine, startColumn);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
throw new Error(`Unexpected character: ${c} at ${startLine}:${startColumn}`);
|
throw new Error(`Unexpected character: ${c} at ${startLine}:${startColumn}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -110,9 +111,11 @@ export class Lexer {
|
|||||||
default:
|
default:
|
||||||
if (this.isDigit(c)) {
|
if (this.isDigit(c)) {
|
||||||
this.scanNumber(start, startLine, startColumn);
|
this.scanNumber(start, startLine, startColumn);
|
||||||
} else if (this.isAlpha(c)) {
|
}
|
||||||
|
else if (this.isAlpha(c)) {
|
||||||
this.scanIdentifier(start, startLine, startColumn);
|
this.scanIdentifier(start, startLine, startColumn);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
throw new Error(`Unexpected character: ${c} at ${startLine}:${startColumn}`);
|
throw new Error(`Unexpected character: ${c} at ${startLine}:${startColumn}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -182,7 +185,7 @@ export class Lexer {
|
|||||||
line: line || this.line,
|
line: line || this.line,
|
||||||
column: column || this.column,
|
column: column || this.column,
|
||||||
start: start || this.position,
|
start: start || this.position,
|
||||||
end: this.position
|
end: this.position,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,20 +197,24 @@ export class Lexer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private match(expected: string): boolean {
|
private match(expected: string): boolean {
|
||||||
if (this.isAtEnd()) return false;
|
if (this.isAtEnd())
|
||||||
if (this.source.charAt(this.position) !== expected) return false;
|
return false;
|
||||||
|
if (this.source.charAt(this.position) !== expected)
|
||||||
|
return false;
|
||||||
this.position++;
|
this.position++;
|
||||||
this.column++;
|
this.column++;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private peek(): string {
|
private peek(): string {
|
||||||
if (this.isAtEnd()) return '\0';
|
if (this.isAtEnd())
|
||||||
|
return '\0';
|
||||||
return this.source.charAt(this.position);
|
return this.source.charAt(this.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
private peekNext(): string {
|
private peekNext(): string {
|
||||||
if (this.position + 1 >= this.source.length) return '\0';
|
if (this.position + 1 >= this.source.length)
|
||||||
|
return '\0';
|
||||||
return this.source.charAt(this.position + 1);
|
return this.source.charAt(this.position + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,12 +227,12 @@ export class Lexer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isAlpha(c: string): boolean {
|
private isAlpha(c: string): boolean {
|
||||||
return (c >= 'a' && c <= 'z') ||
|
return (c >= 'a' && c <= 'z')
|
||||||
(c >= 'A' && c <= 'Z') ||
|
|| (c >= 'A' && c <= 'Z')
|
||||||
(c >= 'а' && c <= 'я') ||
|
|| (c >= 'а' && c <= 'я')
|
||||||
(c >= 'А' && c <= 'Я') ||
|
|| (c >= 'А' && c <= 'Я')
|
||||||
c === 'ё' || c === 'Ё' ||
|
|| c === 'ё' || c === 'Ё'
|
||||||
c === '_';
|
|| c === '_';
|
||||||
}
|
}
|
||||||
|
|
||||||
private isAlphaNumeric(c: string): boolean {
|
private isAlphaNumeric(c: string): boolean {
|
||||||
|
|||||||
167
src/parser.ts
167
src/parser.ts
@@ -1,17 +1,5 @@
|
|||||||
|
import type { ActionNode, EndingNode, GraphNode, InitialNode, NodeDefinition, OptionChoice, QuestProgram, Token } from './ast';
|
||||||
import { TokenType } from './ast';
|
import { TokenType } from './ast';
|
||||||
import type { Token } from './ast';
|
|
||||||
import type {
|
|
||||||
ASTNode,
|
|
||||||
QuestProgram,
|
|
||||||
GraphNode,
|
|
||||||
NodeDefinition,
|
|
||||||
InitialNode,
|
|
||||||
ActionNode,
|
|
||||||
EndingNode,
|
|
||||||
OptionChoice,
|
|
||||||
StringLiteral,
|
|
||||||
Identifier
|
|
||||||
} from './ast';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parser for QuestLang
|
* Parser for QuestLang
|
||||||
@@ -22,9 +10,9 @@ export class Parser {
|
|||||||
|
|
||||||
constructor(tokens: Token[]) {
|
constructor(tokens: Token[]) {
|
||||||
// Filter out comments and whitespace
|
// Filter out comments and whitespace
|
||||||
this.tokens = tokens.filter(token =>
|
this.tokens = tokens.filter(token =>
|
||||||
token.type !== TokenType.COMMENT &&
|
token.type !== TokenType.COMMENT
|
||||||
token.type !== TokenType.WHITESPACE
|
&& token.type !== TokenType.WHITESPACE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,15 +24,15 @@ export class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseQuest(): QuestProgram {
|
private parseQuest(): QuestProgram {
|
||||||
const questToken = this.consume(TokenType.QUEST, "Expected 'квест'");
|
const questToken = this.consume(TokenType.QUEST, 'Expected \'квест\'');
|
||||||
const name = this.consume(TokenType.IDENTIFIER, "Expected quest name").value;
|
const name = this.consume(TokenType.IDENTIFIER, 'Expected quest name').value;
|
||||||
this.consume(TokenType.SEMICOLON, "Expected ';' after quest name");
|
this.consume(TokenType.SEMICOLON, 'Expected \';\' after quest name');
|
||||||
|
|
||||||
const goal = this.parseGoal();
|
const goal = this.parseGoal();
|
||||||
const graph = this.parseGraph();
|
const graph = this.parseGraph();
|
||||||
|
|
||||||
this.consume(TokenType.END, "Expected 'конец'");
|
this.consume(TokenType.END, 'Expected \'конец\'');
|
||||||
this.consume(TokenType.SEMICOLON, "Expected ';' after 'конец'");
|
this.consume(TokenType.SEMICOLON, 'Expected \';\' after \'конец\'');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'QuestProgram',
|
type: 'QuestProgram',
|
||||||
@@ -52,20 +40,20 @@ export class Parser {
|
|||||||
goal,
|
goal,
|
||||||
graph,
|
graph,
|
||||||
line: questToken.line,
|
line: questToken.line,
|
||||||
column: questToken.column
|
column: questToken.column,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseGoal(): string {
|
private parseGoal(): string {
|
||||||
this.consume(TokenType.GOAL, "Expected 'цель'");
|
this.consume(TokenType.GOAL, 'Expected \'цель\'');
|
||||||
const goalValue = this.consume(TokenType.STRING, "Expected goal description").value;
|
const goalValue = this.consume(TokenType.STRING, 'Expected goal description').value;
|
||||||
this.consume(TokenType.SEMICOLON, "Expected ';' after goal");
|
this.consume(TokenType.SEMICOLON, 'Expected \';\' after goal');
|
||||||
return goalValue;
|
return goalValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseGraph(): GraphNode {
|
private parseGraph(): GraphNode {
|
||||||
const graphToken = this.consume(TokenType.GRAPH, "Expected 'граф'");
|
const graphToken = this.consume(TokenType.GRAPH, 'Expected \'граф\'');
|
||||||
this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'граф'");
|
this.consume(TokenType.LEFT_BRACE, 'Expected \'{\' after \'граф\'');
|
||||||
|
|
||||||
const nodes: Record<string, NodeDefinition> = {};
|
const nodes: Record<string, NodeDefinition> = {};
|
||||||
let start = '';
|
let start = '';
|
||||||
@@ -73,41 +61,43 @@ export class Parser {
|
|||||||
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
|
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
|
||||||
if (this.match(TokenType.NODES)) {
|
if (this.match(TokenType.NODES)) {
|
||||||
this.parseNodes(nodes);
|
this.parseNodes(nodes);
|
||||||
} else if (this.match(TokenType.START)) {
|
}
|
||||||
this.consume(TokenType.COLON, "Expected ':' after 'начало'");
|
else if (this.match(TokenType.START)) {
|
||||||
start = this.consume(TokenType.IDENTIFIER, "Expected start node identifier").value;
|
this.consume(TokenType.COLON, 'Expected \':\' after \'начало\'');
|
||||||
this.consume(TokenType.SEMICOLON, "Expected ';' after start node");
|
start = this.consume(TokenType.IDENTIFIER, 'Expected start node identifier').value;
|
||||||
} else {
|
this.consume(TokenType.SEMICOLON, 'Expected \';\' after start node');
|
||||||
|
}
|
||||||
|
else {
|
||||||
throw new Error(`Unexpected token in graph: ${this.peek().type} at ${this.peek().line}:${this.peek().column}`);
|
throw new Error(`Unexpected token in graph: ${this.peek().type} at ${this.peek().line}:${this.peek().column}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after graph body");
|
this.consume(TokenType.RIGHT_BRACE, 'Expected \'}\' after graph body');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'Graph',
|
type: 'Graph',
|
||||||
nodes,
|
nodes,
|
||||||
start,
|
start,
|
||||||
line: graphToken.line,
|
line: graphToken.line,
|
||||||
column: graphToken.column
|
column: graphToken.column,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseNodes(nodes: Record<string, NodeDefinition>): void {
|
private parseNodes(nodes: Record<string, NodeDefinition>): void {
|
||||||
this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'узлы'");
|
this.consume(TokenType.LEFT_BRACE, 'Expected \'{\' after \'узлы\'');
|
||||||
|
|
||||||
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
|
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
|
||||||
const nodeId = this.consume(TokenType.IDENTIFIER, "Expected node identifier").value;
|
const nodeId = this.consume(TokenType.IDENTIFIER, 'Expected node identifier').value;
|
||||||
this.consume(TokenType.COLON, "Expected ':' after node identifier");
|
this.consume(TokenType.COLON, 'Expected \':\' after node identifier');
|
||||||
this.consume(TokenType.LEFT_BRACE, "Expected '{' after node identifier");
|
this.consume(TokenType.LEFT_BRACE, 'Expected \'{\' after node identifier');
|
||||||
|
|
||||||
const node = this.parseNodeDefinition(nodeId);
|
const node = this.parseNodeDefinition(nodeId);
|
||||||
nodes[nodeId] = node;
|
nodes[nodeId] = node;
|
||||||
|
|
||||||
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after node definition");
|
this.consume(TokenType.RIGHT_BRACE, 'Expected \'}\' after node definition');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after nodes");
|
this.consume(TokenType.RIGHT_BRACE, 'Expected \'}\' after nodes');
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseNodeDefinition(id: string): NodeDefinition {
|
private parseNodeDefinition(id: string): NodeDefinition {
|
||||||
@@ -120,33 +110,41 @@ export class Parser {
|
|||||||
|
|
||||||
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
|
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
|
||||||
if (this.match(TokenType.TYPE)) {
|
if (this.match(TokenType.TYPE)) {
|
||||||
this.consume(TokenType.COLON, "Expected ':' after 'тип'");
|
this.consume(TokenType.COLON, 'Expected \':\' after \'тип\'');
|
||||||
const typeToken = this.advance();
|
const typeToken = this.advance();
|
||||||
if (typeToken.type === TokenType.INITIAL) {
|
if (typeToken.type === TokenType.INITIAL) {
|
||||||
nodeType = 'начальный';
|
nodeType = 'начальный';
|
||||||
} else if (typeToken.type === TokenType.ACTION) {
|
}
|
||||||
|
else if (typeToken.type === TokenType.ACTION) {
|
||||||
nodeType = 'действие';
|
nodeType = 'действие';
|
||||||
} else if (typeToken.type === TokenType.ENDING) {
|
}
|
||||||
|
else if (typeToken.type === TokenType.ENDING) {
|
||||||
nodeType = 'концовка';
|
nodeType = 'концовка';
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
throw new Error(`Invalid node type: ${typeToken.value} at ${typeToken.line}:${typeToken.column}`);
|
throw new Error(`Invalid node type: ${typeToken.value} at ${typeToken.line}:${typeToken.column}`);
|
||||||
}
|
}
|
||||||
this.consume(TokenType.SEMICOLON, "Expected ';' after node type");
|
this.consume(TokenType.SEMICOLON, 'Expected \';\' after node type');
|
||||||
} else if (this.match(TokenType.DESCRIPTION)) {
|
}
|
||||||
this.consume(TokenType.COLON, "Expected ':' after 'описание'");
|
else if (this.match(TokenType.DESCRIPTION)) {
|
||||||
description = this.consume(TokenType.STRING, "Expected description string").value;
|
this.consume(TokenType.COLON, 'Expected \':\' after \'описание\'');
|
||||||
this.consume(TokenType.SEMICOLON, "Expected ';' after description");
|
description = this.consume(TokenType.STRING, 'Expected description string').value;
|
||||||
} else if (this.match(TokenType.TRANSITIONS)) {
|
this.consume(TokenType.SEMICOLON, 'Expected \';\' after description');
|
||||||
this.consume(TokenType.COLON, "Expected ':' after 'переходы'");
|
}
|
||||||
|
else if (this.match(TokenType.TRANSITIONS)) {
|
||||||
|
this.consume(TokenType.COLON, 'Expected \':\' after \'переходы\'');
|
||||||
this.parseTransitions(transitions);
|
this.parseTransitions(transitions);
|
||||||
} else if (this.match(TokenType.OPTIONS)) {
|
}
|
||||||
this.consume(TokenType.COLON, "Expected ':' after 'варианты'");
|
else if (this.match(TokenType.OPTIONS)) {
|
||||||
|
this.consume(TokenType.COLON, 'Expected \':\' after \'варианты\'');
|
||||||
this.parseOptions(options);
|
this.parseOptions(options);
|
||||||
} else if (this.match(TokenType.TITLE)) {
|
}
|
||||||
this.consume(TokenType.COLON, "Expected ':' after 'название'");
|
else if (this.match(TokenType.TITLE)) {
|
||||||
title = this.consume(TokenType.STRING, "Expected title string").value;
|
this.consume(TokenType.COLON, 'Expected \':\' after \'название\'');
|
||||||
this.consume(TokenType.SEMICOLON, "Expected ';' after title");
|
title = this.consume(TokenType.STRING, 'Expected title string').value;
|
||||||
} else {
|
this.consume(TokenType.SEMICOLON, 'Expected \';\' after title');
|
||||||
|
}
|
||||||
|
else {
|
||||||
throw new Error(`Unexpected token in node definition: ${this.peek().type} at ${this.peek().line}:${this.peek().column}`);
|
throw new Error(`Unexpected token in node definition: ${this.peek().type} at ${this.peek().line}:${this.peek().column}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,7 +155,7 @@ export class Parser {
|
|||||||
nodeType,
|
nodeType,
|
||||||
description,
|
description,
|
||||||
line: startToken.line,
|
line: startToken.line,
|
||||||
column: startToken.column
|
column: startToken.column,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (nodeType) {
|
switch (nodeType) {
|
||||||
@@ -165,61 +163,61 @@ export class Parser {
|
|||||||
return {
|
return {
|
||||||
...baseNode,
|
...baseNode,
|
||||||
type: 'InitialNode',
|
type: 'InitialNode',
|
||||||
transitions
|
transitions,
|
||||||
} as InitialNode;
|
} as InitialNode;
|
||||||
case 'действие':
|
case 'действие':
|
||||||
return {
|
return {
|
||||||
...baseNode,
|
...baseNode,
|
||||||
type: 'ActionNode',
|
type: 'ActionNode',
|
||||||
options
|
options,
|
||||||
} as ActionNode;
|
} as ActionNode;
|
||||||
case 'концовка':
|
case 'концовка':
|
||||||
return {
|
return {
|
||||||
...baseNode,
|
...baseNode,
|
||||||
type: 'EndingNode',
|
type: 'EndingNode',
|
||||||
title
|
title,
|
||||||
} as EndingNode;
|
} as EndingNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseTransitions(transitions: string[]): void {
|
private parseTransitions(transitions: string[]): void {
|
||||||
this.consume(TokenType.LEFT_BRACKET, "Expected '[' for transitions");
|
this.consume(TokenType.LEFT_BRACKET, 'Expected \'[\' for transitions');
|
||||||
|
|
||||||
if (!this.check(TokenType.RIGHT_BRACKET)) {
|
if (!this.check(TokenType.RIGHT_BRACKET)) {
|
||||||
do {
|
do {
|
||||||
const transition = this.consume(TokenType.IDENTIFIER, "Expected transition identifier").value;
|
const transition = this.consume(TokenType.IDENTIFIER, 'Expected transition identifier').value;
|
||||||
transitions.push(transition);
|
transitions.push(transition);
|
||||||
} while (this.match(TokenType.COMMA));
|
} while (this.match(TokenType.COMMA));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after transitions");
|
this.consume(TokenType.RIGHT_BRACKET, 'Expected \']\' after transitions');
|
||||||
this.consume(TokenType.SEMICOLON, "Expected ';' after transitions");
|
this.consume(TokenType.SEMICOLON, 'Expected \';\' after transitions');
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseOptions(options: OptionChoice[]): void {
|
private parseOptions(options: OptionChoice[]): void {
|
||||||
this.consume(TokenType.LEFT_BRACKET, "Expected '[' for options");
|
this.consume(TokenType.LEFT_BRACKET, 'Expected \'[\' for options');
|
||||||
|
|
||||||
if (!this.check(TokenType.RIGHT_BRACKET)) {
|
if (!this.check(TokenType.RIGHT_BRACKET)) {
|
||||||
do {
|
do {
|
||||||
const optionToken = this.peek();
|
const optionToken = this.peek();
|
||||||
this.consume(TokenType.LEFT_PAREN, "Expected '(' for option");
|
this.consume(TokenType.LEFT_PAREN, 'Expected \'(\' for option');
|
||||||
const text = this.consume(TokenType.STRING, "Expected option text").value;
|
const text = this.consume(TokenType.STRING, 'Expected option text').value;
|
||||||
this.consume(TokenType.COMMA, "Expected ',' in option");
|
this.consume(TokenType.COMMA, 'Expected \',\' in option');
|
||||||
const target = this.consume(TokenType.IDENTIFIER, "Expected target identifier").value;
|
const target = this.consume(TokenType.IDENTIFIER, 'Expected target identifier').value;
|
||||||
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after option");
|
this.consume(TokenType.RIGHT_PAREN, 'Expected \')\' after option');
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
type: 'OptionChoice',
|
type: 'OptionChoice',
|
||||||
text,
|
text,
|
||||||
target,
|
target,
|
||||||
line: optionToken.line,
|
line: optionToken.line,
|
||||||
column: optionToken.column
|
column: optionToken.column,
|
||||||
});
|
});
|
||||||
} while (this.match(TokenType.COMMA));
|
} while (this.match(TokenType.COMMA));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after options");
|
this.consume(TokenType.RIGHT_BRACKET, 'Expected \']\' after options');
|
||||||
this.consume(TokenType.SEMICOLON, "Expected ';' after options");
|
this.consume(TokenType.SEMICOLON, 'Expected \';\' after options');
|
||||||
}
|
}
|
||||||
|
|
||||||
private match(...types: TokenType[]): boolean {
|
private match(...types: TokenType[]): boolean {
|
||||||
@@ -233,12 +231,14 @@ export class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private check(type: TokenType): boolean {
|
private check(type: TokenType): boolean {
|
||||||
if (this.isAtEnd()) return false;
|
if (this.isAtEnd())
|
||||||
|
return false;
|
||||||
return this.peek().type === type;
|
return this.peek().type === type;
|
||||||
}
|
}
|
||||||
|
|
||||||
private advance(): Token {
|
private advance(): Token {
|
||||||
if (!this.isAtEnd()) this.current++;
|
if (!this.isAtEnd())
|
||||||
|
this.current++;
|
||||||
return this.previous();
|
return this.previous();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,16 +247,17 @@ export class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private peek(): Token {
|
private peek(): Token {
|
||||||
return this.tokens[this.current];
|
return this.tokens[this.current] || { type: TokenType.EOF, value: '', line: 0, column: 0, start: 0, end: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
private previous(): Token {
|
private previous(): Token {
|
||||||
return this.tokens[this.current - 1];
|
return this.tokens[this.current - 1] || { type: TokenType.EOF, value: '', line: 0, column: 0, start: 0, end: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
private consume(type: TokenType, message: string): Token {
|
private consume(type: TokenType, message: string): Token {
|
||||||
if (this.check(type)) return this.advance();
|
if (this.check(type))
|
||||||
|
return this.advance();
|
||||||
|
|
||||||
const current = this.peek();
|
const current = this.peek();
|
||||||
throw new Error(`${message}. Got ${current.type} at ${current.line}:${current.column}`);
|
throw new Error(`${message}. Got ${current.type} at ${current.line}:${current.column}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ export default defineConfig([
|
|||||||
{
|
{
|
||||||
entry: 'src/cli.ts',
|
entry: 'src/cli.ts',
|
||||||
noExternal: ['@clack/prompts'],
|
noExternal: ['@clack/prompts'],
|
||||||
|
minify: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user