@newt-dev/compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/ast.d.ts +220 -0
- package/dist/src/ast.js +1 -0
- package/dist/src/codegen.d.ts +6 -0
- package/dist/src/codegen.js +266 -0
- package/dist/src/errors.d.ts +31 -0
- package/dist/src/errors.js +120 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +33 -0
- package/dist/src/lexer.d.ts +9 -0
- package/dist/src/lexer.js +285 -0
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +459 -0
- package/dist/src/validator.d.ts +3 -0
- package/dist/src/validator.js +176 -0
- package/dist/test/integration.test.d.ts +1 -0
- package/dist/test/integration.test.js +37 -0
- package/package.json +37 -0
- package/src/ast.ts +307 -0
- package/src/codegen.ts +286 -0
- package/src/errors.ts +164 -0
- package/src/index.ts +48 -0
- package/src/lexer.ts +349 -0
- package/src/parser.ts +534 -0
- package/src/validator.ts +191 -0
- package/test/integration.test.ts +42 -0
- package/tsconfig.json +18 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
export type NewtErrorSeverity = "error" | "warning";
|
|
2
|
+
|
|
3
|
+
export type NewtErrorCode =
|
|
4
|
+
| "NEWT_E001"
|
|
5
|
+
| "NEWT_E002"
|
|
6
|
+
| "NEWT_E003"
|
|
7
|
+
| "NEWT_E004"
|
|
8
|
+
| "NEWT_E005"
|
|
9
|
+
| "NEWT_E006"
|
|
10
|
+
| "NEWT_E007"
|
|
11
|
+
| "NEWT_E008"
|
|
12
|
+
| "NEWT_E009"
|
|
13
|
+
| "NEWT_E010"
|
|
14
|
+
| "NEWT_E011"
|
|
15
|
+
| "NEWT_E012"
|
|
16
|
+
| "NEWT_E013"
|
|
17
|
+
| "NEWT_E014"
|
|
18
|
+
| "NEWT_E015";
|
|
19
|
+
|
|
20
|
+
export interface NewtErrorOptions {
|
|
21
|
+
code: NewtErrorCode;
|
|
22
|
+
message: string;
|
|
23
|
+
line: number;
|
|
24
|
+
column: number;
|
|
25
|
+
sourceLine?: string;
|
|
26
|
+
suggestion?: string;
|
|
27
|
+
severity?: NewtErrorSeverity;
|
|
28
|
+
filename?: string;
|
|
29
|
+
length?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class NewtError extends Error {
|
|
33
|
+
readonly code: NewtErrorCode;
|
|
34
|
+
readonly line: number;
|
|
35
|
+
readonly column: number;
|
|
36
|
+
readonly sourceLine?: string;
|
|
37
|
+
readonly suggestion?: string;
|
|
38
|
+
readonly severity: NewtErrorSeverity;
|
|
39
|
+
readonly filename?: string;
|
|
40
|
+
readonly length: number;
|
|
41
|
+
|
|
42
|
+
constructor(options: NewtErrorOptions) {
|
|
43
|
+
super(options.message);
|
|
44
|
+
this.name = "NewtError";
|
|
45
|
+
this.code = options.code;
|
|
46
|
+
this.line = options.line;
|
|
47
|
+
this.column = options.column;
|
|
48
|
+
this.sourceLine = options.sourceLine;
|
|
49
|
+
this.suggestion = options.suggestion;
|
|
50
|
+
this.severity = options.severity ?? "error";
|
|
51
|
+
this.filename = options.filename;
|
|
52
|
+
this.length = options.length ?? 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const errorCatalog: Record<NewtErrorCode, { message: string; suggestion: string; severity?: NewtErrorSeverity }> = {
|
|
57
|
+
NEWT_E001: {
|
|
58
|
+
message: "Strings must be in quotes.",
|
|
59
|
+
suggestion: "Wrap the text in double quotes, like: reply \"Hello!\""
|
|
60
|
+
},
|
|
61
|
+
NEWT_E002: {
|
|
62
|
+
message: "Event handlers need a colon at the end.",
|
|
63
|
+
suggestion: "Try ending the line with a colon, like: on command \"hello\":"
|
|
64
|
+
},
|
|
65
|
+
NEWT_E003: {
|
|
66
|
+
message: "That is not a known event name.",
|
|
67
|
+
suggestion: "Try one of: on ready, on command, on join, on leave, on message contains, or on reaction add."
|
|
68
|
+
},
|
|
69
|
+
NEWT_E004: {
|
|
70
|
+
message: "This line needs to be inside a handler.",
|
|
71
|
+
suggestion: "Put this action under a line like: on command \"hello\":"
|
|
72
|
+
},
|
|
73
|
+
NEWT_E005: {
|
|
74
|
+
message: "[SECURITY WARNING] Never put your bot token directly in the file.",
|
|
75
|
+
suggestion: "Use: bot token from env \"DISCORD_TOKEN\"",
|
|
76
|
+
severity: "warning"
|
|
77
|
+
},
|
|
78
|
+
NEWT_E006: {
|
|
79
|
+
message: "Indentation looks off on this line.",
|
|
80
|
+
suggestion: "Use 2 or 4 spaces consistently within the file."
|
|
81
|
+
},
|
|
82
|
+
NEWT_E007: {
|
|
83
|
+
message: "This variable has not been defined yet.",
|
|
84
|
+
suggestion: "Add a let line before using it, like: let name = \"Newt\""
|
|
85
|
+
},
|
|
86
|
+
NEWT_E008: {
|
|
87
|
+
message: "Your bot needs a name.",
|
|
88
|
+
suggestion: "Add a line near the top: bot name \"MyBot\""
|
|
89
|
+
},
|
|
90
|
+
NEWT_E009: {
|
|
91
|
+
message: "Your bot needs a token source.",
|
|
92
|
+
suggestion: "Add: bot token from env \"DISCORD_TOKEN\""
|
|
93
|
+
},
|
|
94
|
+
NEWT_E010: {
|
|
95
|
+
message: "Embed colors must be hex colors.",
|
|
96
|
+
suggestion: "Use a six-digit color like: color #5865F2"
|
|
97
|
+
},
|
|
98
|
+
NEWT_E011: {
|
|
99
|
+
message: "Network requests should have an error fallback.",
|
|
100
|
+
suggestion: "Put fetch inside try: and add an on error: block."
|
|
101
|
+
},
|
|
102
|
+
NEWT_E012: {
|
|
103
|
+
message: "target only works when a command message mentions someone.",
|
|
104
|
+
suggestion: "Make sure this command is used like: !mute @Someone"
|
|
105
|
+
},
|
|
106
|
+
NEWT_E013: {
|
|
107
|
+
message: "That built-in variable does not exist.",
|
|
108
|
+
suggestion: "Use a built-in like user.name, message.content, channel.name, server.name, args, or target."
|
|
109
|
+
},
|
|
110
|
+
NEWT_E014: {
|
|
111
|
+
message: "Timer intervals must be greater than zero.",
|
|
112
|
+
suggestion: "Use a positive interval, like: every 1 hour:"
|
|
113
|
+
},
|
|
114
|
+
NEWT_E015: {
|
|
115
|
+
message: "Role names cannot be empty.",
|
|
116
|
+
suggestion: "Use a real role name, like: require role \"Moderator\""
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export function makeCatalogError(
|
|
121
|
+
code: NewtErrorCode,
|
|
122
|
+
line: number,
|
|
123
|
+
column: number,
|
|
124
|
+
sourceLine?: string,
|
|
125
|
+
overrides: Partial<NewtErrorOptions> = {}
|
|
126
|
+
): NewtError {
|
|
127
|
+
const entry = errorCatalog[code];
|
|
128
|
+
return new NewtError({
|
|
129
|
+
code,
|
|
130
|
+
line,
|
|
131
|
+
column,
|
|
132
|
+
sourceLine,
|
|
133
|
+
message: entry.message,
|
|
134
|
+
suggestion: entry.suggestion,
|
|
135
|
+
severity: entry.severity,
|
|
136
|
+
...overrides
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function formatError(error: NewtError, sourceOrFilename = ""): string {
|
|
141
|
+
const filename = error.filename ?? (sourceOrFilename.includes("\n") ? "input.newt" : sourceOrFilename || "input.newt");
|
|
142
|
+
const sourceLine = error.sourceLine ?? getSourceLine(sourceOrFilename, error.line);
|
|
143
|
+
const label = error.severity === "warning" ? "Warning" : "Error";
|
|
144
|
+
const caretOffset = Math.max(0, error.column - 1);
|
|
145
|
+
const caretLength = Math.max(1, error.length);
|
|
146
|
+
const caretLine = `${" ".repeat(caretOffset)}${"^".repeat(caretLength)}`;
|
|
147
|
+
const suggestion = error.suggestion ? ` ${error.suggestion}` : "";
|
|
148
|
+
|
|
149
|
+
return [
|
|
150
|
+
`${label} [${error.code}] on line ${error.line} in ${filename}:`,
|
|
151
|
+
"",
|
|
152
|
+
` ${sourceLine}`,
|
|
153
|
+
` ${caretLine}`,
|
|
154
|
+
`${error.message}${suggestion}`
|
|
155
|
+
].join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getSourceLine(source: string, line: number): string {
|
|
159
|
+
if (!source.includes("\n")) {
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return source.split(/\r?\n/)[line - 1] ?? "";
|
|
164
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { generate, type GeneratedProject } from "./codegen.js";
|
|
2
|
+
import { formatError, NewtError } from "./errors.js";
|
|
3
|
+
import { tokenize } from "./lexer.js";
|
|
4
|
+
import { parse } from "./parser.js";
|
|
5
|
+
import { validate } from "./validator.js";
|
|
6
|
+
|
|
7
|
+
export interface CompileSuccess extends GeneratedProject {
|
|
8
|
+
success: true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CompileFailure {
|
|
12
|
+
success: false;
|
|
13
|
+
errors: NewtError[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type CompileResult = CompileSuccess | CompileFailure;
|
|
17
|
+
|
|
18
|
+
export function compile(source: string, filename = "input.newt"): CompileResult {
|
|
19
|
+
try {
|
|
20
|
+
const tokens = tokenize(source);
|
|
21
|
+
const program = parse(tokens);
|
|
22
|
+
const errors = validate(program, source).map((error) => {
|
|
23
|
+
if (!error.filename) {
|
|
24
|
+
return new NewtError({ ...error, filename });
|
|
25
|
+
}
|
|
26
|
+
return error;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const fatalErrors = errors.filter((error) => error.severity === "error");
|
|
30
|
+
if (fatalErrors.length > 0) {
|
|
31
|
+
return { success: false, errors };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { success: true, ...generate(program) };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error instanceof NewtError) {
|
|
37
|
+
return { success: false, errors: [new NewtError({ ...error, filename })] };
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { generate } from "./codegen.js";
|
|
44
|
+
export { formatError, NewtError } from "./errors.js";
|
|
45
|
+
export { tokenize, type Token, type TokenType } from "./lexer.js";
|
|
46
|
+
export { parse } from "./parser.js";
|
|
47
|
+
export { validate } from "./validator.js";
|
|
48
|
+
export type * from "./ast.js";
|
package/src/lexer.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { makeCatalogError, NewtError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export type TokenType =
|
|
4
|
+
| "KEYWORD"
|
|
5
|
+
| "STRING"
|
|
6
|
+
| "NUMBER"
|
|
7
|
+
| "IDENTIFIER"
|
|
8
|
+
| "INDENT"
|
|
9
|
+
| "DEDENT"
|
|
10
|
+
| "NEWLINE"
|
|
11
|
+
| "COLON"
|
|
12
|
+
| "EQUALS"
|
|
13
|
+
| "LPAREN"
|
|
14
|
+
| "RPAREN"
|
|
15
|
+
| "LBRACKET"
|
|
16
|
+
| "RBRACKET"
|
|
17
|
+
| "COMMA"
|
|
18
|
+
| "DOT"
|
|
19
|
+
| "OPERATOR"
|
|
20
|
+
| "HASH_COLOR"
|
|
21
|
+
| "COMMENT"
|
|
22
|
+
| "EOF";
|
|
23
|
+
|
|
24
|
+
export interface Token {
|
|
25
|
+
type: TokenType;
|
|
26
|
+
value: string;
|
|
27
|
+
line: number;
|
|
28
|
+
column: number;
|
|
29
|
+
interpolated?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const keywords = new Set([
|
|
33
|
+
"bot",
|
|
34
|
+
"name",
|
|
35
|
+
"prefix",
|
|
36
|
+
"token",
|
|
37
|
+
"on",
|
|
38
|
+
"ready",
|
|
39
|
+
"command",
|
|
40
|
+
"join",
|
|
41
|
+
"leave",
|
|
42
|
+
"message",
|
|
43
|
+
"reaction",
|
|
44
|
+
"add",
|
|
45
|
+
"let",
|
|
46
|
+
"if",
|
|
47
|
+
"else",
|
|
48
|
+
"for",
|
|
49
|
+
"every",
|
|
50
|
+
"at",
|
|
51
|
+
"reply",
|
|
52
|
+
"say",
|
|
53
|
+
"give",
|
|
54
|
+
"remove",
|
|
55
|
+
"require",
|
|
56
|
+
"role",
|
|
57
|
+
"store",
|
|
58
|
+
"load",
|
|
59
|
+
"fetch",
|
|
60
|
+
"try",
|
|
61
|
+
"error",
|
|
62
|
+
"in",
|
|
63
|
+
"from",
|
|
64
|
+
"env",
|
|
65
|
+
"each",
|
|
66
|
+
"contains",
|
|
67
|
+
"has",
|
|
68
|
+
"or",
|
|
69
|
+
"and",
|
|
70
|
+
"not",
|
|
71
|
+
"embed",
|
|
72
|
+
"title",
|
|
73
|
+
"description",
|
|
74
|
+
"color",
|
|
75
|
+
"field",
|
|
76
|
+
"daily",
|
|
77
|
+
"mute",
|
|
78
|
+
"kick",
|
|
79
|
+
"ban",
|
|
80
|
+
"pin",
|
|
81
|
+
"delete",
|
|
82
|
+
"wait",
|
|
83
|
+
"target",
|
|
84
|
+
"channel",
|
|
85
|
+
"server",
|
|
86
|
+
"user",
|
|
87
|
+
"member",
|
|
88
|
+
"members",
|
|
89
|
+
"minutes",
|
|
90
|
+
"minute",
|
|
91
|
+
"hours",
|
|
92
|
+
"hour",
|
|
93
|
+
"seconds",
|
|
94
|
+
"second",
|
|
95
|
+
"days",
|
|
96
|
+
"day",
|
|
97
|
+
"args",
|
|
98
|
+
"random",
|
|
99
|
+
"between",
|
|
100
|
+
"round",
|
|
101
|
+
"floor",
|
|
102
|
+
"ceil",
|
|
103
|
+
"uppercase",
|
|
104
|
+
"lowercase",
|
|
105
|
+
"replace",
|
|
106
|
+
"split"
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
export function tokenize(source: string): Token[] {
|
|
110
|
+
const lexer = new Lexer(source);
|
|
111
|
+
return lexer.tokenize();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class Lexer {
|
|
115
|
+
private readonly tokens: Token[] = [];
|
|
116
|
+
private readonly indentStack = [0];
|
|
117
|
+
private indentUnit: number | undefined;
|
|
118
|
+
private readonly lines: string[];
|
|
119
|
+
|
|
120
|
+
constructor(private readonly source: string) {
|
|
121
|
+
this.lines = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
tokenize(): Token[] {
|
|
125
|
+
for (let index = 0; index < this.lines.length; index += 1) {
|
|
126
|
+
this.tokenizeLine(this.lines[index] ?? "", index + 1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const lastLine = Math.max(1, this.lines.length);
|
|
130
|
+
while (this.indentStack.length > 1) {
|
|
131
|
+
this.indentStack.pop();
|
|
132
|
+
this.push("DEDENT", "", lastLine, 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.push("EOF", "", lastLine, (this.lines[this.lines.length - 1]?.length ?? 0) + 1);
|
|
136
|
+
return this.tokens;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private tokenizeLine(rawLine: string, lineNumber: number): void {
|
|
140
|
+
const lineWithoutTrailing = rawLine.replace(/\s+$/u, "");
|
|
141
|
+
const contentStart = this.countIndent(lineWithoutTrailing);
|
|
142
|
+
const content = lineWithoutTrailing.slice(contentStart);
|
|
143
|
+
|
|
144
|
+
if (content.length === 0 || content.startsWith("#")) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.applyIndent(contentStart, lineNumber, rawLine);
|
|
149
|
+
|
|
150
|
+
let index = contentStart;
|
|
151
|
+
while (index < lineWithoutTrailing.length) {
|
|
152
|
+
const char = lineWithoutTrailing[index] ?? "";
|
|
153
|
+
const column = index + 1;
|
|
154
|
+
|
|
155
|
+
if (char === " " || char === "\t") {
|
|
156
|
+
index += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (char === "#") {
|
|
161
|
+
const maybeColor = lineWithoutTrailing.slice(index, index + 7);
|
|
162
|
+
if (/^#[0-9a-fA-F]{6}$/u.test(maybeColor)) {
|
|
163
|
+
this.push("HASH_COLOR", maybeColor, lineNumber, column);
|
|
164
|
+
index += 7;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.push("COMMENT", lineWithoutTrailing.slice(index), lineNumber, column);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (char === "\"") {
|
|
173
|
+
index = this.readString(lineWithoutTrailing, index, lineNumber);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (/[0-9]/u.test(char)) {
|
|
178
|
+
index = this.readNumber(lineWithoutTrailing, index, lineNumber);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (/[A-Za-z_]/u.test(char)) {
|
|
183
|
+
index = this.readWord(lineWithoutTrailing, index, lineNumber);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const singleToken = this.singleCharToken(char);
|
|
188
|
+
if (singleToken) {
|
|
189
|
+
this.push(singleToken, char, lineNumber, column);
|
|
190
|
+
index += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if ("+-*/<>!".includes(char)) {
|
|
195
|
+
const next = lineWithoutTrailing[index + 1];
|
|
196
|
+
const value = next === "=" ? `${char}=` : char;
|
|
197
|
+
this.push("OPERATOR", value, lineNumber, column);
|
|
198
|
+
index += value.length;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
throw new NewtError({
|
|
203
|
+
code: "NEWT_E001",
|
|
204
|
+
message: `I do not know what to do with "${char}" here.`,
|
|
205
|
+
suggestion: "Check for a missing quote or an extra symbol.",
|
|
206
|
+
line: lineNumber,
|
|
207
|
+
column,
|
|
208
|
+
sourceLine: rawLine
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.push("NEWLINE", "", lineNumber, lineWithoutTrailing.length + 1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private applyIndent(indent: number, lineNumber: number, sourceLine: string): void {
|
|
216
|
+
const current = this.indentStack[this.indentStack.length - 1] ?? 0;
|
|
217
|
+
if (indent === current) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (indent > current) {
|
|
222
|
+
const diff = indent - current;
|
|
223
|
+
if (!this.indentUnit) {
|
|
224
|
+
if (diff !== 2 && diff !== 4) {
|
|
225
|
+
throw makeCatalogError("NEWT_E006", lineNumber, 1, sourceLine);
|
|
226
|
+
}
|
|
227
|
+
this.indentUnit = diff;
|
|
228
|
+
} else if (diff % this.indentUnit !== 0) {
|
|
229
|
+
throw makeCatalogError("NEWT_E006", lineNumber, 1, sourceLine);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.indentStack.push(indent);
|
|
233
|
+
this.push("INDENT", "", lineNumber, 1);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
while (this.indentStack.length > 1 && indent < (this.indentStack[this.indentStack.length - 1] ?? 0)) {
|
|
238
|
+
this.indentStack.pop();
|
|
239
|
+
this.push("DEDENT", "", lineNumber, 1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (indent !== (this.indentStack[this.indentStack.length - 1] ?? 0)) {
|
|
243
|
+
throw makeCatalogError("NEWT_E006", lineNumber, 1, sourceLine);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private countIndent(line: string): number {
|
|
248
|
+
let count = 0;
|
|
249
|
+
for (const char of line) {
|
|
250
|
+
if (char === " ") {
|
|
251
|
+
count += 1;
|
|
252
|
+
} else if (char === "\t") {
|
|
253
|
+
count += 4;
|
|
254
|
+
} else {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return count;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private readString(line: string, start: number, lineNumber: number): number {
|
|
262
|
+
let index = start + 1;
|
|
263
|
+
let value = "";
|
|
264
|
+
let escaped = false;
|
|
265
|
+
|
|
266
|
+
while (index < line.length) {
|
|
267
|
+
const char = line[index] ?? "";
|
|
268
|
+
|
|
269
|
+
if (escaped) {
|
|
270
|
+
value += char;
|
|
271
|
+
escaped = false;
|
|
272
|
+
index += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (char === "\\") {
|
|
277
|
+
escaped = true;
|
|
278
|
+
index += 1;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (char === "\"") {
|
|
283
|
+
this.push("STRING", value, lineNumber, start + 1, /\{[^}]+\}/u.test(value));
|
|
284
|
+
return index + 1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
value += char;
|
|
288
|
+
index += 1;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
throw new NewtError({
|
|
292
|
+
code: "NEWT_E001",
|
|
293
|
+
message: "This string starts with a quote but never closes.",
|
|
294
|
+
suggestion: "Add a closing double quote at the end of the text.",
|
|
295
|
+
line: lineNumber,
|
|
296
|
+
column: start + 1,
|
|
297
|
+
sourceLine: line,
|
|
298
|
+
length: Math.max(1, line.length - start)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private readNumber(line: string, start: number, lineNumber: number): number {
|
|
303
|
+
let index = start;
|
|
304
|
+
while (/[0-9.]/u.test(line[index] ?? "")) {
|
|
305
|
+
index += 1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.push("NUMBER", line.slice(start, index), lineNumber, start + 1);
|
|
309
|
+
return index;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private readWord(line: string, start: number, lineNumber: number): number {
|
|
313
|
+
let index = start;
|
|
314
|
+
while (/[A-Za-z0-9_]/u.test(line[index] ?? "")) {
|
|
315
|
+
index += 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const value = line.slice(start, index);
|
|
319
|
+
this.push(keywords.has(value) ? "KEYWORD" : "IDENTIFIER", value, lineNumber, start + 1);
|
|
320
|
+
return index;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private singleCharToken(char: string): TokenType | undefined {
|
|
324
|
+
switch (char) {
|
|
325
|
+
case ":":
|
|
326
|
+
return "COLON";
|
|
327
|
+
case "=":
|
|
328
|
+
return "EQUALS";
|
|
329
|
+
case "(":
|
|
330
|
+
return "LPAREN";
|
|
331
|
+
case ")":
|
|
332
|
+
return "RPAREN";
|
|
333
|
+
case "[":
|
|
334
|
+
return "LBRACKET";
|
|
335
|
+
case "]":
|
|
336
|
+
return "RBRACKET";
|
|
337
|
+
case ",":
|
|
338
|
+
return "COMMA";
|
|
339
|
+
case ".":
|
|
340
|
+
return "DOT";
|
|
341
|
+
default:
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private push(type: TokenType, value: string, line: number, column: number, interpolated?: boolean): void {
|
|
347
|
+
this.tokens.push({ type, value, line, column, interpolated });
|
|
348
|
+
}
|
|
349
|
+
}
|