Modularisiere Plattform (Phase 1 & 2): RunnerRegistry-Plugin-System, standardisierter Python-Adapter und deklaratives Kurs-Manifest (courses/python/course.json)
This commit is contained in:
parent
108da4c12a
commit
58ef8a3fb4
7 changed files with 443 additions and 211 deletions
|
|
@ -13,5 +13,5 @@
|
||||||
"streak_tage": 1,
|
"streak_tage": 1,
|
||||||
"letzter_aktiver_tag": "2026-08-29",
|
"letzter_aktiver_tag": "2026-08-29",
|
||||||
"historie": [],
|
"historie": [],
|
||||||
"zuletzt_aktualisiert": "2026-08-29T23:32:59.778016"
|
"zuletzt_aktualisiert": "2026-08-30T00:30:42.090205"
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ window.GAMIFICATION_DATA = {
|
||||||
"streak_tage": 1,
|
"streak_tage": 1,
|
||||||
"letzter_aktiver_tag": "2026-08-29",
|
"letzter_aktiver_tag": "2026-08-29",
|
||||||
"historie": [],
|
"historie": [],
|
||||||
"zuletzt_aktualisiert": "2026-08-29T23:32:59.778016"
|
"zuletzt_aktualisiert": "2026-08-30T00:30:42.090205"
|
||||||
};
|
};
|
||||||
window.GAMIFICATION_BADGES_KATALOG = {
|
window.GAMIFICATION_BADGES_KATALOG = {
|
||||||
"rechenkonegin": {
|
"rechenkonegin": {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* ⚡ GEHÄRTETE MONACO WEB-IDE & PYODIDE RUNNER ENGINE ⚡
|
* ⚡ UNIVERSAL MODULAR MONACO WEB-IDE ⚡
|
||||||
* =====================================================
|
* =======================================
|
||||||
* Interaktive Entwicklungsumgebung mit:
|
* Sprachunabhängige Entwicklungsumgebung mit:
|
||||||
* - Integriertem CODE_GUARD Sicherheits-Filter (Schadcode- & Exploit-Sperre)
|
* - Dynamischer Monaco Editor Initialisierung (Python, JavaScript, SQL, Rust etc.)
|
||||||
* - Monaco Editor (VS Code Engine mit Autocomplete & Syntax-Highlighting)
|
* - Delegierung an pluggbare Language Runner via RunnerRegistry
|
||||||
* - Tastatur-Shortcuts: Strg+Enter (Run), Strg+Shift+Enter (Test), Strg+S (Save)
|
* - Tastatur-Shortcuts: Strg+Enter (Run), Strg+Shift+Enter (Test), Strg+S (Save)
|
||||||
* - In-Browser Pyodide WebAssembly Python Runner (95% Client-Execution)
|
* - Reaktives Terminal, Auto-Save & Gamification Integration
|
||||||
* - Endlosschleifen-Timeout-Schutz (5000ms)
|
|
||||||
* - Reaktivität mit Fehler-Dolmetscher & Gamification
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
let monacoEditor = null;
|
let monacoEditor = null;
|
||||||
let pyodideInstance = null;
|
|
||||||
let pyodideLoading = false;
|
|
||||||
let currentChapterData = null;
|
let currentChapterData = null;
|
||||||
|
let currentLanguage = "python";
|
||||||
let autoSaveTimeout = null;
|
let autoSaveTimeout = null;
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
|
|
@ -27,13 +24,22 @@
|
||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Initialisiere Monaco Editor via CDN
|
function getActiveRunner() {
|
||||||
function initMonacoEditor(initialCode = "") {
|
if (window.RunnerRegistry) {
|
||||||
|
const runner = window.RunnerRegistry.get(currentLanguage);
|
||||||
|
if (runner) return runner;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Initialisiere Monaco Editor
|
||||||
|
function initMonacoEditor(initialCode = "", langId = "python") {
|
||||||
|
currentLanguage = langId;
|
||||||
const editorContainer = document.getElementById("monaco-editor-container");
|
const editorContainer = document.getElementById("monaco-editor-container");
|
||||||
if (!editorContainer) return;
|
if (!editorContainer) return;
|
||||||
|
|
||||||
if (window.monaco) {
|
if (window.monaco) {
|
||||||
createMonacoInstance(initialCode);
|
createMonacoInstance(initialCode, langId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +50,7 @@
|
||||||
paths: { vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs" }
|
paths: { vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs" }
|
||||||
});
|
});
|
||||||
window.require(["vs/editor/editor.main"], () => {
|
window.require(["vs/editor/editor.main"], () => {
|
||||||
createMonacoInstance(initialCode);
|
createMonacoInstance(initialCode, langId);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
loaderScript.onerror = () => {
|
loaderScript.onerror = () => {
|
||||||
|
|
@ -53,14 +59,17 @@
|
||||||
document.head.appendChild(loaderScript);
|
document.head.appendChild(loaderScript);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMonacoInstance(initialCode) {
|
function createMonacoInstance(initialCode, langId) {
|
||||||
const editorContainer = document.getElementById("monaco-editor-container");
|
const editorContainer = document.getElementById("monaco-editor-container");
|
||||||
if (!editorContainer) return;
|
if (!editorContainer) return;
|
||||||
editorContainer.innerHTML = "";
|
editorContainer.innerHTML = "";
|
||||||
|
|
||||||
|
const runner = getActiveRunner();
|
||||||
|
const monacoLang = runner ? runner.monacoLanguage : langId;
|
||||||
|
|
||||||
monacoEditor = window.monaco.editor.create(editorContainer, {
|
monacoEditor = window.monaco.editor.create(editorContainer, {
|
||||||
value: initialCode,
|
value: initialCode,
|
||||||
language: "python",
|
language: monacoLang,
|
||||||
theme: "vs-dark",
|
theme: "vs-dark",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace",
|
fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace",
|
||||||
|
|
@ -95,88 +104,29 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Pyodide WebAssembly Initialisierung mit Sandbox-Bootstrap
|
// 2. Code-Ausführung via aktivem Language-Runner
|
||||||
async function getPyodide() {
|
|
||||||
if (pyodideInstance) return pyodideInstance;
|
|
||||||
if (pyodideLoading) {
|
|
||||||
while (pyodideLoading) {
|
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
|
||||||
}
|
|
||||||
return pyodideInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
pyodideLoading = true;
|
|
||||||
logToTerminal("⏳ Initialisiere geschützte Python Sandbox Engine... Bitte kurz warten.\n", "info");
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!window.loadPyodide) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.src = "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js";
|
|
||||||
script.onload = resolve;
|
|
||||||
script.onerror = reject;
|
|
||||||
document.head.appendChild(script);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pyodideInstance = await window.loadPyodide({
|
|
||||||
stdout: (text) => logToTerminal(text + "\n", "stdout"),
|
|
||||||
stderr: (text) => logToTerminal(text + "\n", "stderr")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sandbox-Härtung im Pyodide-Interpreter aktivieren
|
|
||||||
if (window.CODE_GUARD && window.CODE_GUARD.getSandboxBootstrap) {
|
|
||||||
await pyodideInstance.runPythonAsync(window.CODE_GUARD.getSandboxBootstrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
logToTerminal("✅ Geschützte Python WebAssembly Sandbox bereitgestellt!\n\n", "success");
|
|
||||||
} catch (err) {
|
|
||||||
logToTerminal(`❌ Fehler beim Laden der Sandbox: ${err.message}\n`, "error");
|
|
||||||
} finally {
|
|
||||||
pyodideLoading = false;
|
|
||||||
}
|
|
||||||
return pyodideInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Code-Ausführung mit Sicherheitsprüfung & Timeout
|
|
||||||
async function runCode() {
|
async function runCode() {
|
||||||
const code = getEditorCode();
|
const code = getEditorCode();
|
||||||
clearTerminal();
|
clearTerminal();
|
||||||
|
|
||||||
// Layer 1: Code-Guard Sicherheits-Prüfung
|
const runner = getActiveRunner();
|
||||||
if (window.CODE_GUARD) {
|
if (!runner) {
|
||||||
const check = window.CODE_GUARD.validate(code);
|
logToTerminal(`❌ Kein Language-Runner für '${currentLanguage}' registriert.\n`, "error");
|
||||||
if (!check.safe) {
|
|
||||||
logToTerminal(`\n${check.error}\n\n`, "error");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
logToTerminal("▶ Starte Python-Skript (Strg+Enter)...\n----------------------------------------\n", "info");
|
logToTerminal(`▶ Starte Ausführung mit ${runner.displayName} (Strg+Enter)...\n----------------------------------------\n`, "info");
|
||||||
|
|
||||||
const py = await getPyodide();
|
const result = await runner.runCode(code, logToTerminal);
|
||||||
if (!py) return;
|
|
||||||
|
|
||||||
try {
|
if (result.success) {
|
||||||
let timeoutHandle;
|
logToTerminal("\n----------------------------------------\n✨ Erfolgreich ausgeführt!\n", "success");
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
} else if (result.error) {
|
||||||
timeoutHandle = setTimeout(() => {
|
suggestErrorInterpreter(result.error);
|
||||||
reject(new Error("Zeitüberschreitung: Dein Code lief länger als 5 Sekunden (Endlosschleife vermutet)."));
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
const execPromise = py.runPythonAsync(code);
|
|
||||||
await Promise.race([execPromise, timeoutPromise]);
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
|
|
||||||
logToTerminal("\n----------------------------------------\n✨ Skript erfolgreich ausgeführt!\n", "success");
|
|
||||||
} catch (err) {
|
|
||||||
logToTerminal(`\n❌ Ausführungsfehler:\n${err.message}\n`, "error");
|
|
||||||
suggestErrorInterpreter(err.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Unittest-Ausführung mit Sicherheitsprüfung
|
// 3. Test-Ausführung via aktivem Language-Runner
|
||||||
async function runTests() {
|
async function runTests() {
|
||||||
if (!currentChapterData || !currentChapterData.testCode) {
|
if (!currentChapterData || !currentChapterData.testCode) {
|
||||||
logToTerminal("❌ Keine Unittests für dieses Kapitel gefunden.\n", "error");
|
logToTerminal("❌ Keine Unittests für dieses Kapitel gefunden.\n", "error");
|
||||||
|
|
@ -186,78 +136,29 @@
|
||||||
const userCode = getEditorCode();
|
const userCode = getEditorCode();
|
||||||
clearTerminal();
|
clearTerminal();
|
||||||
|
|
||||||
// Layer 1: Code-Guard Sicherheits-Prüfung
|
const runner = getActiveRunner();
|
||||||
if (window.CODE_GUARD) {
|
if (!runner) {
|
||||||
const check = window.CODE_GUARD.validate(userCode);
|
logToTerminal(`❌ Kein Test-Runner für '${currentLanguage}' verfügbar.\n`, "error");
|
||||||
if (!check.safe) {
|
|
||||||
logToTerminal(`\n${check.error}\n\n`, "error");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
logToTerminal("🧪 Führe automatisierte Unittests aus (Strg+Shift+Enter)...\n----------------------------------------\n", "info");
|
logToTerminal(`🧪 Führe automatisierte Tests aus (Strg+Shift+Enter)...\n----------------------------------------\n`, "info");
|
||||||
|
|
||||||
const py = await getPyodide();
|
const result = await runner.runTests(userCode, currentChapterData.testCode, logToTerminal);
|
||||||
if (!py) return;
|
|
||||||
|
|
||||||
try {
|
if (result.success) {
|
||||||
py.FS.writeFile("aufgabe.py", userCode);
|
logToTerminal(`\n🎉 HERVORRAGEND! Alle ${result.total || 'erforderlichen'} Tests erfolgreich bestanden! (+100 XP)\n`, "success");
|
||||||
|
|
||||||
const testRunnerScript = `
|
|
||||||
import unittest, io, sys
|
|
||||||
from unittest import TextTestRunner
|
|
||||||
|
|
||||||
for mod in list(sys.modules.keys()):
|
|
||||||
if mod in ('aufgabe', 'test_aufgabe'):
|
|
||||||
del sys.modules[mod]
|
|
||||||
|
|
||||||
import aufgabe
|
|
||||||
|
|
||||||
${currentChapterData.testCode}
|
|
||||||
|
|
||||||
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
|
|
||||||
stream = io.StringIO()
|
|
||||||
runner = TextTestRunner(stream=stream, verbosity=2)
|
|
||||||
result = runner.run(suite)
|
|
||||||
|
|
||||||
output_text = stream.getvalue()
|
|
||||||
success = result.wasSuccessful()
|
|
||||||
total_runs = result.testsRun
|
|
||||||
failures = len(result.failures)
|
|
||||||
errors = len(result.errors)
|
|
||||||
(success, total_runs, failures, errors, output_text)
|
|
||||||
`;
|
|
||||||
|
|
||||||
let timeoutHandle;
|
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
|
||||||
timeoutHandle = setTimeout(() => {
|
|
||||||
reject(new Error("Zeitüberschreitung: Unittest lief länger als 6 Sekunden."));
|
|
||||||
}, 6000);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isSuccess, totalRuns, fails, errs, outputText] = await Promise.race([
|
|
||||||
py.runPythonAsync(testRunnerScript),
|
|
||||||
timeoutPromise
|
|
||||||
]);
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
|
|
||||||
logToTerminal(outputText + "\n", "stdout");
|
|
||||||
|
|
||||||
if (isSuccess) {
|
|
||||||
logToTerminal(`\n🎉 HERVORRAGEND! Alle ${totalRuns} Tests erfolgreich bestanden! (+100 XP)\n`, "success");
|
|
||||||
triggerSuccessCelebration();
|
triggerSuccessCelebration();
|
||||||
recordChapterSolved();
|
recordChapterSolved();
|
||||||
} else {
|
} else {
|
||||||
logToTerminal(`\n⚠️ ${fails + errs} von ${totalRuns} Tests fehlgeschlagen. Überarbeite deinen Code!\n`, "warning");
|
logToTerminal(`\n⚠️ ${result.failures || 1} Tests fehlgeschlagen. Überarbeite deinen Code!\n`, "warning");
|
||||||
suggestErrorInterpreter(outputText);
|
if (result.rawOutput) {
|
||||||
|
suggestErrorInterpreter(result.rawOutput);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
logToTerminal(`\n❌ Fehler beim Ausführen der Testsuite:\n${err.message}\n`, "error");
|
|
||||||
suggestErrorInterpreter(err.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Hilfsfunktionen
|
// 4. Hilfsfunktionen
|
||||||
function getEditorCode() {
|
function getEditorCode() {
|
||||||
if (monacoEditor) {
|
if (monacoEditor) {
|
||||||
return monacoEditor.getValue();
|
return monacoEditor.getValue();
|
||||||
|
|
@ -378,14 +279,15 @@ errors = len(result.errors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Init Workspace Controller
|
// 5. Init Workspace Controller
|
||||||
window.initWorkspace = function (chapterData) {
|
window.initWorkspace = function (chapterData, language = "python") {
|
||||||
currentChapterData = chapterData;
|
currentChapterData = chapterData;
|
||||||
|
currentLanguage = language || "python";
|
||||||
|
|
||||||
const savedDraft = localStorage.getItem(`code_draft_${chapterData.chapterId}`);
|
const savedDraft = localStorage.getItem(`code_draft_${chapterData.chapterId}`);
|
||||||
const initialCode = savedDraft || chapterData.starterCode || "";
|
const initialCode = savedDraft || chapterData.starterCode || "";
|
||||||
|
|
||||||
initMonacoEditor(initialCode);
|
initMonacoEditor(initialCode, currentLanguage);
|
||||||
|
|
||||||
document.getElementById("btn-run-code")?.addEventListener("click", runCode);
|
document.getElementById("btn-run-code")?.addEventListener("click", runCode);
|
||||||
document.getElementById("btn-run-tests")?.addEventListener("click", runTests);
|
document.getElementById("btn-run-tests")?.addEventListener("click", runTests);
|
||||||
|
|
|
||||||
202
assets/runners/python_runner.js
Normal file
202
assets/runners/python_runner.js
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
/**
|
||||||
|
* 🐍 PYTHON LANGUAGE RUNNER ADAPTER (Pyodide WebAssembly) 🐍
|
||||||
|
* ==========================================================
|
||||||
|
* Implementiert das standardisierte LanguageRunnerAdapter-Interface:
|
||||||
|
* - Pyodide WASM Engine mit In-Memory Python 3.12
|
||||||
|
* - CODE_GUARD Sicherheits-Prüfung & Sandbox-Bootstrap
|
||||||
|
* - Timeout-Schutz gegen Endlosschleifen
|
||||||
|
* - Unittest-Runner für aufgabe.py & test_aufgabe.py
|
||||||
|
* - Fehler-Dolmetscher Anbindung
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
let pyodideInstance = null;
|
||||||
|
let pyodideLoading = false;
|
||||||
|
|
||||||
|
const PythonAdapter = {
|
||||||
|
id: "python",
|
||||||
|
displayName: "Python 3.12 (Pyodide WebAssembly)",
|
||||||
|
icon: "🐍",
|
||||||
|
monacoLanguage: "python",
|
||||||
|
fileExtension: ".py",
|
||||||
|
testFileExtension: "test_aufgabe.py",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt und initialisiert die Pyodide WebAssembly Engine
|
||||||
|
*/
|
||||||
|
async loadEngine(logCallback) {
|
||||||
|
if (pyodideInstance) return pyodideInstance;
|
||||||
|
if (pyodideLoading) {
|
||||||
|
while (pyodideLoading) {
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
return pyodideInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
pyodideLoading = true;
|
||||||
|
if (logCallback) logCallback("⏳ Initialisiere Python WebAssembly Engine (Pyodide)... Bitte kurz warten.\n", "info");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!window.loadPyodide) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js";
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = reject;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pyodideInstance = await window.loadPyodide();
|
||||||
|
|
||||||
|
// Sandbox-Härtung im Pyodide-Interpreter aktivieren
|
||||||
|
if (window.CODE_GUARD && window.CODE_GUARD.getSandboxBootstrap) {
|
||||||
|
await pyodideInstance.runPythonAsync(window.CODE_GUARD.getSandboxBootstrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logCallback) logCallback("✅ Python WebAssembly Sandbox bereitgestellt!\n\n", "success");
|
||||||
|
} catch (err) {
|
||||||
|
if (logCallback) logCallback(`❌ Fehler beim Laden der Python Sandbox: ${err.message}\n`, "error");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
pyodideLoading = false;
|
||||||
|
}
|
||||||
|
return pyodideInstance;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sicherheitsprüfung vor Code-Ausführung
|
||||||
|
*/
|
||||||
|
validateSecurity(code) {
|
||||||
|
if (window.CODE_GUARD && window.CODE_GUARD.validate) {
|
||||||
|
return window.CODE_GUARD.validate(code);
|
||||||
|
}
|
||||||
|
return { safe: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt Python-Code aus (▶ Code ausführen)
|
||||||
|
*/
|
||||||
|
async runCode(code, logCallback) {
|
||||||
|
const securityCheck = this.validateSecurity(code);
|
||||||
|
if (!securityCheck.safe) {
|
||||||
|
logCallback(`\n${securityCheck.error}\n\n`, "error");
|
||||||
|
return { success: false, error: securityCheck.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const py = await this.loadEngine(logCallback);
|
||||||
|
if (!py) return { success: false, error: "Pyodide nicht verfügbar" };
|
||||||
|
|
||||||
|
// Umleitung von stdout & stderr
|
||||||
|
py.setStdout({ batched: (text) => logCallback(text + "\n", "stdout") });
|
||||||
|
py.setStderr({ batched: (text) => logCallback(text + "\n", "stderr") });
|
||||||
|
|
||||||
|
try {
|
||||||
|
let timeoutHandle;
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
timeoutHandle = setTimeout(() => {
|
||||||
|
reject(new Error("Zeitüberschreitung: Dein Code lief länger als 5 Sekunden (Endlosschleife vermutet)."));
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const execPromise = py.runPythonAsync(code);
|
||||||
|
await Promise.race([execPromise, timeoutPromise]);
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
logCallback(`\n❌ Ausführungsfehler:\n${err.message}\n`, "error");
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt automatisierte Unittests aus (🧪 Tests starten)
|
||||||
|
*/
|
||||||
|
async runTests(userCode, testCode, logCallback) {
|
||||||
|
const securityCheck = this.validateSecurity(userCode);
|
||||||
|
if (!securityCheck.safe) {
|
||||||
|
logCallback(`\n${securityCheck.error}\n\n`, "error");
|
||||||
|
return { success: false, error: securityCheck.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const py = await this.loadEngine(logCallback);
|
||||||
|
if (!py) return { success: false, error: "Pyodide nicht verfügbar" };
|
||||||
|
|
||||||
|
try {
|
||||||
|
py.FS.writeFile("aufgabe.py", userCode);
|
||||||
|
|
||||||
|
const testRunnerScript = `
|
||||||
|
import unittest, io, sys
|
||||||
|
from unittest import TextTestRunner
|
||||||
|
|
||||||
|
for mod in list(sys.modules.keys()):
|
||||||
|
if mod in ('aufgabe', 'test_aufgabe'):
|
||||||
|
del sys.modules[mod]
|
||||||
|
|
||||||
|
import aufgabe
|
||||||
|
|
||||||
|
${testCode}
|
||||||
|
|
||||||
|
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
|
||||||
|
stream = io.StringIO()
|
||||||
|
runner = TextTestRunner(stream=stream, verbosity=2)
|
||||||
|
result = runner.run(suite)
|
||||||
|
|
||||||
|
output_text = stream.getvalue()
|
||||||
|
success = result.wasSuccessful()
|
||||||
|
total_runs = result.testsRun
|
||||||
|
failures = len(result.failures)
|
||||||
|
errors = len(result.errors)
|
||||||
|
(success, total_runs, failures, errors, output_text)
|
||||||
|
`;
|
||||||
|
|
||||||
|
let timeoutHandle;
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
timeoutHandle = setTimeout(() => {
|
||||||
|
reject(new Error("Zeitüberschreitung: Unittest lief länger als 6 Sekunden."));
|
||||||
|
}, 6000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isSuccess, totalRuns, fails, errs, outputText] = await Promise.race([
|
||||||
|
py.runPythonAsync(testRunnerScript),
|
||||||
|
timeoutPromise
|
||||||
|
]);
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
|
||||||
|
logCallback(outputText + "\n", "stdout");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: Boolean(isSuccess),
|
||||||
|
total: totalRuns,
|
||||||
|
passed: totalRuns - (fails + errs),
|
||||||
|
failures: fails,
|
||||||
|
errors: errs,
|
||||||
|
rawOutput: outputText
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
logCallback(`\n❌ Fehler beim Ausführen der Testsuite:\n${err.message}\n`, "error");
|
||||||
|
return { success: false, error: err.message, rawOutput: err.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Übersetzt Python-Tracebacks
|
||||||
|
*/
|
||||||
|
translateError(traceback) {
|
||||||
|
if (window.INTERPRETER && window.INTERPRETER.translate) {
|
||||||
|
return window.INTERPRETER.translate(traceback);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// In Registry registrieren
|
||||||
|
if (window.RunnerRegistry) {
|
||||||
|
window.RunnerRegistry.register(PythonAdapter);
|
||||||
|
} else {
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
if (window.RunnerRegistry) window.RunnerRegistry.register(PythonAdapter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
52
assets/runners/runner_registry.js
Normal file
52
assets/runners/runner_registry.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* 🔌 UNIVERSAL LANGUAGE RUNNER REGISTRY 🔌
|
||||||
|
* =========================================
|
||||||
|
* Verwaltet austauschbare Language-Runner für polyglotte Code-Ausführung
|
||||||
|
* (Python, JavaScript, SQL, Rust etc.) in der Monaco Web-IDE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const runners = new Map();
|
||||||
|
|
||||||
|
window.RunnerRegistry = {
|
||||||
|
/**
|
||||||
|
* Registriert einen neuen Language Runner Adapter
|
||||||
|
* @param {Object} adapter
|
||||||
|
*/
|
||||||
|
register(adapter) {
|
||||||
|
if (!adapter || !adapter.id) {
|
||||||
|
throw new Error("Runner Adapter muss eine gültige 'id' besitzen.");
|
||||||
|
}
|
||||||
|
runners.set(adapter.id.toLowerCase(), adapter);
|
||||||
|
console.log(`🔌 [RunnerRegistry] Runner '${adapter.displayName || adapter.id}' erfolgreich registriert.`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruft einen registrierten Runner anhand der Sprach-ID ab
|
||||||
|
* @param {string} languageId
|
||||||
|
* @returns {Object|null}
|
||||||
|
*/
|
||||||
|
get(languageId) {
|
||||||
|
if (!languageId) return null;
|
||||||
|
return runners.get(languageId.toLowerCase()) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt alle registrierten Sprach-IDs zurück
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
list() {
|
||||||
|
return Array.from(runners.keys());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob ein Runner für eine Sprache verfügbar ist
|
||||||
|
* @param {string} languageId
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
has(languageId) {
|
||||||
|
if (!languageId) return false;
|
||||||
|
return runners.has(languageId.toLowerCase());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
77
courses/python/course.json
Normal file
77
courses/python/course.json
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
{
|
||||||
|
"id": "python",
|
||||||
|
"title": "Python Software Engineering",
|
||||||
|
"language": "python",
|
||||||
|
"icon": "🐍",
|
||||||
|
"runner": "python",
|
||||||
|
"description": "Vom Einsteiger über professionelle Objektorientierung bis hin zu Data Engineering und Desktop-Apps in 33 Modulen.",
|
||||||
|
"tracks": [
|
||||||
|
{
|
||||||
|
"id": "lehrpfad_1_grundlagen",
|
||||||
|
"title": "🌱 Lehrpfad 1: Grundlagen der Programmierung",
|
||||||
|
"description": "Zahlen, Variablen, Bedingungen, Schleifen, Funktionen, Listen, Strings, Dicts & Sets.",
|
||||||
|
"certificateKey": "lehrpfad_1",
|
||||||
|
"chapters": [
|
||||||
|
{ "folder": "01_erste_schritte_taschenrechner", "title": "G01: Python als Taschenrechner", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "02_variablen_und_datentypen", "title": "G02: Variablen & Datentypen", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "03_ein_und_ausgabe", "title": "G03: Interaktive Ein- & Ausgabe", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "04_verzweigungen_und_bedingungen", "title": "G04: Verzweigungen & Bedingungen", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "05_schleifen_und_wiederholungen", "title": "G05: Schleifen & Wiederholungen", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "06_funktionen_und_module", "title": "G06: Eigene Funktionen & Module", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "07_listen_und_sequenzen", "title": "G07: Listen & Sequenzen", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "08_textverarbeitung_und_strings", "title": "G08: Textverarbeitung & Strings", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "09_dictionaries_und_sets", "title": "G09: Dictionaries & Sets", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "10_comprehensions_datum_algorithmen", "title": "G10: Comprehensions & Algorithmen", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lehrpfad_2_oop_einstieg",
|
||||||
|
"title": "🏗️ Lehrpfad 2: Einstieg in die Objektorientierung (OOP)",
|
||||||
|
"description": "Klassen, Konstruktor, self, Methoden, Tools (VS Code & Git) und Tamagotchi-Projekt.",
|
||||||
|
"certificateKey": "lehrpfad_2",
|
||||||
|
"chapters": [
|
||||||
|
{ "folder": "00_fehlersuche_und_grundlagen", "title": "OOP 00: Python-Fehlersuche", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "01_einstieg_klassen", "title": "OOP 01: Erste Klassen & Instanzen", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "02_init_und_self", "title": "OOP 02: Konstruktor __init__ & self", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "03_methoden_und_verhalten", "title": "OOP 03: Methoden & Verhalten", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "04_str_und_darstellung", "title": "OOP 04: __str__ & String-Darstellung", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "04b_umstieg_vscode", "title": "Tooling 04b: Umstieg auf VS Code", "taskFile": "vscode_test.py", "testFile": "" },
|
||||||
|
{ "folder": "04c_git_und_versionskontrolle", "title": "Tooling 04c: Git & Versionskontrolle", "taskFile": "git_uebung.py", "testFile": "" },
|
||||||
|
{ "folder": "05_objekte_kombinieren", "title": "OOP 05: Objekte kombinieren", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "06_abschlussprojekt_tamagotchi", "title": "Projekt 06: Virtuelles Haustier (Tamagotchi)", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lehrpfad_3_fortgeschrittenes_oop",
|
||||||
|
"title": "🚀 Lehrpfad 3: Fortgeschrittenes OOP & GUIs",
|
||||||
|
"description": "Speicher-Referenzen, Dunder-Methoden, TDD, Vererbung, Polymorphie, Exceptions, JSON/CSV, Tkinter & Masterprojekt.",
|
||||||
|
"certificateKey": "lehrpfad_3",
|
||||||
|
"chapters": [
|
||||||
|
{ "folder": "07_referenzen_und_speicher", "title": "Adv 07: Referenzen & Speicher", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "08_operator_overloading_dunder", "title": "Adv 08: Operator Overloading & Dunder", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "09_eigene_unit_tests_schreiben", "title": "Adv 09: Eigene Unit Tests & TDD", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "10_vererbung_und_super", "title": "Adv 10: Vererbung & super()", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "11_polymorphie_und_interfaces", "title": "Adv 11: Polymorphie & Interfaces", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "12_exceptions_und_fehlerbehandlung", "title": "Adv 12: Exceptions & Fehlerbehandlung", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "13_persistenz_json_und_csv", "title": "Adv 13: Datei-Persistenz (JSON & CSV)", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "14_gui_mit_tkinter", "title": "Adv 14: Desktop-GUIs mit Tkinter", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "15_parameter_und_container", "title": "Adv 15: Parameter & Eigene Container", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "16_master_abschlussprojekt", "title": "Master 16: Master-Abschlussprojekt", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lehrpfad_4_professional_data_engineering",
|
||||||
|
"title": "🌟 Lehrpfad 4: Python Professional & Data Engineering",
|
||||||
|
"description": "Reguläre Ausdrücke (re), funktionale Idiome, Generatoren, Dataclasses, Type Hints, SQLite & REST-APIs.",
|
||||||
|
"certificateKey": "lehrpfad_4",
|
||||||
|
"chapters": [
|
||||||
|
{ "folder": "17_regulaere_ausdruecke_re", "title": "Pro 17: Reguläre Ausdrücke (re)", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "18_funktionale_programmierung_lambda", "title": "Pro 18: Funktionale Programmierung (lambda)", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "19_generatoren_und_itertools", "title": "Pro 19: Generatoren & itertools (yield)", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "20_dataclasses_und_typehints", "title": "Pro 20: Dataclasses & Type Hints", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "21_datenbanken_und_sqlite", "title": "Pro 21: Relationale Datenbanken & SQLite", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" },
|
||||||
|
{ "folder": "22_web_apis_und_json_feeds", "title": "Pro 22: Web-APIs & JSON-Feeds", "taskFile": "aufgabe.py", "testFile": "test_aufgabe.py" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -328,47 +328,19 @@
|
||||||
<script src="assets/certificate.js"></script>
|
<script src="assets/certificate.js"></script>
|
||||||
<script src="assets/parsons.js"></script>
|
<script src="assets/parsons.js"></script>
|
||||||
<script src="assets/code_guard.js"></script>
|
<script src="assets/code_guard.js"></script>
|
||||||
|
<script src="assets/runners/runner_registry.js"></script>
|
||||||
|
<script src="assets/runners/python_runner.js"></script>
|
||||||
<script src="assets/monaco_ide.js"></script>
|
<script src="assets/monaco_ide.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Manifest aller Kapitel für sequentielle Navigation
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const CHAPTERS_MANIFEST = [
|
const courseId = urlParams.get("course") || "python";
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "01_erste_schritte_taschenrechner", title: "G01. Taschenrechner" },
|
const trackFolder = urlParams.get("track") || "lehrpfad_1_grundlagen";
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "02_variablen_und_datentypen", title: "G02. Variablen & Typen" },
|
const chapterFolder = urlParams.get("chapter") || "01_erste_schritte_taschenrechner";
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "03_ein_und_ausgabe", title: "G03. Ein- & Ausgabe" },
|
const chapterId = `${trackFolder}/${chapterFolder}`;
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "04_verzweigungen_und_bedingungen", title: "G04. Bedingungen" },
|
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "05_schleifen_und_wiederholungen", title: "G05. Schleifen" },
|
let CHAPTERS_MANIFEST = [];
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "06_funktionen_und_module", title: "G06. Funktionen & Module" },
|
let currentCourseManifest = null;
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "07_listen_und_sequenzen", title: "G07. Listen" },
|
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "08_textverarbeitung_und_strings", title: "G08. Strings" },
|
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "09_dictionaries_und_sets", title: "G09. Dicts & Sets" },
|
|
||||||
{ track: "lehrpfad_1_grundlagen", folder: "10_comprehensions_datum_algorithmen", title: "G10. Comprehensions" },
|
|
||||||
{ track: "lehrpfad_2_oop_einstieg", folder: "00_fehlersuche_und_grundlagen", title: "OOP 00. Debugging" },
|
|
||||||
{ track: "lehrpfad_2_oop_einstieg", folder: "01_einstieg_klassen", title: "OOP 01. Klassen" },
|
|
||||||
{ track: "lehrpfad_2_oop_einstieg", folder: "02_init_und_self", title: "OOP 02. Konstruktor" },
|
|
||||||
{ track: "lehrpfad_2_oop_einstieg", folder: "03_methoden_und_verhalten", title: "OOP 03. Methoden" },
|
|
||||||
{ track: "lehrpfad_2_oop_einstieg", folder: "04_str_und_darstellung", title: "OOP 04. __str__" },
|
|
||||||
{ track: "lehrpfad_2_oop_einstieg", folder: "04b_umstieg_vscode", title: "Tooling 04b. VS Code" },
|
|
||||||
{ track: "lehrpfad_2_oop_einstieg", folder: "04c_git_und_versionskontrolle", title: "Tooling 04c. Git" },
|
|
||||||
{ track: "lehrpfad_2_oop_einstieg", folder: "05_objekte_kombinieren", title: "OOP 05. Komposition" },
|
|
||||||
{ track: "lehrpfad_2_oop_einstieg", folder: "06_abschlussprojekt_tamagotchi", title: "Projekt 06. Tamagotchi" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "07_referenzen_und_speicher", title: "Adv 07. Speicher" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "08_operator_overloading_dunder", title: "Adv 08. Dunder" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "09_eigene_unit_tests_schreiben", title: "Adv 09. TDD Tests" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "10_vererbung_und_super", title: "Adv 10. Vererbung" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "11_polymorphie_und_interfaces", title: "Adv 11. Polymorphie" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "12_exceptions_und_fehlerbehandlung", title: "Adv 12. Exceptions" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "13_persistenz_json_und_csv", title: "Adv 13. Persistenz" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "14_gui_mit_tkinter", title: "Adv 14. Tkinter" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "15_parameter_und_container", title: "Adv 15. Container" },
|
|
||||||
{ track: "lehrpfad_3_fortgeschrittenes_oop", folder: "16_master_abschlussprojekt", title: "Master 16. Abschlussprojekt" },
|
|
||||||
{ track: "lehrpfad_4_professional_data_engineering", folder: "17_regulaere_ausdruecke_re", title: "Pro 17. Regex & Text Mining" },
|
|
||||||
{ track: "lehrpfad_4_professional_data_engineering", folder: "18_funktionale_programmierung_lambda", title: "Pro 18. Functional & Lambda" },
|
|
||||||
{ track: "lehrpfad_4_professional_data_engineering", folder: "19_generatoren_und_itertools", title: "Pro 19. Generatoren & Itertools" },
|
|
||||||
{ track: "lehrpfad_4_professional_data_engineering", folder: "20_dataclasses_und_typehints", title: "Pro 20. Dataclasses & Decorators" },
|
|
||||||
{ track: "lehrpfad_4_professional_data_engineering", folder: "21_datenbanken_und_sqlite", title: "Pro 21. SQLite & Relationale DBs" },
|
|
||||||
{ track: "lehrpfad_4_professional_data_engineering", folder: "22_web_apis_und_json_feeds", title: "Pro 22. REST APIs & JSON Feeds" }
|
|
||||||
];
|
|
||||||
|
|
||||||
function switchTab(tabId) {
|
function switchTab(tabId) {
|
||||||
document.querySelectorAll(".pane-tab-btn").forEach(b => b.classList.remove("active"));
|
document.querySelectorAll(".pane-tab-btn").forEach(b => b.classList.remove("active"));
|
||||||
|
|
@ -385,22 +357,45 @@
|
||||||
if (el) el.style.display = el.style.display === "block" ? "none" : "block";
|
if (el) el.style.display = el.style.display === "block" ? "none" : "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
async function loadCourseManifest() {
|
||||||
const trackFolder = urlParams.get("track") || "lehrpfad_1_grundlagen";
|
try {
|
||||||
const chapterFolder = urlParams.get("chapter") || "01_erste_schritte_taschenrechner";
|
const res = await fetch(`courses/${courseId}/course.json`);
|
||||||
const chapterId = `${trackFolder}/${chapterFolder}`;
|
if (res.ok) {
|
||||||
|
currentCourseManifest = await res.json();
|
||||||
|
const list = [];
|
||||||
|
(currentCourseManifest.tracks || []).forEach(tr => {
|
||||||
|
(tr.chapters || []).forEach(ch => {
|
||||||
|
list.push({
|
||||||
|
track: tr.id,
|
||||||
|
folder: ch.folder,
|
||||||
|
title: ch.title,
|
||||||
|
taskFile: ch.taskFile || "aufgabe.py",
|
||||||
|
testFile: ch.testFile || "test_aufgabe.py"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
CHAPTERS_MANIFEST = list;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Konnte course.json nicht laden, nutze Fallback-Manifest:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Nächstes Kapitel ermitteln
|
async function setupNextChapterButton() {
|
||||||
const currentIndex = CHAPTERS_MANIFEST.findIndex(c => c.folder === chapterFolder);
|
const currentIndex = CHAPTERS_MANIFEST.findIndex(c => c.folder === chapterFolder);
|
||||||
const nextChapter = (currentIndex >= 0 && currentIndex < CHAPTERS_MANIFEST.length - 1) ? CHAPTERS_MANIFEST[currentIndex + 1] : null;
|
const nextChapter = (currentIndex >= 0 && currentIndex < CHAPTERS_MANIFEST.length - 1) ? CHAPTERS_MANIFEST[currentIndex + 1] : null;
|
||||||
|
|
||||||
document.getElementById("btn-next-chapter")?.addEventListener("click", () => {
|
const nextBtn = document.getElementById("btn-next-chapter");
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.onclick = () => {
|
||||||
if (nextChapter) {
|
if (nextChapter) {
|
||||||
window.location.href = `workspace.html?track=${nextChapter.track}&chapter=${nextChapter.folder}`;
|
window.location.href = `workspace.html?course=${courseId}&track=${nextChapter.track}&chapter=${nextChapter.folder}`;
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "index.html#zertifikate";
|
window.location.href = "index.html#zertifikate";
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadWorkspaceData() {
|
async function loadWorkspaceData() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -488,13 +483,17 @@
|
||||||
document.getElementById("hints-modal-overlay").style.display = "flex";
|
document.getElementById("hints-modal-overlay").style.display = "flex";
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. Monaco IDE initialisieren
|
await loadCourseManifest();
|
||||||
|
setupNextChapterButton();
|
||||||
|
|
||||||
|
// 5. Monaco IDE initialisieren mit modularer Sprache
|
||||||
|
const lang = currentCourseManifest ? (currentCourseManifest.language || "python") : "python";
|
||||||
window.initWorkspace({
|
window.initWorkspace({
|
||||||
chapterId: chapterId,
|
chapterId: chapterId,
|
||||||
starterCode: aufgabeCode,
|
starterCode: aufgabeCode,
|
||||||
testCode: testCode,
|
testCode: testCode,
|
||||||
solutionCode: loesungCode
|
solutionCode: loesungCode
|
||||||
});
|
}, lang);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
document.getElementById("tab-content-theorie").innerHTML = `<div class="box box-danger">Fehler beim Laden: ${err.message}</div>`;
|
document.getElementById("tab-content-theorie").innerHTML = `<div class="box box-danger">Fehler beim Laden: ${err.message}</div>`;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue