hispano-lang 1.0.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,445 @@
1
+ /**
2
+ * Tokenizer for HispanoLang
3
+ * Converts source code into tokens for the parser
4
+ */
5
+
6
+ class Tokenizer {
7
+ constructor() {
8
+ this.source = '';
9
+ this.tokens = [];
10
+ this.current = 0;
11
+ this.startPos = 0;
12
+ this.currentLine = 1;
13
+ }
14
+
15
+ /**
16
+ * Tokenizes source code and returns a list of tokens
17
+ * @param {string} source - Source code to tokenize
18
+ * @returns {Array} List of tokens
19
+ */
20
+ tokenize(source) {
21
+ this.source = source;
22
+ this.tokens = [];
23
+ this.current = 0;
24
+
25
+ while (!this.isAtEnd()) {
26
+ this.scanToken();
27
+ }
28
+
29
+ this.tokens.push({
30
+ type: 'EOF',
31
+ lexeme: '',
32
+ literal: null,
33
+ line: this.currentLine,
34
+ });
35
+ return this.tokens;
36
+ }
37
+
38
+ /**
39
+ * Scans the next token
40
+ */
41
+ scanToken() {
42
+ this.startPos = this.current;
43
+
44
+ // Check for comments before advancing
45
+ if (
46
+ this.source[this.current] === '/' &&
47
+ this.source[this.current + 1] === '/'
48
+ ) {
49
+ this.comment();
50
+ return;
51
+ }
52
+
53
+ const char = this.advance();
54
+
55
+ switch (char) {
56
+ case ' ':
57
+ case '\r':
58
+ case '\t':
59
+ // Ignore whitespace
60
+ break;
61
+
62
+ case '\n':
63
+ this.currentLine++;
64
+ break;
65
+
66
+ case '=':
67
+ if (this.peek() === '=') {
68
+ this.advance();
69
+ this.addToken('EQUAL_EQUAL');
70
+ } else {
71
+ this.addToken('EQUAL');
72
+ }
73
+ break;
74
+
75
+ case '*':
76
+ if (this.peek() === '=') {
77
+ this.advance();
78
+ this.addToken('STAR_EQUAL');
79
+ } else {
80
+ this.addToken('STAR');
81
+ }
82
+ break;
83
+
84
+ case '/':
85
+ if (this.peek() === '=') {
86
+ this.advance();
87
+ this.addToken('SLASH_EQUAL');
88
+ } else {
89
+ this.addToken('SLASH');
90
+ }
91
+ break;
92
+
93
+ case '%':
94
+ if (this.peek() === '=') {
95
+ this.advance();
96
+ this.addToken('PERCENT_EQUAL');
97
+ } else {
98
+ this.addToken('PERCENT');
99
+ }
100
+ break;
101
+
102
+ case '>':
103
+ if (this.peek() === '=') {
104
+ this.advance();
105
+ this.addToken('GREATER_EQUAL');
106
+ } else {
107
+ this.addToken('GREATER');
108
+ }
109
+ break;
110
+
111
+ case '<':
112
+ if (this.peek() === '=') {
113
+ this.advance();
114
+ this.addToken('LESS_EQUAL');
115
+ } else {
116
+ this.addToken('LESS');
117
+ }
118
+ break;
119
+
120
+ case '!':
121
+ if (this.peek() === '=') {
122
+ this.advance();
123
+ this.addToken('BANG_EQUAL');
124
+ } else {
125
+ this.addToken('BANG');
126
+ }
127
+ break;
128
+
129
+ case '+':
130
+ if (this.peek() === '+') {
131
+ this.advance();
132
+ this.addToken('PLUS_PLUS');
133
+ } else if (this.peek() === '=') {
134
+ this.advance();
135
+ this.addToken('PLUS_EQUAL');
136
+ } else {
137
+ this.addToken('PLUS');
138
+ }
139
+ break;
140
+
141
+ case '-':
142
+ if (this.peek() === '-') {
143
+ this.advance();
144
+ this.addToken('MINUS_MINUS');
145
+ } else if (this.peek() === '=') {
146
+ this.advance();
147
+ this.addToken('MINUS_EQUAL');
148
+ } else {
149
+ this.addToken('MINUS');
150
+ }
151
+ break;
152
+
153
+ case '{':
154
+ this.addToken('LEFT_BRACE');
155
+ break;
156
+
157
+ case '}':
158
+ this.addToken('RIGHT_BRACE');
159
+ break;
160
+
161
+ case '(':
162
+ this.addToken('LEFT_PAREN');
163
+ break;
164
+
165
+ case ')':
166
+ this.addToken('RIGHT_PAREN');
167
+ break;
168
+
169
+ case ',':
170
+ this.addToken('COMMA');
171
+ break;
172
+
173
+ case ';':
174
+ this.addToken('SEMICOLON');
175
+ break;
176
+
177
+ case ':':
178
+ this.addToken('COLON');
179
+ break;
180
+
181
+ case '[':
182
+ this.addToken('LEFT_BRACKET');
183
+ break;
184
+
185
+ case ']':
186
+ this.addToken('RIGHT_BRACKET');
187
+ break;
188
+
189
+ case '.':
190
+ this.addToken('DOT');
191
+ break;
192
+
193
+ case '"':
194
+ this.string();
195
+ break;
196
+
197
+ case '/':
198
+ if (this.peek() === '/') {
199
+ this.comment();
200
+ } else {
201
+ this.addToken('SLASH');
202
+ }
203
+ break;
204
+
205
+ default:
206
+ if (this.isDigit(char)) {
207
+ this.number();
208
+ } else if (this.isAlpha(char)) {
209
+ this.identifier();
210
+ } else {
211
+ throw new Error(
212
+ `Unexpected character: ${char} at line ${this.currentLine}`
213
+ );
214
+ }
215
+ break;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Processes a string literal
221
+ */
222
+ string() {
223
+ while (this.peek() !== '"' && !this.isAtEnd()) {
224
+ if (this.peek() === '\n') this.currentLine++;
225
+ this.advance();
226
+ }
227
+
228
+ if (this.isAtEnd()) {
229
+ throw new Error('Unterminated string');
230
+ }
231
+
232
+ // Consume the closing quote
233
+ this.advance();
234
+
235
+ // Extract the string value
236
+ const value = this.source.substring(this.startPos + 1, this.current - 1);
237
+ this.addToken('STRING', value);
238
+ }
239
+
240
+ /**
241
+ * Scans a comment
242
+ */
243
+ comment() {
244
+ // Consume the second '/'
245
+ this.advance();
246
+
247
+ // Skip until end of line
248
+ while (this.peek() !== '\n' && !this.isAtEnd()) {
249
+ this.advance();
250
+ }
251
+
252
+ // Don't add comment as a token - just skip it
253
+ // The comment is now fully consumed
254
+ }
255
+
256
+ /**
257
+ * Processes a number
258
+ */
259
+ number() {
260
+ while (this.isDigit(this.peek())) {
261
+ this.advance();
262
+ }
263
+
264
+ // Look for decimal part
265
+ if (this.peek() === '.' && this.isDigit(this.peekNext())) {
266
+ // Consume the dot
267
+ this.advance();
268
+
269
+ while (this.isDigit(this.peek())) {
270
+ this.advance();
271
+ }
272
+ }
273
+
274
+ const value = this.source.substring(this.startPos, this.current);
275
+ this.addToken('NUMBER', parseFloat(value));
276
+ }
277
+
278
+ /**
279
+ * Returns the next character without advancing
280
+ * @returns {string} Next character
281
+ */
282
+ peekNext() {
283
+ if (this.current + 1 >= this.source.length) return '\0';
284
+ return this.source[this.current + 1];
285
+ }
286
+
287
+ /**
288
+ * Processes an identifier or keyword
289
+ */
290
+ identifier() {
291
+ while (this.isAlphaNumeric(this.peek())) {
292
+ this.advance();
293
+ }
294
+
295
+ const text = this.source.substring(this.startPos, this.current);
296
+
297
+ // Special handling for 'y' - only treat as AND in logical contexts
298
+ if (text === 'y') {
299
+ // Check if this is a logical context by looking at previous tokens
300
+ const prevToken = this.tokens[this.tokens.length - 1];
301
+ if (
302
+ prevToken &&
303
+ (prevToken.type === 'IDENTIFIER' ||
304
+ prevToken.type === 'NUMBER' ||
305
+ prevToken.type === 'STRING' ||
306
+ prevToken.type === 'TRUE' ||
307
+ prevToken.type === 'FALSE' ||
308
+ prevToken.type === 'RIGHT_PAREN' ||
309
+ prevToken.type === 'RIGHT_BRACKET')
310
+ ) {
311
+ // Check if the next token is a logical operator or end of expression
312
+ const nextChar = this.peek();
313
+ if (
314
+ nextChar === ' ' ||
315
+ nextChar === '\n' ||
316
+ nextChar === '\t' ||
317
+ nextChar === ')' ||
318
+ nextChar === '}' ||
319
+ nextChar === ';' ||
320
+ this.isAtEnd()
321
+ ) {
322
+ this.addToken('AND');
323
+ } else {
324
+ this.addToken('IDENTIFIER');
325
+ }
326
+ } else {
327
+ this.addToken('IDENTIFIER');
328
+ }
329
+ } else {
330
+ const type = this.getKeywordType(text);
331
+ this.addToken(type);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Determines if the identifier is a keyword
337
+ * @param {string} text - Identifier text
338
+ * @returns {string} Token type
339
+ */
340
+ getKeywordType(text) {
341
+ const keywords = {
342
+ variable: 'VARIABLE',
343
+ mostrar: 'MOSTRAR',
344
+ leer: 'LEER',
345
+ si: 'SI',
346
+ sino: 'SINO',
347
+ mientras: 'MIENTRAS',
348
+ para: 'PARA',
349
+ funcion: 'FUNCION',
350
+ retornar: 'RETORNAR',
351
+ verdadero: 'TRUE',
352
+ falso: 'FALSE',
353
+ o: 'OR',
354
+ romper: 'ROMPER',
355
+ continuar: 'CONTINUAR',
356
+ intentar: 'INTENTAR',
357
+ capturar: 'CAPTURAR',
358
+ nulo: 'NULL',
359
+ indefinido: 'UNDEFINED',
360
+ };
361
+
362
+ return keywords[text] || 'IDENTIFIER';
363
+ }
364
+
365
+ /**
366
+ * Advances the pointer and returns the previous character
367
+ * @returns {string} Previous character
368
+ */
369
+ advance() {
370
+ this.current++;
371
+ return this.source[this.current - 1];
372
+ }
373
+
374
+ /**
375
+ * Marks the start of the current token
376
+ */
377
+ get start() {
378
+ return this.current - 1;
379
+ }
380
+
381
+ /**
382
+ * Returns the current character without advancing
383
+ * @returns {string} Current character
384
+ */
385
+ peek() {
386
+ if (this.isAtEnd()) return '\0';
387
+ return this.source[this.current];
388
+ }
389
+
390
+ /**
391
+ * Checks if we have reached the end of the code
392
+ * @returns {boolean} True if we reached the end
393
+ */
394
+ isAtEnd() {
395
+ return this.current >= this.source.length;
396
+ }
397
+
398
+ /**
399
+ * Checks if a character is a digit
400
+ * @param {string} char - Character to check
401
+ * @returns {boolean} True if it is a digit
402
+ */
403
+ isDigit(char) {
404
+ return char >= '0' && char <= '9';
405
+ }
406
+
407
+ /**
408
+ * Checks if a character is alphabetic
409
+ * @param {string} char - Character to check
410
+ * @returns {boolean} True if it is alphabetic
411
+ */
412
+ isAlpha(char) {
413
+ return (
414
+ (char >= 'a' && char <= 'z') ||
415
+ (char >= 'A' && char <= 'Z') ||
416
+ char === '_'
417
+ );
418
+ }
419
+
420
+ /**
421
+ * Checks if a character is alphanumeric
422
+ * @param {string} char - Character to check
423
+ * @returns {boolean} True if it is alphanumeric
424
+ */
425
+ isAlphaNumeric(char) {
426
+ return this.isAlpha(char) || this.isDigit(char);
427
+ }
428
+
429
+ /**
430
+ * Adds a token to the list
431
+ * @param {string} type - Token type
432
+ * @param {any} literal - Literal value of the token
433
+ */
434
+ addToken(type, literal = null) {
435
+ const text = this.source.substring(this.startPos, this.current);
436
+ this.tokens.push({
437
+ type,
438
+ lexeme: text,
439
+ literal,
440
+ line: this.currentLine,
441
+ });
442
+ }
443
+ }
444
+
445
+ module.exports = Tokenizer;
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "hispano-lang",
3
+ "version": "1.0.0",
4
+ "description": "Un lenguaje de programación educativo en español para enseñar programación sin barreras de idioma",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "hispano": "./bin/hispano.js",
9
+ "hispano-lang": "./bin/hispano.js"
10
+ },
11
+ "files": [
12
+ "dist/",
13
+ "bin/",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "start": "node main.js",
19
+ "test": "node test/test.js",
20
+ "dev": "nodemon main.js",
21
+ "lint": "eslint src/ test/ --ext .js",
22
+ "format": "prettier --write src/ test/ *.js",
23
+ "coverage": "echo 'Coverage not configured yet'",
24
+ "build": "npm run build:types && npm run build:js",
25
+ "build:js": "mkdir -p dist && cp -r src/* dist/ && cp main.js dist/index.js",
26
+ "build:types": "mkdir -p dist && node scripts/generate-types.js",
27
+ "demo": "node main.js",
28
+ "prepublishOnly": "npm run build",
29
+ "prepack": "npm run build"
30
+ },
31
+ "keywords": [
32
+ "programming-language",
33
+ "education",
34
+ "spanish",
35
+ "interpreter",
36
+ "javascript",
37
+ "learning",
38
+ "español",
39
+ "lenguaje-programacion",
40
+ "educativo",
41
+ "intérprete"
42
+ ],
43
+ "author": {
44
+ "name": "Nicolas Vazquez",
45
+ "email": "nicorvazquezs@gmail.com",
46
+ "url": "https://github.com/nicvazquezdev"
47
+ },
48
+ "license": "MIT",
49
+ "engines": {
50
+ "node": ">=20.0.0"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/nicvazquezdev/hispano-lang.git"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/nicvazquezdev/hispano-lang/issues"
58
+ },
59
+ "homepage": "https://github.com/nicvazquezdev/hispano-lang#readme",
60
+ "devDependencies": {
61
+ "nodemon": "^3.1.10",
62
+ "eslint": "^8.57.0",
63
+ "prettier": "^3.2.5"
64
+ },
65
+ "dependencies": {
66
+ "readline-sync": "^1.4.10"
67
+ },
68
+ "preferGlobal": true,
69
+ "publishConfig": {
70
+ "access": "public"
71
+ }
72
+ }