@styx-api/core 0.6.0 → 0.7.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.
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Tokenizer for the argtype sugar DSL.
3
+ *
4
+ * Notable points:
5
+ * - `///` doc comments are significant tokens (they attach to the next node);
6
+ * `//` line comments are discarded.
7
+ * - Template literals (`` `{ref}.png` ``) are captured whole as a single token;
8
+ * their internal `{...}` structure is parsed separately by `template.ts`.
9
+ * - Combinator/terminal words (`seq`, `int`, ...) are plain identifiers; the
10
+ * parser classifies them, so they are not reserved at the lexer level.
11
+ */
12
+
13
+ export type TokenKind =
14
+ | "ident"
15
+ | "string"
16
+ | "number"
17
+ | "template"
18
+ | "doc"
19
+ | "colon"
20
+ | "eq"
21
+ | "pipe"
22
+ | "dot"
23
+ | "comma"
24
+ | "lparen"
25
+ | "rparen"
26
+ | "eof";
27
+
28
+ export interface Token {
29
+ kind: TokenKind;
30
+ /** Raw text: identifier name, unquoted string body, number text, doc text, or template body. */
31
+ value: string;
32
+ line: number;
33
+ column: number;
34
+ }
35
+
36
+ export interface LexError {
37
+ message: string;
38
+ line: number;
39
+ column: number;
40
+ }
41
+
42
+ export interface LexResult {
43
+ tokens: Token[];
44
+ errors: LexError[];
45
+ }
46
+
47
+ const PUNCT: Record<string, TokenKind> = {
48
+ ":": "colon",
49
+ "=": "eq",
50
+ "|": "pipe",
51
+ ".": "dot",
52
+ ",": "comma",
53
+ "(": "lparen",
54
+ ")": "rparen",
55
+ };
56
+
57
+ function isIdentStart(ch: string): boolean {
58
+ return /[A-Za-z_]/.test(ch);
59
+ }
60
+
61
+ function isIdentPart(ch: string): boolean {
62
+ return /[A-Za-z0-9_]/.test(ch);
63
+ }
64
+
65
+ function isDigit(ch: string): boolean {
66
+ return ch >= "0" && ch <= "9";
67
+ }
68
+
69
+ export function lex(source: string): LexResult {
70
+ const tokens: Token[] = [];
71
+ const errors: LexError[] = [];
72
+ let i = 0;
73
+ let line = 1;
74
+ let col = 1;
75
+
76
+ const peek = (o = 0): string => source[i + o] ?? "";
77
+ const advance = (): string => {
78
+ const ch = source[i++]!;
79
+ if (ch === "\n") {
80
+ line++;
81
+ col = 1;
82
+ } else {
83
+ col++;
84
+ }
85
+ return ch;
86
+ };
87
+ const push = (kind: TokenKind, value: string, l: number, c: number): void => {
88
+ tokens.push({ kind, value, line: l, column: c });
89
+ };
90
+
91
+ while (i < source.length) {
92
+ const ch = peek();
93
+ const startLine = line;
94
+ const startCol = col;
95
+
96
+ // Whitespace (incl. CR for Windows line endings).
97
+ if (ch === " " || ch === "\t" || ch === "\r" || ch === "\n") {
98
+ advance();
99
+ continue;
100
+ }
101
+
102
+ // Comments: `///` doc (kept) vs `//` line (dropped).
103
+ if (ch === "/" && peek(1) === "/") {
104
+ const isDoc = peek(2) === "/";
105
+ // Consume the slashes.
106
+ advance();
107
+ advance();
108
+ if (isDoc) advance();
109
+ let text = "";
110
+ while (i < source.length && peek() !== "\n") text += advance();
111
+ if (isDoc) push("doc", text.trim(), startLine, startCol);
112
+ continue;
113
+ }
114
+
115
+ // Double-quoted string literal.
116
+ if (ch === '"') {
117
+ advance();
118
+ let str = "";
119
+ let closed = false;
120
+ while (i < source.length) {
121
+ const c = advance();
122
+ if (c === "\\") {
123
+ const esc = i < source.length ? advance() : "";
124
+ str += unescape(esc);
125
+ } else if (c === '"') {
126
+ closed = true;
127
+ break;
128
+ } else if (c === "\n") {
129
+ break;
130
+ } else {
131
+ str += c;
132
+ }
133
+ }
134
+ if (!closed)
135
+ errors.push({ message: "Unterminated string literal", line: startLine, column: startCol });
136
+ push("string", str, startLine, startCol);
137
+ continue;
138
+ }
139
+
140
+ // Template literal: capture the raw body between backticks. A backslash
141
+ // escapes the next character (kept raw here, unescaped by `template.ts`), so
142
+ // an escaped backtick does not close the template.
143
+ if (ch === "`") {
144
+ advance();
145
+ let body = "";
146
+ let closed = false;
147
+ while (i < source.length) {
148
+ const c = advance();
149
+ if (c === "\\") {
150
+ body += c;
151
+ if (i < source.length) body += advance();
152
+ continue;
153
+ }
154
+ if (c === "`") {
155
+ closed = true;
156
+ break;
157
+ }
158
+ body += c;
159
+ }
160
+ if (!closed)
161
+ errors.push({
162
+ message: "Unterminated template literal",
163
+ line: startLine,
164
+ column: startCol,
165
+ });
166
+ push("template", body, startLine, startCol);
167
+ continue;
168
+ }
169
+
170
+ // Number: optional leading `-`, digits, optional fractional part, optional
171
+ // exponent (e.g. `1e-05`, `2.2e-308` - tool value bounds use these).
172
+ if (isDigit(ch) || (ch === "-" && isDigit(peek(1)))) {
173
+ let num = advance(); // first char (digit or '-')
174
+ while (i < source.length && isDigit(peek())) num += advance();
175
+ if (peek() === "." && isDigit(peek(1))) {
176
+ num += advance(); // '.'
177
+ while (i < source.length && isDigit(peek())) num += advance();
178
+ }
179
+ // Exponent: e/E, optional sign, digits.
180
+ if (peek() === "e" || peek() === "E") {
181
+ const signed = peek(1) === "+" || peek(1) === "-";
182
+ if (isDigit(peek(1)) || (signed && isDigit(peek(2)))) {
183
+ num += advance(); // e/E
184
+ if (peek() === "+" || peek() === "-") num += advance();
185
+ while (i < source.length && isDigit(peek())) num += advance();
186
+ } else {
187
+ // `e`/`E` right after the digits but no exponent value follows: a
188
+ // malformed number (digits never abut a bare identifier in argtype),
189
+ // so flag it rather than silently emitting `1` + ident `e`.
190
+ num += advance(); // consume the stray e/E
191
+ errors.push({
192
+ message: `Malformed number '${num}': exponent has no digits`,
193
+ line: startLine,
194
+ column: startCol,
195
+ });
196
+ }
197
+ }
198
+ push("number", num, startLine, startCol);
199
+ continue;
200
+ }
201
+
202
+ // Identifier / keyword.
203
+ if (isIdentStart(ch)) {
204
+ let id = advance();
205
+ while (i < source.length && isIdentPart(peek())) id += advance();
206
+ push("ident", id, startLine, startCol);
207
+ continue;
208
+ }
209
+
210
+ // Punctuation.
211
+ const punct = PUNCT[ch];
212
+ if (punct) {
213
+ advance();
214
+ push(punct, ch, startLine, startCol);
215
+ continue;
216
+ }
217
+
218
+ errors.push({ message: `Unexpected character '${ch}'`, line: startLine, column: startCol });
219
+ advance();
220
+ }
221
+
222
+ push("eof", "", line, col);
223
+ return { tokens, errors };
224
+ }
225
+
226
+ /** Minimal escape handling inside double-quoted strings. */
227
+ function unescape(esc: string): string {
228
+ switch (esc) {
229
+ case "n":
230
+ return "\n";
231
+ case "t":
232
+ return "\t";
233
+ case "r":
234
+ return "\r";
235
+ case '"':
236
+ return '"';
237
+ case "\\":
238
+ return "\\";
239
+ case "":
240
+ return "";
241
+ default:
242
+ return esc;
243
+ }
244
+ }