lny-interpreter 0.99.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/.vscode/launch.json +19 -0
- package/README.md +96 -0
- package/book_ALT/LennySmartIcon1.png +0 -0
- package/book_ALT/chapter01.md +141 -0
- package/book_ALT/chapter02.md +48 -0
- package/book_ALT/chapter03.md +28 -0
- package/book_ALT/chapter04.md +19 -0
- package/book_ALT/chapter05.md +21 -0
- package/book_ALT/chapter06.md +167 -0
- package/book_ALT/chapter07.md +232 -0
- package/book_ALT/chapter08.md +200 -0
- package/book_ALT/chapter09.md +175 -0
- package/book_ALT/chapter10.md +196 -0
- package/book_ALT/deckblatt.md +60 -0
- package/book_ALT/images/Lenny01.png +0 -0
- package/book_ALT/images/Lenny02.png +0 -0
- package/book_ALT/images/Lenny03.png +0 -0
- package/book_ALT/images/Lenny04.png +0 -0
- package/book_ALT/images/Lenny05.png +0 -0
- package/book_ALT/images/Lenny06.png +0 -0
- package/book_ALT/images/Lenny07.png +0 -0
- package/book_ALT/images/Lenny08.png +0 -0
- package/book_ALT/images/Lenny09.png +0 -0
- package/book_ALT/images/Lenny10.png +0 -0
- package/book_ALT/images/Lenny11.png +0 -0
- package/book_ALT/images/Lenny12.png +0 -0
- package/book_ALT/images/Lenny13.png +0 -0
- package/book_ALT/images/Lenny14.png +0 -0
- package/book_ALT/images/Lenny15.png +0 -0
- package/book_ALT/lehrerheft.md +255 -0
- package/dist/ast/ast.d.ts +126 -0
- package/dist/ast/ast.d.ts.map +1 -0
- package/dist/ast/ast.js +3 -0
- package/dist/ast/ast.js.map +1 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +1067 -0
- package/dist/browser.js.map +1 -0
- package/dist/examples.d.ts +11 -0
- package/dist/examples.d.ts.map +1 -0
- package/dist/examples.js +65 -0
- package/dist/examples.js.map +1 -0
- package/dist/i18n/i18n.d.ts +18 -0
- package/dist/i18n/i18n.d.ts.map +1 -0
- package/dist/i18n/i18n.js +505 -0
- package/dist/i18n/i18n.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/interpreter/interpreter.d.ts +40 -0
- package/dist/interpreter/interpreter.d.ts.map +1 -0
- package/dist/interpreter/interpreter.js +415 -0
- package/dist/interpreter/interpreter.js.map +1 -0
- package/dist/lexer/lexer.d.ts +87 -0
- package/dist/lexer/lexer.d.ts.map +1 -0
- package/dist/lexer/lexer.js +425 -0
- package/dist/lexer/lexer.js.map +1 -0
- package/dist/lny-config.yml +6 -0
- package/dist/parser/parser.d.ts +50 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/parser/parser.js +605 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/repl.d.ts +3 -0
- package/dist/repl.d.ts.map +1 -0
- package/dist/repl.js +472 -0
- package/dist/repl.js.map +1 -0
- package/dist/run-file.d.ts +2 -0
- package/dist/run-file.d.ts.map +1 -0
- package/dist/run-file.js +140 -0
- package/dist/run-file.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +72 -0
- package/dist/test.js.map +1 -0
- package/dist/world/world.d.ts +103 -0
- package/dist/world/world.d.ts.map +1 -0
- package/dist/world/world.js +416 -0
- package/dist/world/world.js.map +1 -0
- package/docs/About.md +19 -0
- package/docs/INTERPRETER.md +240 -0
- package/docs/README.md +68 -0
- package/docs/i18n/README.md +20 -0
- package/docs/lny-referenz.md +161 -0
- package/docs/lny_ueberblick.md +33 -0
- package/examples/00-demo.lny +3 -0
- package/examples/01-variable-assignment.lny +4 -0
- package/examples/02-conditional.lny +7 -0
- package/examples/03-while-loop.lny +6 -0
- package/examples/04-for-loop.lny +5 -0
- package/examples/05-lenny-movement.lny +10 -0
- package/examples/06-procedure.lny +6 -0
- package/examples/fuer-range-1.lny +4 -0
- package/examples/fuer-range-2.lny +6 -0
- package/examples/fuer-range-3.lny +6 -0
- package/examples/fuer-range-4.lny +4 -0
- package/examples/fuer-range-5.lny +6 -0
- package/examples/solange-verschachtelt.lny +12 -0
- package/favicon.ico +0 -0
- package/index.html +108 -0
- package/jest.config.js +7 -0
- package/lny-config.yml +6 -0
- package/package.json +37 -0
- package/settings.json +4 -0
- package/src/ast/ast.ts +182 -0
- package/src/browser.ts +1274 -0
- package/src/examples.ts +68 -0
- package/src/i18n/i18n.ts +537 -0
- package/src/index.ts +6 -0
- package/src/interpreter/interpreter.ts +453 -0
- package/src/lexer/lexer.ts +493 -0
- package/src/parser/parser.ts +711 -0
- package/src/repl.ts +496 -0
- package/src/test.ts +71 -0
- package/src/world/world.ts +512 -0
- package/style.css +315 -0
- package/test1.lny +2 -0
- package/tests/interpreter.test.ts +42 -0
- package/tests/parser.test.ts +18 -0
- package/tsconfig.json +19 -0
- package/vercel.json +7 -0
- package/vscode-lny/.vscode/launch.json +13 -0
- package/vscode-lny/.vscodeignore +3 -0
- package/vscode-lny/README.md +83 -0
- package/vscode-lny/language-configuration.json +19 -0
- package/vscode-lny/license.txt +21 -0
- package/vscode-lny/lny-language-0.1.0.vsix +0 -0
- package/vscode-lny/package.json +41 -0
- package/vscode-lny/snippets/lny.json +136 -0
- package/vscode-lny/syntaxes/lny.tmLanguage.json +85 -0
package/dist/browser.js
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const parser_1 = require("./parser/parser");
|
|
4
|
+
const interpreter_1 = require("./interpreter/interpreter");
|
|
5
|
+
const world_1 = require("./world/world");
|
|
6
|
+
const uiStrings = {
|
|
7
|
+
de: {
|
|
8
|
+
placeholder: 'Schreib hier deinen Lny-Code...',
|
|
9
|
+
executing: 'Fuehre Code aus...',
|
|
10
|
+
finished: 'Ausfuehrung abgeschlossen.',
|
|
11
|
+
errorLabel: 'Fehler',
|
|
12
|
+
helpTitle: 'Kurzreferenz',
|
|
13
|
+
exampleLoaded: 'Beispiel geladen',
|
|
14
|
+
exampleLoadFailed: 'Beispiel konnte nicht geladen werden',
|
|
15
|
+
stateTitle: 'Lenny Status',
|
|
16
|
+
fieldSize: 'Feldgroesse',
|
|
17
|
+
pos: 'Pos',
|
|
18
|
+
dir: 'Richtung',
|
|
19
|
+
bones: 'Knochen',
|
|
20
|
+
runs: 'Laeufe',
|
|
21
|
+
saveFile: 'Speichern',
|
|
22
|
+
savePrompt: 'Dateiname (ohne Erweiterung):',
|
|
23
|
+
loadFile: 'Laden',
|
|
24
|
+
fileLoaded: 'Datei geladen',
|
|
25
|
+
fileLoadFailed: 'Datei konnte nicht geladen werden',
|
|
26
|
+
},
|
|
27
|
+
en: {
|
|
28
|
+
placeholder: 'Write your Lny code here...',
|
|
29
|
+
executing: 'Executing code...',
|
|
30
|
+
finished: 'Execution finished.',
|
|
31
|
+
errorLabel: 'Error',
|
|
32
|
+
helpTitle: 'Quick Reference',
|
|
33
|
+
exampleLoaded: 'Example loaded',
|
|
34
|
+
exampleLoadFailed: 'Could not load example',
|
|
35
|
+
stateTitle: 'Lenny State',
|
|
36
|
+
fieldSize: 'Field Size',
|
|
37
|
+
pos: 'Pos',
|
|
38
|
+
dir: 'Direction',
|
|
39
|
+
bones: 'Bones',
|
|
40
|
+
runs: 'Runs',
|
|
41
|
+
saveFile: 'Save',
|
|
42
|
+
savePrompt: 'File name (without extension):',
|
|
43
|
+
loadFile: 'Load',
|
|
44
|
+
fileLoaded: 'File loaded',
|
|
45
|
+
fileLoadFailed: 'Could not load file',
|
|
46
|
+
},
|
|
47
|
+
fr: {
|
|
48
|
+
placeholder: 'Ecrivez votre code Lny ici...',
|
|
49
|
+
executing: 'Execution du code...',
|
|
50
|
+
finished: 'Execution terminee.',
|
|
51
|
+
errorLabel: 'Erreur',
|
|
52
|
+
helpTitle: 'Reference Rapide',
|
|
53
|
+
exampleLoaded: 'Exemple charge',
|
|
54
|
+
exampleLoadFailed: 'Impossible de charger l\'exemple',
|
|
55
|
+
stateTitle: 'Etat de Lenny',
|
|
56
|
+
fieldSize: 'Taille',
|
|
57
|
+
pos: 'Pos',
|
|
58
|
+
dir: 'Direction',
|
|
59
|
+
bones: 'Os',
|
|
60
|
+
runs: 'Courses',
|
|
61
|
+
saveFile: 'Sauvegarder',
|
|
62
|
+
savePrompt: 'Nom du fichier (sans extension) :',
|
|
63
|
+
loadFile: 'Charger',
|
|
64
|
+
fileLoaded: 'Fichier charge',
|
|
65
|
+
fileLoadFailed: 'Impossible de charger le fichier',
|
|
66
|
+
},
|
|
67
|
+
es: {
|
|
68
|
+
placeholder: 'Escribe tu codigo Lny aqui...',
|
|
69
|
+
executing: 'Ejecutando codigo...',
|
|
70
|
+
finished: 'Ejecucion finalizada.',
|
|
71
|
+
errorLabel: 'Error',
|
|
72
|
+
helpTitle: 'Referencia Rapida',
|
|
73
|
+
exampleLoaded: 'Ejemplo cargado',
|
|
74
|
+
exampleLoadFailed: 'No se pudo cargar el ejemplo',
|
|
75
|
+
stateTitle: 'Estado de Lenny',
|
|
76
|
+
fieldSize: 'Tamano',
|
|
77
|
+
pos: 'Pos',
|
|
78
|
+
dir: 'Direccion',
|
|
79
|
+
bones: 'Huesos',
|
|
80
|
+
runs: 'Carreras',
|
|
81
|
+
saveFile: 'Guardar',
|
|
82
|
+
savePrompt: 'Nombre de archivo (sin extensión):',
|
|
83
|
+
loadFile: 'Cargar',
|
|
84
|
+
fileLoaded: 'Archivo cargado',
|
|
85
|
+
fileLoadFailed: 'No se pudo cargar el archivo',
|
|
86
|
+
},
|
|
87
|
+
it: {
|
|
88
|
+
placeholder: 'Scrivi qui il tuo codice Lny...',
|
|
89
|
+
executing: 'Esecuzione del codice...',
|
|
90
|
+
finished: 'Esecuzione completata.',
|
|
91
|
+
errorLabel: 'Errore',
|
|
92
|
+
helpTitle: 'Riferimento Rapido',
|
|
93
|
+
exampleLoaded: 'Esempio caricato',
|
|
94
|
+
exampleLoadFailed: 'Impossibile caricare l\'esempio',
|
|
95
|
+
stateTitle: 'Stato di Lenny',
|
|
96
|
+
fieldSize: 'Dimensione',
|
|
97
|
+
pos: 'Pos',
|
|
98
|
+
dir: 'Direzione',
|
|
99
|
+
bones: 'Ossa',
|
|
100
|
+
runs: 'Corse',
|
|
101
|
+
saveFile: 'Salva',
|
|
102
|
+
savePrompt: 'Nome file (senza estensione):',
|
|
103
|
+
loadFile: 'Carica',
|
|
104
|
+
fileLoaded: 'File caricato',
|
|
105
|
+
fileLoadFailed: 'Impossibile caricare il file',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const referenceLists = {
|
|
109
|
+
de: {
|
|
110
|
+
core: ['WENN', 'DANN', 'SONST', 'ENDE', 'SOLANGE', 'FUER', 'IN', 'VAR', 'SETZE', 'SCHREIB', 'PROZEDUR', 'BEMERKUNG'],
|
|
111
|
+
world: ['LAUF', 'DREH_LINKS', 'NIMM', 'GIB', 'IST_FREI', 'IST_KNOCHEN', 'SPRING', 'SCHAU'],
|
|
112
|
+
operators: ['+', '-', '*', '/', '%', '**', '<', '>', '<<', '<=', '>=', '==', '<>', 'UND', 'ODER', 'NICHT'],
|
|
113
|
+
},
|
|
114
|
+
en: {
|
|
115
|
+
core: ['IF', 'THEN', 'ELSE', 'END', 'WHILE', 'FOR', 'IN', 'VAR', 'SET', 'LOG', 'PROC', 'REM'],
|
|
116
|
+
world: ['RUN', 'TURN_LEFT', 'TAKE', 'GIVE', 'FREE_AHEAD', 'THING_THERE', 'JUMP', 'LOOK'],
|
|
117
|
+
operators: ['+', '-', '*', '/', '%', '**', '<', '>', '<<', '<=', '>=', '==', '<>', 'AND', 'OR', 'NOT'],
|
|
118
|
+
},
|
|
119
|
+
fr: {
|
|
120
|
+
core: ['SI', 'ALORS', 'SINON', 'FIN', 'TANT', 'POUR', 'DANS', 'VAR', 'DEFINI', 'ECRIS', 'PROCEDURE', 'REMARQUE'],
|
|
121
|
+
world: ['MARCHE', 'TOURNE_GAUCHE', 'PRENDRE', 'DONNER', 'EST_LIBRE', 'EST_OS', 'SAUTE', 'REGARDE'],
|
|
122
|
+
operators: ['+', '-', '*', '/', '%', '**', '<', '>', '<<', '<=', '>=', '==', '<>', 'ET', 'OU', 'NON'],
|
|
123
|
+
},
|
|
124
|
+
es: {
|
|
125
|
+
core: ['SI', 'ENTONCES', 'SINO', 'FIN', 'MIENTRAS', 'PARA', 'EN', 'VAR', 'ASIGNA', 'ESCRIBE', 'PROCEDIMIENTO', 'COMENTARIO'],
|
|
126
|
+
world: ['CORRE', 'GIRA_IZQUIERDA', 'TOMA', 'DA', 'ES_LIBRE', 'ES_HUESO', 'SALTA', 'MIRA'],
|
|
127
|
+
operators: ['+', '-', '*', '/', '%', '**', '<', '>', '<<', '<=', '>=', '==', '<>', 'Y', 'O', 'NO'],
|
|
128
|
+
},
|
|
129
|
+
it: {
|
|
130
|
+
core: ['SE', 'ALLORA', 'ALTRIMENTI', 'FINE', 'MENTRE', 'PER', 'IN', 'VAR', 'ASSEGNA', 'SCRIVI', 'PROCEDURA', 'COMMENTO'],
|
|
131
|
+
world: ['CORRI', 'GIRA_SINISTRA', 'PRENDI', 'DAI', 'E_LIBERO', 'E_OSSO', 'SALTA', 'GUARDA'],
|
|
132
|
+
operators: ['+', '-', '*', '/', '%', '**', '<', '>', '<<', '<=', '>=', '==', '<>', 'E', 'O', 'NON'],
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const helpLabels = {
|
|
136
|
+
de: {
|
|
137
|
+
title: 'Vollstaendige Dokumentation',
|
|
138
|
+
close: 'Schliessen',
|
|
139
|
+
loading: 'Dokumentation wird geladen...',
|
|
140
|
+
empty: 'Dokumentation konnte nicht geladen werden.',
|
|
141
|
+
docsSection: 'Befehle und Referenz',
|
|
142
|
+
examplesSection: 'Alle Beispielskripte',
|
|
143
|
+
},
|
|
144
|
+
en: {
|
|
145
|
+
title: 'Complete Documentation',
|
|
146
|
+
close: 'Close',
|
|
147
|
+
loading: 'Loading documentation...',
|
|
148
|
+
empty: 'Documentation could not be loaded.',
|
|
149
|
+
docsSection: 'Commands and Reference',
|
|
150
|
+
examplesSection: 'All Example Scripts',
|
|
151
|
+
},
|
|
152
|
+
fr: {
|
|
153
|
+
title: 'Documentation Complete',
|
|
154
|
+
close: 'Fermer',
|
|
155
|
+
loading: 'Chargement de la documentation...',
|
|
156
|
+
empty: 'La documentation n\'a pas pu etre chargee.',
|
|
157
|
+
docsSection: 'Commandes et Reference',
|
|
158
|
+
examplesSection: 'Tous les Scripts Exemple',
|
|
159
|
+
},
|
|
160
|
+
es: {
|
|
161
|
+
title: 'Documentacion Completa',
|
|
162
|
+
close: 'Cerrar',
|
|
163
|
+
loading: 'Cargando documentacion...',
|
|
164
|
+
empty: 'No se pudo cargar la documentacion.',
|
|
165
|
+
docsSection: 'Comandos y Referencia',
|
|
166
|
+
examplesSection: 'Todos los Scripts de Ejemplo',
|
|
167
|
+
},
|
|
168
|
+
it: {
|
|
169
|
+
title: 'Documentazione Completa',
|
|
170
|
+
close: 'Chiudi',
|
|
171
|
+
loading: 'Caricamento documentazione...',
|
|
172
|
+
empty: 'Impossibile caricare la documentazione.',
|
|
173
|
+
docsSection: 'Comandi e Riferimento',
|
|
174
|
+
examplesSection: 'Tutti gli Script di Esempio',
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
const documentationFiles = [
|
|
178
|
+
{
|
|
179
|
+
titles: {
|
|
180
|
+
de: 'Lny Befehlsreferenz',
|
|
181
|
+
en: 'Lny Command Reference',
|
|
182
|
+
fr: 'Reference des commandes Lny',
|
|
183
|
+
es: 'Referencia de comandos Lny',
|
|
184
|
+
it: 'Riferimento comandi Lny',
|
|
185
|
+
},
|
|
186
|
+
sourcePath: 'docs/lny-referenz.md',
|
|
187
|
+
fetchPath: './docs/lny-referenz.md',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
titles: {
|
|
191
|
+
de: 'Lny Interpreter Handbuch',
|
|
192
|
+
en: 'Lny Interpreter Manual',
|
|
193
|
+
fr: 'Manuel de l\'interpreteur Lny',
|
|
194
|
+
es: 'Manual del interprete Lny',
|
|
195
|
+
it: 'Manuale interprete Lny',
|
|
196
|
+
},
|
|
197
|
+
sourcePath: 'docs/INTERPRETER.md',
|
|
198
|
+
fetchPath: './docs/INTERPRETER.md',
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
const exampleFileNames = [
|
|
202
|
+
'00-demo.lny',
|
|
203
|
+
'01-variable-assignment.lny',
|
|
204
|
+
'02-conditional.lny',
|
|
205
|
+
'03-while-loop.lny',
|
|
206
|
+
'04-for-loop.lny',
|
|
207
|
+
'05-lenny-movement.lny',
|
|
208
|
+
'06-procedure.lny',
|
|
209
|
+
];
|
|
210
|
+
const supportedLanguages = ['de', 'en', 'fr', 'es', 'it'];
|
|
211
|
+
const exampleTranslationCategories = ['core', 'world'];
|
|
212
|
+
const helpTranslationCategories = ['core', 'world'];
|
|
213
|
+
const helpAdditionalKeywordTranslations = {
|
|
214
|
+
TRUE: { de: 'WAHR', en: 'TRUE', fr: 'VRAI', es: 'VERDADERO', it: 'VERO' },
|
|
215
|
+
FALSE: { de: 'FALSCH', en: 'FALSE', fr: 'FAUX', es: 'FALSO', it: 'FALSO' },
|
|
216
|
+
NOTHING: { de: 'NICHTS', en: 'NOTHING', fr: 'RIEN', es: 'NADA', it: 'NULLA' },
|
|
217
|
+
PRIVATE: { de: 'PRIVAT', en: 'PRIVATE', fr: 'PRIVE', es: 'PRIVADO', it: 'PRIVATO' },
|
|
218
|
+
GLOBAL: { de: 'GLOBAL', en: 'GLOBAL', fr: 'GLOBAL', es: 'GLOBAL', it: 'GLOBALE' },
|
|
219
|
+
};
|
|
220
|
+
class BrowserRunner {
|
|
221
|
+
constructor() {
|
|
222
|
+
this.echoMode = false;
|
|
223
|
+
this.echoBtn = document.getElementById('echoBtn');
|
|
224
|
+
this.fontSize = 13;
|
|
225
|
+
this.fontFamily = "";
|
|
226
|
+
this.language = 'de';
|
|
227
|
+
this.worldWidth = world_1.DEFAULT_WORLD_WIDTH;
|
|
228
|
+
this.worldHeight = world_1.DEFAULT_WORLD_HEIGHT;
|
|
229
|
+
this.interpreter = new interpreter_1.Interpreter(this.language, {
|
|
230
|
+
worldWidth: world_1.DEFAULT_WORLD_WIDTH,
|
|
231
|
+
worldHeight: world_1.DEFAULT_WORLD_HEIGHT,
|
|
232
|
+
});
|
|
233
|
+
this.codeEditor = document.getElementById('codeEditor');
|
|
234
|
+
this.consoleEl = document.getElementById('console');
|
|
235
|
+
this.kernCommandsList = document.getElementById('kernCommandsList');
|
|
236
|
+
this.worldCommandsList = document.getElementById('worldCommandsList');
|
|
237
|
+
this.operatorsList = document.getElementById('operatorsList');
|
|
238
|
+
this.exampleScriptsList = document.getElementById('exampleScriptsList');
|
|
239
|
+
this.lennyState = document.getElementById('lennyState');
|
|
240
|
+
this.helpModal = document.getElementById('helpModal');
|
|
241
|
+
this.helpModalTitle = document.getElementById('helpModalTitle');
|
|
242
|
+
this.helpContent = document.getElementById('helpContent');
|
|
243
|
+
this.closeHelpBtn = document.getElementById('closeHelpBtn');
|
|
244
|
+
this.fileInput = document.getElementById('fileInput');
|
|
245
|
+
this.aboutBtn = document.getElementById('aboutBtn');
|
|
246
|
+
this.aboutModal = document.getElementById('aboutModal');
|
|
247
|
+
this.aboutContent = document.getElementById('aboutContent');
|
|
248
|
+
this.closeAboutBtn = document.getElementById('closeAboutBtn');
|
|
249
|
+
// About/Lny-Book UI elements removed
|
|
250
|
+
this.loadedHelpSections = null;
|
|
251
|
+
this.exampleScriptCache = new Map();
|
|
252
|
+
this.exampleKeywordMaps = new Map();
|
|
253
|
+
this.helpKeywordMaps = new Map();
|
|
254
|
+
this.selectedExampleScript = null;
|
|
255
|
+
}
|
|
256
|
+
setupEchoButton() {
|
|
257
|
+
if (!this.echoBtn)
|
|
258
|
+
return;
|
|
259
|
+
this.echoBtn.addEventListener('click', () => {
|
|
260
|
+
this.echoMode = !this.echoMode;
|
|
261
|
+
this.echoBtn.textContent = this.echoMode ? 'ECHO ON' : 'ECHO OFF';
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async init() {
|
|
265
|
+
this.setupEchoButton();
|
|
266
|
+
// Settings laden (fontSize, fontFamily)
|
|
267
|
+
try {
|
|
268
|
+
const resp = await fetch('./settings.json', { cache: 'no-store' });
|
|
269
|
+
if (resp.ok) {
|
|
270
|
+
const settings = await resp.json();
|
|
271
|
+
if (settings.fontSize && typeof settings.fontSize === 'number') {
|
|
272
|
+
this.fontSize = settings.fontSize;
|
|
273
|
+
}
|
|
274
|
+
if (settings.fontFamily && typeof settings.fontFamily === 'string') {
|
|
275
|
+
this.fontFamily = settings.fontFamily;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch { }
|
|
280
|
+
// Theme aus LocalStorage setzen
|
|
281
|
+
const theme = localStorage.getItem('theme');
|
|
282
|
+
if (theme === 'dark') {
|
|
283
|
+
document.body.classList.add('dark');
|
|
284
|
+
const btn = document.getElementById('themeToggleBtn');
|
|
285
|
+
if (btn)
|
|
286
|
+
btn.textContent = '☀️';
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
document.body.classList.remove('dark');
|
|
290
|
+
const btn = document.getElementById('themeToggleBtn');
|
|
291
|
+
if (btn)
|
|
292
|
+
btn.textContent = '🌙';
|
|
293
|
+
}
|
|
294
|
+
await this.loadConfig();
|
|
295
|
+
this.interpreter = new interpreter_1.Interpreter(this.language, {
|
|
296
|
+
worldWidth: this.worldWidth,
|
|
297
|
+
worldHeight: this.worldHeight,
|
|
298
|
+
});
|
|
299
|
+
// Schriftgröße anwenden
|
|
300
|
+
this.applyFontSize();
|
|
301
|
+
// About/Lny-Book modals removed
|
|
302
|
+
this.bindEvents();
|
|
303
|
+
this.refreshUi();
|
|
304
|
+
// Global ESC handler for modals
|
|
305
|
+
document.addEventListener('keydown', (event) => {
|
|
306
|
+
if (event.key === 'Escape' && this.helpModal.classList.contains('open')) {
|
|
307
|
+
this.hideHelp();
|
|
308
|
+
}
|
|
309
|
+
if (event.key === 'Escape' && this.aboutModal.classList.contains('open')) {
|
|
310
|
+
this.hideAbout();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async loadConfig() {
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch('./lny-config.yml', { cache: 'no-store' });
|
|
317
|
+
if (!response.ok) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const configContent = await response.text();
|
|
321
|
+
const parsedFieldSize = this.parseFieldSizeFromConfig(configContent);
|
|
322
|
+
if (parsedFieldSize) {
|
|
323
|
+
this.worldWidth = parsedFieldSize.width;
|
|
324
|
+
this.worldHeight = parsedFieldSize.height;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Keep defaults when config is unavailable in browser runtime.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
parseFieldSizeFromConfig(configContent) {
|
|
332
|
+
const lines = configContent.replace(/^\uFEFF/, '').split(/\r?\n/);
|
|
333
|
+
let width = world_1.DEFAULT_WORLD_WIDTH;
|
|
334
|
+
let height = world_1.DEFAULT_WORLD_HEIGHT;
|
|
335
|
+
let foundSizeConfig = false;
|
|
336
|
+
for (const rawLine of lines) {
|
|
337
|
+
const line = rawLine.trim();
|
|
338
|
+
if (!line || line.startsWith('#')) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const sizeMatch = line.match(/^field(?:_|-)?size\s*:\s*(.+)$/i);
|
|
342
|
+
if (sizeMatch) {
|
|
343
|
+
const parsedSize = this.parseCombinedFieldSize(this.normalizeConfigValue(sizeMatch[1]));
|
|
344
|
+
if (parsedSize) {
|
|
345
|
+
width = parsedSize.width;
|
|
346
|
+
height = parsedSize.height;
|
|
347
|
+
foundSizeConfig = true;
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const widthMatch = line.match(/^field(?:_|-)?width\s*:\s*(.+)$/i);
|
|
352
|
+
if (widthMatch) {
|
|
353
|
+
const parsedWidth = this.parsePositiveInt(this.normalizeConfigValue(widthMatch[1]));
|
|
354
|
+
if (parsedWidth !== null) {
|
|
355
|
+
width = parsedWidth;
|
|
356
|
+
foundSizeConfig = true;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const heightMatch = line.match(/^field(?:_|-)?height\s*:\s*(.+)$/i);
|
|
361
|
+
if (heightMatch) {
|
|
362
|
+
const parsedHeight = this.parsePositiveInt(this.normalizeConfigValue(heightMatch[1]));
|
|
363
|
+
if (parsedHeight !== null) {
|
|
364
|
+
height = parsedHeight;
|
|
365
|
+
foundSizeConfig = true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return foundSizeConfig ? { width, height } : null;
|
|
370
|
+
}
|
|
371
|
+
normalizeConfigValue(rawValue) {
|
|
372
|
+
return rawValue.split('#')[0].trim().replace(/^['"]|['"]$/g, '').trim();
|
|
373
|
+
}
|
|
374
|
+
parseCombinedFieldSize(rawValue) {
|
|
375
|
+
const match = rawValue.match(/^(\d+)\s*[xX]\s*(\d+)$/);
|
|
376
|
+
if (!match) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
const width = this.parsePositiveInt(match[1]);
|
|
380
|
+
const height = this.parsePositiveInt(match[2]);
|
|
381
|
+
if (width === null || height === null) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
return { width, height };
|
|
385
|
+
}
|
|
386
|
+
parsePositiveInt(rawValue) {
|
|
387
|
+
const numericValue = Number(rawValue);
|
|
388
|
+
return Number.isInteger(numericValue) && numericValue > 0 ? numericValue : null;
|
|
389
|
+
}
|
|
390
|
+
bindEvents() {
|
|
391
|
+
// Schriftgröße dynamisch setzen, falls settings.json geändert wird
|
|
392
|
+
// (Optional: Hot-Reload, hier nicht implementiert)
|
|
393
|
+
// Theme Toggle Button
|
|
394
|
+
const themeToggleBtn = document.getElementById('themeToggleBtn');
|
|
395
|
+
if (themeToggleBtn) {
|
|
396
|
+
themeToggleBtn.addEventListener('click', () => {
|
|
397
|
+
const isDark = document.body.classList.toggle('dark');
|
|
398
|
+
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
399
|
+
themeToggleBtn.textContent = isDark ? '☀️' : '🌙';
|
|
400
|
+
console.log('[ThemeToggle]', isDark ? 'Darkmode aktiviert' : 'Lightmode aktiviert');
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const runBtn = document.getElementById('runBtn');
|
|
404
|
+
const clearBtn = document.getElementById('clearBtn');
|
|
405
|
+
const clearConsoleBtn = document.getElementById('clearConsoleBtn');
|
|
406
|
+
const helpBtn = document.getElementById('helpBtn');
|
|
407
|
+
const saveBtn = document.getElementById('saveBtn');
|
|
408
|
+
const loadBtn = document.getElementById('loadBtn');
|
|
409
|
+
runBtn.addEventListener('click', () => this.runCode());
|
|
410
|
+
clearBtn.addEventListener('click', () => this.clearConsole());
|
|
411
|
+
clearConsoleBtn.addEventListener('click', () => this.clearConsole());
|
|
412
|
+
helpBtn.addEventListener('click', () => {
|
|
413
|
+
void this.showHelp();
|
|
414
|
+
});
|
|
415
|
+
saveBtn.addEventListener('click', () => this.saveFile());
|
|
416
|
+
loadBtn.addEventListener('click', () => this.fileInput.click());
|
|
417
|
+
this.fileInput.addEventListener('change', () => {
|
|
418
|
+
void this.loadFile();
|
|
419
|
+
});
|
|
420
|
+
this.closeHelpBtn.addEventListener('click', () => this.hideHelp());
|
|
421
|
+
this.helpModal.addEventListener('click', (event) => {
|
|
422
|
+
if (event.target === this.helpModal) {
|
|
423
|
+
this.hideHelp();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
// About button logic
|
|
427
|
+
this.aboutBtn.addEventListener('click', () => {
|
|
428
|
+
this.showAbout();
|
|
429
|
+
});
|
|
430
|
+
this.closeAboutBtn.addEventListener('click', () => this.hideAbout());
|
|
431
|
+
this.aboutModal.addEventListener('click', (event) => {
|
|
432
|
+
if (event.target === this.aboutModal) {
|
|
433
|
+
this.hideAbout();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
// --- About modal logic ---
|
|
438
|
+
async showAbout() {
|
|
439
|
+
this.aboutModal.style.display = '';
|
|
440
|
+
this.aboutModal.setAttribute('aria-hidden', 'false');
|
|
441
|
+
document.body.classList.add('help-open');
|
|
442
|
+
this.aboutContent.innerHTML = '<p>Lade Info...</p>';
|
|
443
|
+
const content = await this.fetchTextFile('./docs/About.md');
|
|
444
|
+
if (content) {
|
|
445
|
+
this.aboutContent.innerHTML = this.renderMarkdownDocument(content);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
this.aboutContent.innerHTML = '<p>About.md nicht gefunden.</p>';
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
hideAbout() {
|
|
452
|
+
this.aboutModal.style.display = 'none';
|
|
453
|
+
this.aboutModal.setAttribute('aria-hidden', 'true');
|
|
454
|
+
document.body.classList.remove('help-open');
|
|
455
|
+
}
|
|
456
|
+
// About/Lny-Book event listeners removed
|
|
457
|
+
// Language button and example script event bindings
|
|
458
|
+
// (remains in bindEvents as before)
|
|
459
|
+
runCode() {
|
|
460
|
+
const code = this.codeEditor.value;
|
|
461
|
+
if (!code.trim()) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.appendConsoleLine(uiStrings[this.language].executing, 'info');
|
|
465
|
+
const start = performance.now();
|
|
466
|
+
try {
|
|
467
|
+
this.interpreter.clearOutput();
|
|
468
|
+
const parser = new parser_1.Parser(code, this.language);
|
|
469
|
+
const ast = parser.parse();
|
|
470
|
+
// --- ECHO-Modus: Hook für LAUF/DREH_LINKS ---
|
|
471
|
+
const world = this.interpreter.getWorld();
|
|
472
|
+
world.setEchoCallback(this.echoMode ? (state) => {
|
|
473
|
+
let lines = [
|
|
474
|
+
`State:`,
|
|
475
|
+
` Pos: (${state.x},${state.y})`,
|
|
476
|
+
` Dir: ${state.direction}`,
|
|
477
|
+
` Bones: ${state.bones}`,
|
|
478
|
+
` Runs: ${state.howManyRun}`,
|
|
479
|
+
` Turns: ${state.howManyTurn}`,
|
|
480
|
+
` IST_KNOCHEN: ${state.ist_knochen}`
|
|
481
|
+
];
|
|
482
|
+
if (state.field) {
|
|
483
|
+
lines.push(` Feld:`);
|
|
484
|
+
lines.push(` Bones: ${state.field.bones}`);
|
|
485
|
+
lines.push(` Log: ${state.field.hasLog ? 'ja' : 'nein'}`);
|
|
486
|
+
lines.push(` Doghut: ${state.field.hasDoghut ? 'ja' : 'nein'}`);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
lines.push(' Feld: ungültig');
|
|
490
|
+
}
|
|
491
|
+
this.appendConsoleLine(lines.join('\n'));
|
|
492
|
+
} : undefined);
|
|
493
|
+
this.interpreter.interpret(ast);
|
|
494
|
+
const output = this.interpreter.getOutput();
|
|
495
|
+
if (output.trim().length > 0) {
|
|
496
|
+
output.split('\n').forEach((line) => {
|
|
497
|
+
this.appendConsoleLine(line);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
const end = performance.now();
|
|
501
|
+
const ms = Math.round(end - start);
|
|
502
|
+
this.appendConsoleLine(`${uiStrings[this.language].finished} (${ms} ms)`, 'info');
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
const message = error.message;
|
|
506
|
+
this.appendConsoleLine(`${uiStrings[this.language].errorLabel}: ${message}`, 'error');
|
|
507
|
+
}
|
|
508
|
+
this.renderState();
|
|
509
|
+
}
|
|
510
|
+
clearConsole() {
|
|
511
|
+
this.consoleEl.innerHTML = '';
|
|
512
|
+
}
|
|
513
|
+
applyFontSize() {
|
|
514
|
+
if (this.codeEditor) {
|
|
515
|
+
this.codeEditor.style.fontSize = this.fontSize + 'px';
|
|
516
|
+
if (this.fontFamily) {
|
|
517
|
+
this.codeEditor.style.fontFamily = this.fontFamily;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (this.consoleEl) {
|
|
521
|
+
this.consoleEl.style.fontSize = this.fontSize + 'px';
|
|
522
|
+
if (this.fontFamily) {
|
|
523
|
+
this.consoleEl.style.fontFamily = this.fontFamily;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
saveFile() {
|
|
528
|
+
const ui = uiStrings[this.language];
|
|
529
|
+
if (!this.codeEditor.value.trim()) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const defaultName = this.selectedExampleScript
|
|
533
|
+
? this.selectedExampleScript.replace(/\.lny$/, '')
|
|
534
|
+
: 'script';
|
|
535
|
+
const input = window.prompt(ui.savePrompt, defaultName);
|
|
536
|
+
if (input === null) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const sanitized = input.trim().replace(/[/\\?%*:|"<>]/g, '_') || 'script';
|
|
540
|
+
const fileName = sanitized.endsWith('.lny') ? sanitized : `${sanitized}.lny`;
|
|
541
|
+
const code = this.codeEditor.value;
|
|
542
|
+
const blob = new Blob([code], { type: 'text/plain' });
|
|
543
|
+
const url = URL.createObjectURL(blob);
|
|
544
|
+
const anchor = document.createElement('a');
|
|
545
|
+
anchor.href = url;
|
|
546
|
+
anchor.download = fileName;
|
|
547
|
+
anchor.click();
|
|
548
|
+
URL.revokeObjectURL(url);
|
|
549
|
+
}
|
|
550
|
+
async loadFile() {
|
|
551
|
+
const file = this.fileInput.files?.[0];
|
|
552
|
+
if (!file) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const text = await file.text();
|
|
557
|
+
this.codeEditor.value = text.replace(/^\uFEFF/, '');
|
|
558
|
+
this.selectedExampleScript = null;
|
|
559
|
+
this.renderExampleScriptsList();
|
|
560
|
+
this.appendConsoleLine(`${uiStrings[this.language].fileLoaded}: ${file.name}`, 'info');
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
this.appendConsoleLine(uiStrings[this.language].fileLoadFailed, 'error');
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
this.fileInput.value = '';
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async showHelp() {
|
|
570
|
+
const labels = helpLabels[this.language];
|
|
571
|
+
this.helpModalTitle.textContent = labels.title;
|
|
572
|
+
this.closeHelpBtn.textContent = labels.close;
|
|
573
|
+
this.openHelpModal();
|
|
574
|
+
if (!this.loadedHelpSections) {
|
|
575
|
+
this.helpContent.innerHTML = `<p class="help-status">${this.escapeHtml(labels.loading)}</p>`;
|
|
576
|
+
this.loadedHelpSections = await this.loadHelpSections(this.language);
|
|
577
|
+
}
|
|
578
|
+
this.helpContent.innerHTML = this.renderHelpSections(this.loadedHelpSections);
|
|
579
|
+
}
|
|
580
|
+
refreshUi() {
|
|
581
|
+
this.codeEditor.placeholder = uiStrings[this.language].placeholder;
|
|
582
|
+
this.helpModalTitle.textContent = helpLabels[this.language].title;
|
|
583
|
+
this.closeHelpBtn.textContent = helpLabels[this.language].close;
|
|
584
|
+
if (this.loadedHelpSections && this.helpModal.classList.contains('open')) {
|
|
585
|
+
this.helpContent.innerHTML = this.renderHelpSections(this.loadedHelpSections);
|
|
586
|
+
}
|
|
587
|
+
this.renderReferenceLists();
|
|
588
|
+
this.renderState();
|
|
589
|
+
}
|
|
590
|
+
renderReferenceLists() {
|
|
591
|
+
this.kernCommandsList.innerHTML = referenceLists[this.language].core.map((cmd) => `<li>${cmd}</li>`).join('');
|
|
592
|
+
this.worldCommandsList.innerHTML = referenceLists[this.language].world.map((cmd) => `<li>${cmd}</li>`).join('');
|
|
593
|
+
this.operatorsList.innerHTML = referenceLists[this.language].operators.map((cmd) => `<li>${cmd}</li>`).join('');
|
|
594
|
+
this.renderExampleScriptsList();
|
|
595
|
+
}
|
|
596
|
+
renderExampleScriptsList() {
|
|
597
|
+
this.exampleScriptsList.innerHTML = exampleFileNames.map((fileName) => {
|
|
598
|
+
const isActive = this.selectedExampleScript === fileName ? ' active' : '';
|
|
599
|
+
return `<li class="example-script-item${isActive}" data-example-file="${this.escapeHtml(fileName)}">${this.escapeHtml(fileName)}</li>`;
|
|
600
|
+
}).join('');
|
|
601
|
+
// Event Delegation: Click on example loads it
|
|
602
|
+
this.exampleScriptsList.querySelectorAll('.example-script-item').forEach((el) => {
|
|
603
|
+
el.addEventListener('click', (e) => {
|
|
604
|
+
const file = el.getAttribute('data-example-file');
|
|
605
|
+
if (file && this.isExampleFileName(file)) {
|
|
606
|
+
void this.loadExampleIntoEditor(file);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async loadExampleIntoEditor(fileName, announce = true) {
|
|
612
|
+
let content = this.exampleScriptCache.get(fileName) ?? null;
|
|
613
|
+
if (!content) {
|
|
614
|
+
content = await this.fetchTextFile(`./examples/${fileName}`);
|
|
615
|
+
if (!content) {
|
|
616
|
+
this.appendConsoleLine(`${uiStrings[this.language].exampleLoadFailed}: ${fileName}`, 'error');
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
this.exampleScriptCache.set(fileName, content);
|
|
620
|
+
}
|
|
621
|
+
this.codeEditor.value = this.localizeExampleScript(content, this.language);
|
|
622
|
+
this.selectedExampleScript = fileName;
|
|
623
|
+
this.renderExampleScriptsList();
|
|
624
|
+
if (announce) {
|
|
625
|
+
this.appendConsoleLine(`${uiStrings[this.language].exampleLoaded}: ${fileName}`, 'info');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
isExampleFileName(fileName) {
|
|
629
|
+
return exampleFileNames.includes(fileName);
|
|
630
|
+
}
|
|
631
|
+
localizeExampleScript(content, targetLanguage) {
|
|
632
|
+
const keywordMap = this.getExampleKeywordMap(targetLanguage);
|
|
633
|
+
return this.localizeContentByTokenMap(content, keywordMap);
|
|
634
|
+
}
|
|
635
|
+
getExampleKeywordMap(targetLanguage) {
|
|
636
|
+
const cachedMap = this.exampleKeywordMaps.get(targetLanguage);
|
|
637
|
+
if (cachedMap) {
|
|
638
|
+
return cachedMap;
|
|
639
|
+
}
|
|
640
|
+
const keywordMap = new Map();
|
|
641
|
+
exampleTranslationCategories.forEach((category) => {
|
|
642
|
+
const targetTokens = referenceLists[targetLanguage][category];
|
|
643
|
+
for (let index = 0; index < targetTokens.length; index += 1) {
|
|
644
|
+
const translatedToken = targetTokens[index];
|
|
645
|
+
supportedLanguages.forEach((sourceLanguage) => {
|
|
646
|
+
const sourceToken = referenceLists[sourceLanguage][category][index];
|
|
647
|
+
keywordMap.set(this.normalizeLanguageToken(sourceToken), translatedToken);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
this.exampleKeywordMaps.set(targetLanguage, keywordMap);
|
|
652
|
+
return keywordMap;
|
|
653
|
+
}
|
|
654
|
+
getHelpKeywordMap(targetLanguage) {
|
|
655
|
+
const cachedMap = this.helpKeywordMaps.get(targetLanguage);
|
|
656
|
+
if (cachedMap) {
|
|
657
|
+
return cachedMap;
|
|
658
|
+
}
|
|
659
|
+
const keywordMap = new Map();
|
|
660
|
+
helpTranslationCategories.forEach((category) => {
|
|
661
|
+
const targetTokens = referenceLists[targetLanguage][category];
|
|
662
|
+
for (let index = 0; index < targetTokens.length; index += 1) {
|
|
663
|
+
const translatedToken = targetTokens[index];
|
|
664
|
+
supportedLanguages.forEach((sourceLanguage) => {
|
|
665
|
+
const sourceToken = referenceLists[sourceLanguage][category][index];
|
|
666
|
+
keywordMap.set(this.normalizeLanguageToken(sourceToken), translatedToken);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
Object.values(helpAdditionalKeywordTranslations).forEach((tokenByLanguage) => {
|
|
671
|
+
const translatedToken = tokenByLanguage[targetLanguage];
|
|
672
|
+
supportedLanguages.forEach((sourceLanguage) => {
|
|
673
|
+
keywordMap.set(this.normalizeLanguageToken(tokenByLanguage[sourceLanguage]), translatedToken);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
this.helpKeywordMaps.set(targetLanguage, keywordMap);
|
|
677
|
+
return keywordMap;
|
|
678
|
+
}
|
|
679
|
+
localizeHelpDocumentation(content, targetLanguage) {
|
|
680
|
+
const keywordMap = this.getHelpKeywordMap(targetLanguage);
|
|
681
|
+
return this.localizeContentByTokenMap(content, keywordMap);
|
|
682
|
+
}
|
|
683
|
+
localizeContentByTokenMap(content, keywordMap) {
|
|
684
|
+
return content.replace(/\b[A-Za-z_À-ÖØ-öø-ÿ][A-Za-z_0-9À-ÖØ-öø-ÿ]*\b/g, (token) => {
|
|
685
|
+
const replacement = keywordMap.get(this.normalizeLanguageToken(token));
|
|
686
|
+
return replacement ?? token;
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
normalizeLanguageToken(token) {
|
|
690
|
+
return token
|
|
691
|
+
.normalize('NFD')
|
|
692
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
693
|
+
.toUpperCase();
|
|
694
|
+
}
|
|
695
|
+
getLocalizedHelpSectionTitle(section) {
|
|
696
|
+
return section.localizedTitles?.[this.language] ?? section.title;
|
|
697
|
+
}
|
|
698
|
+
renderState() {
|
|
699
|
+
const state = this.interpreter.getWorld().getLennyState();
|
|
700
|
+
const ui = uiStrings[this.language];
|
|
701
|
+
this.lennyState.innerHTML = `
|
|
702
|
+
<div class="lenny-state">
|
|
703
|
+
<div><strong>${ui.stateTitle}:</strong></div>
|
|
704
|
+
<div><strong>${ui.fieldSize}:</strong> ${state.fieldWidth}x${state.fieldHeight}</div>
|
|
705
|
+
<div><strong>${ui.pos}:</strong> (${state.x}, ${state.y})</div>
|
|
706
|
+
<div><strong>${ui.dir}:</strong> ${state.direction}</div>
|
|
707
|
+
<div><strong>${ui.bones}:</strong> ${state.bones}</div>
|
|
708
|
+
<div><strong>${ui.runs}:</strong> ${state.howManyRun}</div>
|
|
709
|
+
</div>
|
|
710
|
+
`;
|
|
711
|
+
}
|
|
712
|
+
appendConsoleLine(text, className = '') {
|
|
713
|
+
const line = document.createElement('div');
|
|
714
|
+
line.className = className ? `console-line ${className}` : 'console-line';
|
|
715
|
+
line.textContent = text;
|
|
716
|
+
this.consoleEl.appendChild(line);
|
|
717
|
+
this.consoleEl.scrollTop = this.consoleEl.scrollHeight;
|
|
718
|
+
}
|
|
719
|
+
async loadHelpSections(language) {
|
|
720
|
+
const documentation = await Promise.all(documentationFiles.map(async (doc) => {
|
|
721
|
+
const localizedDoc = await this.fetchLocalizedDocumentation(doc, language);
|
|
722
|
+
if (!localizedDoc) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
kind: 'documentation',
|
|
727
|
+
title: doc.titles.de,
|
|
728
|
+
localizedTitles: doc.titles,
|
|
729
|
+
sourcePath: localizedDoc.sourcePath,
|
|
730
|
+
content: localizedDoc.content,
|
|
731
|
+
};
|
|
732
|
+
}));
|
|
733
|
+
const examples = await Promise.all(exampleFileNames.map(async (fileName) => {
|
|
734
|
+
const content = await this.fetchTextFile(`./examples/${fileName}`);
|
|
735
|
+
if (content === null) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
kind: 'example',
|
|
740
|
+
title: fileName,
|
|
741
|
+
sourcePath: `examples/${fileName}`,
|
|
742
|
+
content,
|
|
743
|
+
};
|
|
744
|
+
}));
|
|
745
|
+
const sections = [];
|
|
746
|
+
documentation.forEach((section) => {
|
|
747
|
+
if (section) {
|
|
748
|
+
sections.push(section);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
examples.forEach((section) => {
|
|
752
|
+
if (section) {
|
|
753
|
+
sections.push(section);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
return sections;
|
|
757
|
+
}
|
|
758
|
+
async fetchLocalizedDocumentation(documentationFile, language) {
|
|
759
|
+
const fileName = documentationFile.sourcePath.split('/').pop() ?? documentationFile.sourcePath;
|
|
760
|
+
const candidates = [
|
|
761
|
+
{
|
|
762
|
+
fetchPath: documentationFile.fetchPath,
|
|
763
|
+
sourcePath: documentationFile.sourcePath,
|
|
764
|
+
},
|
|
765
|
+
];
|
|
766
|
+
if (language !== 'de') {
|
|
767
|
+
const localizedSourcePath = `docs/i18n/${language}/${fileName}`;
|
|
768
|
+
candidates.unshift({
|
|
769
|
+
fetchPath: `./${localizedSourcePath}`,
|
|
770
|
+
sourcePath: localizedSourcePath,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return this.fetchFirstAvailableText(candidates);
|
|
774
|
+
}
|
|
775
|
+
async fetchFirstAvailableText(candidates) {
|
|
776
|
+
for (const candidate of candidates) {
|
|
777
|
+
const content = await this.fetchTextFile(candidate.fetchPath);
|
|
778
|
+
if (content !== null) {
|
|
779
|
+
return {
|
|
780
|
+
content,
|
|
781
|
+
sourcePath: candidate.sourcePath,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
async fetchTextFile(path) {
|
|
788
|
+
try {
|
|
789
|
+
const response = await fetch(path, { cache: 'no-store' });
|
|
790
|
+
if (!response.ok) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
return (await response.text()).replace(/^\uFEFF/, '').trimEnd();
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
renderHelpSections(sections) {
|
|
800
|
+
const labels = helpLabels[this.language];
|
|
801
|
+
if (sections.length === 0) {
|
|
802
|
+
return `<p class="help-status">${this.escapeHtml(labels.empty)}</p>`;
|
|
803
|
+
}
|
|
804
|
+
const docs = sections.filter((section) => section.kind === 'documentation');
|
|
805
|
+
const examples = sections.filter((section) => section.kind === 'example');
|
|
806
|
+
const htmlParts = [];
|
|
807
|
+
if (docs.length > 0) {
|
|
808
|
+
htmlParts.push(`<h3 class="help-group-title">${this.escapeHtml(labels.docsSection)}</h3>`);
|
|
809
|
+
docs.forEach((section, index) => {
|
|
810
|
+
htmlParts.push(this.renderHelpSection(section, index === 0));
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (examples.length > 0) {
|
|
814
|
+
htmlParts.push(`<h3 class="help-group-title">${this.escapeHtml(labels.examplesSection)}</h3>`);
|
|
815
|
+
examples.forEach((section) => {
|
|
816
|
+
htmlParts.push(this.renderHelpSection(section, false));
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return htmlParts.join('');
|
|
820
|
+
}
|
|
821
|
+
renderHelpSection(section, openByDefault) {
|
|
822
|
+
const localizedContent = section.kind === 'example'
|
|
823
|
+
? this.localizeExampleScript(section.content, this.language)
|
|
824
|
+
: this.localizeHelpDocumentation(section.content, this.language);
|
|
825
|
+
const bodyHtml = section.kind === 'example'
|
|
826
|
+
? this.renderCodeBlockHtml(localizedContent, 'lny')
|
|
827
|
+
: this.renderMarkdownDocument(localizedContent);
|
|
828
|
+
const sectionTitle = this.getLocalizedHelpSectionTitle(section);
|
|
829
|
+
return `
|
|
830
|
+
<details class="help-doc-section"${openByDefault ? ' open' : ''}>
|
|
831
|
+
<summary>
|
|
832
|
+
<span>${this.escapeHtml(sectionTitle)}</span>
|
|
833
|
+
<span class="help-doc-source">${this.escapeHtml(section.sourcePath)}</span>
|
|
834
|
+
</summary>
|
|
835
|
+
<div class="help-markdown">${bodyHtml}</div>
|
|
836
|
+
</details>
|
|
837
|
+
`;
|
|
838
|
+
}
|
|
839
|
+
renderMarkdownDocument(markdown) {
|
|
840
|
+
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
|
|
841
|
+
const blocks = [];
|
|
842
|
+
let index = 0;
|
|
843
|
+
while (index < lines.length) {
|
|
844
|
+
const line = lines[index];
|
|
845
|
+
const trimmed = line.trim();
|
|
846
|
+
if (!trimmed) {
|
|
847
|
+
index += 1;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const fenceMatch = trimmed.match(/^```([a-zA-Z0-9_-]+)?\s*$/);
|
|
851
|
+
if (fenceMatch) {
|
|
852
|
+
const language = fenceMatch[1] ?? '';
|
|
853
|
+
const codeLines = [];
|
|
854
|
+
index += 1;
|
|
855
|
+
while (index < lines.length && !lines[index].trim().match(/^```\s*$/)) {
|
|
856
|
+
codeLines.push(lines[index]);
|
|
857
|
+
index += 1;
|
|
858
|
+
}
|
|
859
|
+
if (index < lines.length) {
|
|
860
|
+
index += 1;
|
|
861
|
+
}
|
|
862
|
+
blocks.push(this.renderCodeBlockHtml(codeLines.join('\n'), language));
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
866
|
+
if (headingMatch) {
|
|
867
|
+
const level = headingMatch[1].length;
|
|
868
|
+
const content = this.renderInlineMarkdown(headingMatch[2].trim());
|
|
869
|
+
blocks.push(`<h${level}>${content}</h${level}>`);
|
|
870
|
+
index += 1;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (/^\s*([-*_]\s*){3,}\s*$/.test(trimmed)) {
|
|
874
|
+
blocks.push('<hr />');
|
|
875
|
+
index += 1;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (this.isTableStart(lines, index)) {
|
|
879
|
+
const tableRender = this.renderTableHtml(lines, index);
|
|
880
|
+
blocks.push(tableRender.html);
|
|
881
|
+
index = tableRender.nextIndex;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (this.isUnorderedListLine(line)) {
|
|
885
|
+
const items = [];
|
|
886
|
+
while (index < lines.length && this.isUnorderedListLine(lines[index])) {
|
|
887
|
+
const listMatch = lines[index].match(/^\s*[-*]\s+(.+)$/);
|
|
888
|
+
if (listMatch) {
|
|
889
|
+
items.push(`<li>${this.renderInlineMarkdown(listMatch[1].trim())}</li>`);
|
|
890
|
+
}
|
|
891
|
+
index += 1;
|
|
892
|
+
}
|
|
893
|
+
blocks.push(`<ul>${items.join('')}</ul>`);
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (this.isOrderedListLine(line)) {
|
|
897
|
+
const items = [];
|
|
898
|
+
while (index < lines.length && this.isOrderedListLine(lines[index])) {
|
|
899
|
+
const listMatch = lines[index].match(/^\s*\d+\.\s+(.+)$/);
|
|
900
|
+
if (listMatch) {
|
|
901
|
+
items.push(`<li>${this.renderInlineMarkdown(listMatch[1].trim())}</li>`);
|
|
902
|
+
}
|
|
903
|
+
index += 1;
|
|
904
|
+
}
|
|
905
|
+
blocks.push(`<ol>${items.join('')}</ol>`);
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
const paragraphLines = [];
|
|
909
|
+
while (index < lines.length &&
|
|
910
|
+
lines[index].trim() &&
|
|
911
|
+
!this.isMarkdownBlockStart(lines, index)) {
|
|
912
|
+
paragraphLines.push(lines[index].trim());
|
|
913
|
+
index += 1;
|
|
914
|
+
}
|
|
915
|
+
if (paragraphLines.length > 0) {
|
|
916
|
+
const paragraphHtml = paragraphLines
|
|
917
|
+
.map((paragraphLine) => this.renderInlineMarkdown(paragraphLine))
|
|
918
|
+
.join(' ');
|
|
919
|
+
blocks.push(`<p>${paragraphHtml}</p>`);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
// Fallback to avoid an infinite loop when an unsupported pattern appears.
|
|
923
|
+
blocks.push(`<p>${this.renderInlineMarkdown(trimmed)}</p>`);
|
|
924
|
+
index += 1;
|
|
925
|
+
}
|
|
926
|
+
return blocks.join('');
|
|
927
|
+
}
|
|
928
|
+
isMarkdownBlockStart(lines, index) {
|
|
929
|
+
const line = lines[index] ?? '';
|
|
930
|
+
const trimmed = line.trim();
|
|
931
|
+
if (!trimmed) {
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
if (/^```/.test(trimmed)) {
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
if (/^(#{1,6})\s+/.test(line)) {
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
if (/^\s*([-*_]\s*){3,}\s*$/.test(trimmed)) {
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
if (this.isUnorderedListLine(line) || this.isOrderedListLine(line)) {
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
return this.isTableStart(lines, index);
|
|
947
|
+
}
|
|
948
|
+
isUnorderedListLine(line) {
|
|
949
|
+
return /^\s*[-*]\s+.+$/.test(line);
|
|
950
|
+
}
|
|
951
|
+
isOrderedListLine(line) {
|
|
952
|
+
return /^\s*\d+\.\s+.+$/.test(line);
|
|
953
|
+
}
|
|
954
|
+
isTableStart(lines, index) {
|
|
955
|
+
if (index + 1 >= lines.length) {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
const headerLine = lines[index];
|
|
959
|
+
const dividerLine = lines[index + 1];
|
|
960
|
+
if (!headerLine.includes('|') || !dividerLine.includes('|')) {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
const headerCells = this.parseTableRow(headerLine);
|
|
964
|
+
const dividerCells = this.parseTableRow(dividerLine);
|
|
965
|
+
if (headerCells.length < 2 || headerCells.length !== dividerCells.length) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
return dividerCells.every((cell) => this.isTableDividerCell(cell));
|
|
969
|
+
}
|
|
970
|
+
renderTableHtml(lines, startIndex) {
|
|
971
|
+
const headerCells = this.parseTableRow(lines[startIndex]);
|
|
972
|
+
const bodyRows = [];
|
|
973
|
+
let index = startIndex + 2;
|
|
974
|
+
while (index < lines.length) {
|
|
975
|
+
const line = lines[index];
|
|
976
|
+
if (!line.trim() || !line.includes('|')) {
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
const rowCells = this.parseTableRow(line);
|
|
980
|
+
if (rowCells.length < 2) {
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
bodyRows.push(this.normalizeTableCells(rowCells, headerCells.length));
|
|
984
|
+
index += 1;
|
|
985
|
+
}
|
|
986
|
+
const headHtml = `<thead><tr>${headerCells
|
|
987
|
+
.map((cell) => `<th>${this.renderInlineMarkdown(cell)}</th>`)
|
|
988
|
+
.join('')}</tr></thead>`;
|
|
989
|
+
const bodyHtml = bodyRows.length > 0
|
|
990
|
+
? `<tbody>${bodyRows
|
|
991
|
+
.map((rowCells) => `<tr>${rowCells
|
|
992
|
+
.map((cell) => `<td>${this.renderInlineMarkdown(cell)}</td>`)
|
|
993
|
+
.join('')}</tr>`)
|
|
994
|
+
.join('')}</tbody>`
|
|
995
|
+
: '';
|
|
996
|
+
return {
|
|
997
|
+
html: `<table>${headHtml}${bodyHtml}</table>`,
|
|
998
|
+
nextIndex: index,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
parseTableRow(row) {
|
|
1002
|
+
const normalized = row.trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
1003
|
+
return normalized.split('|').map((cell) => cell.trim());
|
|
1004
|
+
}
|
|
1005
|
+
normalizeTableCells(cells, targetLength) {
|
|
1006
|
+
if (cells.length === targetLength) {
|
|
1007
|
+
return cells;
|
|
1008
|
+
}
|
|
1009
|
+
if (cells.length > targetLength) {
|
|
1010
|
+
return [
|
|
1011
|
+
...cells.slice(0, targetLength - 1),
|
|
1012
|
+
cells.slice(targetLength - 1).join(' | '),
|
|
1013
|
+
];
|
|
1014
|
+
}
|
|
1015
|
+
return [
|
|
1016
|
+
...cells,
|
|
1017
|
+
...Array.from({ length: targetLength - cells.length }, () => ''),
|
|
1018
|
+
];
|
|
1019
|
+
}
|
|
1020
|
+
isTableDividerCell(cell) {
|
|
1021
|
+
return /^:?-{3,}:?$/.test(cell.trim());
|
|
1022
|
+
}
|
|
1023
|
+
renderCodeBlockHtml(code, language) {
|
|
1024
|
+
const normalizedLanguage = language.toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
1025
|
+
const classAttribute = normalizedLanguage ? ` class="language-${normalizedLanguage}"` : '';
|
|
1026
|
+
return `<pre><code${classAttribute}>${this.escapeHtml(code)}</code></pre>`;
|
|
1027
|
+
}
|
|
1028
|
+
renderInlineMarkdown(text) {
|
|
1029
|
+
const codeTokens = [];
|
|
1030
|
+
let html = text.replace(/`([^`]+)`/g, (_match, code) => {
|
|
1031
|
+
const token = `@@CODE_TOKEN_${codeTokens.length}@@`;
|
|
1032
|
+
codeTokens.push(`<code>${this.escapeHtml(code)}</code>`);
|
|
1033
|
+
return token;
|
|
1034
|
+
});
|
|
1035
|
+
html = this.escapeHtml(html);
|
|
1036
|
+
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_match, label, url) => `<a href="${url}" target="_blank" rel="noreferrer">${label}</a>`);
|
|
1037
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
1038
|
+
html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
|
|
1039
|
+
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
|
1040
|
+
codeTokens.forEach((tokenHtml, tokenIndex) => {
|
|
1041
|
+
const token = `@@CODE_TOKEN_${tokenIndex}@@`;
|
|
1042
|
+
html = html.split(token).join(tokenHtml);
|
|
1043
|
+
});
|
|
1044
|
+
return html;
|
|
1045
|
+
}
|
|
1046
|
+
escapeHtml(value) {
|
|
1047
|
+
return value
|
|
1048
|
+
.replace(/&/g, '&')
|
|
1049
|
+
.replace(/</g, '<')
|
|
1050
|
+
.replace(/>/g, '>');
|
|
1051
|
+
}
|
|
1052
|
+
openHelpModal() {
|
|
1053
|
+
this.helpModal.classList.add('open');
|
|
1054
|
+
this.helpModal.setAttribute('aria-hidden', 'false');
|
|
1055
|
+
document.body.classList.add('help-open');
|
|
1056
|
+
}
|
|
1057
|
+
hideHelp() {
|
|
1058
|
+
this.helpModal.classList.remove('open');
|
|
1059
|
+
this.helpModal.setAttribute('aria-hidden', 'true');
|
|
1060
|
+
document.body.classList.remove('help-open');
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
1064
|
+
const app = new BrowserRunner();
|
|
1065
|
+
void app.init();
|
|
1066
|
+
});
|
|
1067
|
+
//# sourceMappingURL=browser.js.map
|