From 54f115204f8fda7fd54e8f47caa0baa9716b1bd9 Mon Sep 17 00:00:00 2001 From: dexta Date: Tue, 28 Jul 2026 16:19:17 +0200 Subject: [PATCH] middle step --- 00_aMeta/toolz/CYOA/frontend/dom_rendering.js | 173 ++++++++ 00_aMeta/toolz/CYOA/frontend/engine_ui.js | 75 ++++ 00_aMeta/toolz/CYOA/frontend/style.css | 69 ++++ 00_aMeta/toolz/CYOA/frontend/template.html | 373 ++++++++++++++++++ 00_aMeta/toolz/CYOA/main.go | 155 +++----- 00_aMeta/toolz/CYOA/templates/template.html | 336 ---------------- 6 files changed, 742 insertions(+), 439 deletions(-) create mode 100644 00_aMeta/toolz/CYOA/frontend/dom_rendering.js create mode 100644 00_aMeta/toolz/CYOA/frontend/engine_ui.js create mode 100644 00_aMeta/toolz/CYOA/frontend/style.css create mode 100644 00_aMeta/toolz/CYOA/frontend/template.html delete mode 100644 00_aMeta/toolz/CYOA/templates/template.html diff --git a/00_aMeta/toolz/CYOA/frontend/dom_rendering.js b/00_aMeta/toolz/CYOA/frontend/dom_rendering.js new file mode 100644 index 0000000..684a537 --- /dev/null +++ b/00_aMeta/toolz/CYOA/frontend/dom_rendering.js @@ -0,0 +1,173 @@ +// engine_ui.js - Reines DOM-Handling und visuelle Logik +const EngineUI = { + audioCtx: null, + + init: function() { + this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + + const rawData = document.getElementById("story-data").textContent.trim(); + EngineLogic.init(rawData); + + this.attachEvents(); + this.renderNode(EngineLogic.state.node); + }, + + playBeep: function(freq = 440, type = "square", duration = 0.05) { + try { + if(!this.audioCtx) return; + const osc = this.audioCtx.createOscillator(); + const gain = this.audioCtx.createGain(); + osc.type = type; + osc.frequency.setValueAtTime(freq, this.audioCtx.currentTime); + gain.gain.setValueAtTime(0.05, this.audioCtx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + duration); + osc.connect(gain); gain.connect(this.audioCtx.destination); + osc.start(); osc.stop(this.audioCtx.currentTime + duration); + } catch(e) {} + }, + + renderHUD: function() { + const hud = document.getElementById("hud-dynamic"); + hud.innerHTML = ""; + + hud.innerHTML += `
REBOOTS: #${String(EngineLogic.state.reboots).padStart(3, "0")}
`; + hud.innerHTML += `
KNOTEN: ${EngineLogic.state.node}
`; + + Object.keys(EngineLogic.state).forEach(key => { + if(key !== "reboots" && key !== "node") { + hud.innerHTML += `
${key.toUpperCase()}: ${EngineLogic.state[key]}
`; + } + }); + }, + + renderNode: function(id) { + const node = EngineLogic.processNodeEntry(id); + if (!node) return; + + this.playBeep(600, "sine", 0.04); + if (node.isReboot) this.playBeep(200, "sawtooth", 0.3); + + this.renderHUD(); + + document.getElementById("node-title").innerText = "=== " + node.title + " ==="; + document.getElementById("node-body").innerHTML = node.content; + + const choicesBox = document.getElementById("choices-box"); + choicesBox.innerHTML = ""; + + if (node.choices && node.choices.length > 0) { + let visibleIndex = 1; + node.choices.forEach((choice) => { + const parsed = EngineLogic.parseRequirement(choice.text); + const btn = document.createElement("button"); + btn.className = "choice-btn"; + + if (parsed.meetsCondition) { + btn.innerHTML = `[${visibleIndex}] ${parsed.displayText}`; + btn.dataset.target = choice.targetId; + btn.dataset.index = visibleIndex; + btn.onclick = () => this.renderNode(choice.targetId); + visibleIndex++; + } else { + btn.innerHTML = `[LOCKED] ${parsed.displayText} ${parsed.missingReq}`; + btn.disabled = true; + } + choicesBox.appendChild(btn); + }); + } else { + const btn = document.createElement("button"); + btn.className = "choice-btn"; + btn.style.borderColor = "var(--term-amber)"; + btn.style.color = "var(--term-amber)"; + btn.innerHTML = "[R] Speicherabbild sichern und rebooten..."; + btn.onclick = () => this.triggerReboot(); + choicesBox.appendChild(btn); + } + document.getElementById("terminal-viewport").scrollTop = 0; + }, + + triggerReboot: function() { + const startId = EngineLogic.triggerReboot(); + if (startId) this.renderNode(startId); + }, + + toggleGraph: function() { + const modal = document.getElementById("graph-modal"); + if (modal.style.display === "flex") { + modal.style.display = "none"; + } else { + modal.style.display = "flex"; + this.drawGraphCanvas(); + } + }, + + drawGraphCanvas: function() { + const canvas = document.getElementById("graph-canvas"); + canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "#000"; ctx.fillRect(0, 0, canvas.width, canvas.height); + + const keys = Object.keys(EngineLogic.storyData.nodes || {}); + if (keys.length === 0) return; + + const total = keys.length; const radius = Math.min(canvas.width, canvas.height) * 0.38; + const centerX = canvas.width / 2; const centerY = canvas.height / 2; + const nodeRadius = 8; const positions = {}; + + keys.forEach((key, idx) => { + const angle = (idx / total) * 2 * Math.PI; + positions[key] = { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) }; + }); + + ctx.lineWidth = 1; + keys.forEach(key => { + const node = EngineLogic.storyData.nodes[key]; + if (node && node.choices) { + node.choices.forEach(c => { + if (positions[c.targetId]) { + const start = positions[key], end = positions[c.targetId]; + ctx.beginPath(); ctx.strokeStyle = "rgba(51, 255, 102, 0.4)"; + ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.stroke(); + const dx = end.x - start.x, dy = end.y - start.y, angle = Math.atan2(dy, dx), headlen = 12; + const tipX = end.x - Math.cos(angle) * (nodeRadius + 2), tipY = end.y - Math.sin(angle) * (nodeRadius + 2); + ctx.beginPath(); ctx.moveTo(tipX, tipY); + ctx.lineTo(tipX - headlen * Math.cos(angle - Math.PI / 7), tipY - headlen * Math.sin(angle - Math.PI / 7)); + ctx.lineTo(tipX - headlen * Math.cos(angle + Math.PI / 7), tipY - headlen * Math.sin(angle + Math.PI / 7)); + ctx.closePath(); ctx.fillStyle = "rgba(51, 255, 102, 0.9)"; ctx.fill(); + } + }); + } + }); + + keys.forEach(key => { + const pos = positions[key]; + ctx.beginPath(); ctx.arc(pos.x, pos.y, nodeRadius, 0, 2 * Math.PI); + if (key === EngineLogic.state.node) ctx.fillStyle = "#00ffff"; + else if (key === EngineLogic.storyData.startNodeId) ctx.fillStyle = "#ffb000"; + else ctx.fillStyle = "#116633"; + ctx.fill(); ctx.strokeStyle = "#33ff66"; ctx.stroke(); + ctx.fillStyle = "#fff"; ctx.font = "11px monospace"; + const textAngle = Math.atan2(pos.y - centerY, pos.x - centerX); + ctx.fillText(key, pos.x + Math.cos(textAngle) * 15 - 10, pos.y + Math.sin(textAngle) * 15 + 4); + }); + }, + + attachEvents: function() { + window.addEventListener("keydown", (e) => { + if (document.getElementById("graph-modal").style.display === "flex") { + if (e.key.toLowerCase() === "g" || e.key === "Escape") this.toggleGraph(); + return; + } + if (e.key >= "1" && e.key <= "9") { + const btn = document.querySelector(`.choice-btn[data-index="${e.key}"]`); + if (btn && !btn.disabled) this.renderNode(btn.dataset.target); + } else if (e.key.toLowerCase() === "r") { + this.triggerReboot(); + } else if (e.key.toLowerCase() === "g") { + this.toggleGraph(); + } + }); + } +}; + +window.onload = () => EngineUI.init(); \ No newline at end of file diff --git a/00_aMeta/toolz/CYOA/frontend/engine_ui.js b/00_aMeta/toolz/CYOA/frontend/engine_ui.js new file mode 100644 index 0000000..0ac9751 --- /dev/null +++ b/00_aMeta/toolz/CYOA/frontend/engine_ui.js @@ -0,0 +1,75 @@ +// engine_logic.js - Reine State- und Datenverarbeitung +const EngineLogic = { + storyData: { nodes: {} }, + state: { reboots: 1, node: "" }, + + init: function(rawJsonData) { + try { + if (!rawJsonData || rawJsonData.trim() === "") throw "Leer"; + this.storyData = JSON.parse(rawJsonData); + } catch(e) { + console.warn("[System] Go-Engine Offline. Fallback Modus geladen."); + this.storyData = { + startNodeId: "err", + nodes: { + "err": { + id: "err", title: "SYSTEM OFFLINE", + content: "

Konnte [story-data] nicht parsen. Go-Server läuft nicht.

", + choices: [] + } + } + }; + } + this.state.node = this.storyData.startNodeId || Object.keys(this.storyData.nodes)[0] || ""; + }, + + getNode: function(id) { + return this.storyData.nodes[id]; + }, + + processNodeEntry: function(id) { + const node = this.getNode(id); + if (!node) return null; + + this.state.node = id; + + // Metadaten anwenden (z.B. [meta: schild=80]) + if (node.metadata) { + Object.keys(node.metadata).forEach(key => { + this.state[key] = node.metadata[key]; + }); + } + + // Reboot Erkennung + if (node.isReboot) { + this.state.reboots++; + } + + return node; + }, + + parseRequirement: function(choiceText) { + let conditionMatch = choiceText.match(/\[req:\s*([a-zA-Z0-9_]+)\s*=\s*([^\]]+)\]/i); + let displayText = choiceText; + let meetsCondition = true; + let missingReq = ""; + + if (conditionMatch) { + let reqKey = conditionMatch[1].toLowerCase(); + let reqVal = conditionMatch[2]; + if (String(this.state[reqKey]) !== String(reqVal)) { + meetsCondition = false; + missingReq = `(Benötigt: ${reqKey}=${reqVal})`; + } + displayText = displayText.replace(conditionMatch[0], "").trim(); + } + + return { displayText, meetsCondition, missingReq }; + }, + + triggerReboot: function() { + const currentReboots = this.state.reboots + 1; + this.state = { reboots: currentReboots }; // Resets inventory/state + return this.storyData.startNodeId; + } +}; \ No newline at end of file diff --git a/00_aMeta/toolz/CYOA/frontend/style.css b/00_aMeta/toolz/CYOA/frontend/style.css new file mode 100644 index 0000000..b8b6c34 --- /dev/null +++ b/00_aMeta/toolz/CYOA/frontend/style.css @@ -0,0 +1,69 @@ +:root { + --bg-color: #050b08; + --term-green: #33ff66; + --term-dim: #116633; + --term-cyan: #00ffff; + --term-warn: #ff3333; + --term-amber: #ffb000; + --font-stack: 'Courier New', Courier, monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + background-color: var(--bg-color); color: var(--term-green); + font-family: var(--font-stack); height: 100vh; display: flex; + flex-direction: column; overflow: hidden; text-shadow: 0 0 4px rgba(51, 255, 102, 0.6); +} + +body::before { + content: " "; display: block; position: absolute; + top: 0; left: 0; bottom: 0; right: 0; + background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); + z-index: 10; background-size: 100% 3px, 3px 100%; pointer-events: none; +} + +header { background: #111; border-bottom: 2px solid var(--term-green); padding: 8px 16px; display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem; } +.sys-title { color: var(--term-cyan); font-weight: bold; letter-spacing: 1px; } + +.hud-stats { display: flex; gap: 15px; font-size: 0.85rem; flex-wrap: wrap; } +.stat-item { border: 1px solid var(--term-dim); padding: 2px 8px; background: rgba(0, 20, 10, 0.5); } + +main { flex: 1; display: flex; flex-direction: column; padding: 20px; max-width: 900px; margin: 0 auto; width: 100%; overflow-y: auto; position: relative; z-index: 1; } +.node-card { border: 1px solid var(--term-dim); background: rgba(5, 15, 10, 0.85); padding: 20px; margin-bottom: 20px; box-shadow: 0 0 15px rgba(0, 255, 102, 0.1); } +.node-title { font-size: 1.3rem; color: var(--term-cyan); border-bottom: 1px dashed var(--term-dim); padding-bottom: 8px; margin-bottom: 15px; text-transform: uppercase; } + +/* ASCIIDOCTOR HTML STYLING */ +.node-body { line-height: 1.6; font-size: 1rem; margin-bottom: 20px; } +.node-body p { margin-bottom: 15px; } +.node-body strong, .node-body b { color: var(--term-cyan); font-weight: normal; text-shadow: 0 0 5px rgba(0, 255, 255, 0.4); } +.node-body em, .node-body i { color: var(--term-amber); font-style: italic; } +.node-body ul, .node-body ol { margin-left: 25px; margin-bottom: 15px; list-style-type: square; } +.node-body li { margin-bottom: 5px; } +.node-body h1, .node-body h2, .node-body h3, .node-body h4 { color: var(--term-cyan); margin: 20px 0 10px 0; border-bottom: 1px solid var(--term-dim); padding-bottom: 5px; } +.node-body code { background: rgba(0, 255, 102, 0.1); padding: 2px 5px; border-radius: 3px; font-family: var(--font-stack); } +.node-body pre { background: rgba(0, 20, 10, 0.8); border-left: 3px solid var(--term-dim); padding: 10px; overflow-x: auto; margin-bottom: 15px; } +.node-body table { width: 100%; border-collapse: collapse; margin-bottom: 15px; } +.node-body th, .node-body td { border: 1px solid var(--term-dim); padding: 8px; text-align: left; } +.node-body th { background: rgba(0, 255, 102, 0.1); color: var(--term-cyan); } + +.choices-container { display: flex; flex-direction: column; gap: 10px; margin-top: 15px; } +.choice-btn { + background: rgba(0, 30, 15, 0.6); border: 1px solid var(--term-green); + color: var(--term-green); padding: 12px 16px; font-family: var(--font-stack); + font-size: 0.95rem; text-align: left; cursor: pointer; + transition: all 0.15s ease; display: flex; align-items: center; gap: 10px; +} +.choice-btn:hover:not(:disabled) { background: var(--term-green); color: var(--bg-color); box-shadow: 0 0 10px var(--term-green); outline: none; } +.choice-btn:disabled { border-color: var(--term-warn); color: var(--term-warn); cursor: not-allowed; opacity: 0.6; } + +.choice-key { background: var(--term-dim); color: var(--term-green); padding: 2px 6px; font-size: 0.8rem; border-radius: 2px; } +.choice-btn:hover:not(:disabled) .choice-key { background: var(--bg-color); color: var(--term-green); } +.choice-btn:disabled .choice-key { background: rgba(255, 51, 51, 0.2); color: var(--term-warn); } + +footer { border-top: 1px solid var(--term-dim); padding: 10px 20px; display: flex; justify-content: space-between; background: #080f0c; font-size: 0.8rem; z-index: 2; } +.btn-small { background: transparent; border: 1px solid var(--term-dim); color: var(--term-green); padding: 4px 10px; cursor: pointer; font-family: var(--font-stack); } +.btn-small:hover { border-color: var(--term-cyan); color: var(--term-cyan); } + +#graph-modal { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(5, 11, 8, 0.95); z-index: 20; padding: 20px; flex-direction: column; } +#graph-canvas { flex: 1; border: 1px solid var(--term-green); background: #000; } \ No newline at end of file diff --git a/00_aMeta/toolz/CYOA/frontend/template.html b/00_aMeta/toolz/CYOA/frontend/template.html new file mode 100644 index 0000000..66ebe6e --- /dev/null +++ b/00_aMeta/toolz/CYOA/frontend/template.html @@ -0,0 +1,373 @@ + + + + + + Computer Historyz :: Interactive Terminal Engine + + + + +
+
[COMPUTER HISTORYZ :: INSTANZ_TERMINAL]
+
+
+ +
+
+
System Initialisierung...
+
Verbinde mit Datenstrom...
+
+
+
+ + + +
+
+ === INTERAKTIVER VORHERSAGE-GRAPH === + +
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/00_aMeta/toolz/CYOA/main.go b/00_aMeta/toolz/CYOA/main.go index 4a8c712..cf53217 100644 --- a/00_aMeta/toolz/CYOA/main.go +++ b/00_aMeta/toolz/CYOA/main.go @@ -22,7 +22,7 @@ type Choice struct { type StoryNode struct { ID string `json:"id"` Title string `json:"title"` - Content template.HTML `json:"content"` // HTML für das Asciidoctor Rendering + Content template.HTML `json:"content"` Choices []Choice `json:"choices"` IsReboot bool `json:"isReboot"` IsEnd bool `json:"isEnd"` @@ -38,6 +38,14 @@ type StoryGraph struct { DeadEndNodes []string `json:"deadEndNodes"` } +// Struct für das saubere Injezieren der Frontend-Komponenten +type TemplateContext struct { + Graph template.JS + CSS template.CSS + Logic template.JS + UI template.JS +} + var ( adocNodeRegex = regexp.MustCompile(`\[\[([a-zA-Z0-9_\-]+)\]\]`) adocChoiceRegex = regexp.MustCompile(`<<([a-zA-Z0-9_\-]+)\s*,\s*([^>]+)>>`) @@ -49,12 +57,12 @@ var ( func main() { dirPtr := flag.String("dir", ".", "Verzeichnis mit AsciiDoc/Markdown Dateien") outPtr := flag.String("out", "export/story.html", "Zielpfad für den Dateiexport") - tmplPtr := flag.String("tmpl", "template.html", "Pfad zur ausgelagerten HTML/JS Template-Datei") + tmplPtr := flag.String("tmpl", "template.html", "Pfad zur HTML Template-Datei") servePtr := flag.Bool("serve", false, "Startet den API- & Preview-Server") portPtr := flag.Int("port", 8080, "Port für den Server") flag.Parse() - fmt.Println("\033[1;36m=== Computer Historyz :: CYOA Engine (Asciidoctor Bindings) ===\033[0m") + fmt.Println("\033[1;36m=== Computer Historyz :: CYOA Engine (Modular) ===\033[0m") if *servePtr { startServer(*portPtr, *dirPtr, *outPtr, *tmplPtr) @@ -71,26 +79,26 @@ func main() { fmt.Printf("\033[1;31m[FEHLER]\033[0m %v\n", err) os.Exit(1) } - fmt.Printf("\033[1;32m[ERFOLG]\033[0m Story generiert -> \033[1;33m%s\033[0m\n", *outPtr) + fmt.Printf("\033[1;32m[ERFOLG]\033[0m Story modular generiert -> \033[1;33m%s\033[0m\n", *outPtr) } -// renderAsciidoctor piped rohen AsciiDoc Text in den lokalen Container-Prozess -func renderAsciidoctor(text string) template.HTML { - if strings.TrimSpace(text) == "" { - return "" +func readAsset(basePath, filename string) string { + b, err := os.ReadFile(filepath.Join(filepath.Dir(basePath), filename)) + if err != nil { + return fmt.Sprintf("/* %s nicht gefunden */", filename) } - + return string(b) +} + +func renderAsciidoctor(text string) template.HTML { + if strings.TrimSpace(text) == "" { return "" } cmd := exec.Command("asciidoctor", "--embedded", "-") cmd.Stdin = strings.NewReader(text) - out, err := cmd.Output() if err != nil { - fmt.Printf("\033[1;33m[WARNUNG]\033[0m Asciidoctor Rendering fehlgeschlagen: %v\n", err) - // Fallback auf rohen Text (mit rudimentären Linebreaks), falls asciidoctor fehlschlägt fallback := strings.ReplaceAll(text, "\n", "
") return template.HTML(fallback) } - return template.HTML(string(out)) } @@ -106,86 +114,45 @@ func parseContent(rawContent string, sourceName string, graph *StoryGraph) { var currentBody strings.Builder for _, line := range lines { - // Knotenpunkte identifizieren if matches := adocNodeRegex.FindStringSubmatch(line); len(matches) > 1 { if currentNode != nil { - // Inhalt über Asciidoctor-Container-Binary exportieren currentNode.Content = renderAsciidoctor(cleanBodyText(currentBody.String())) graph.Nodes[currentNode.ID] = currentNode currentBody.Reset() } nodeID := matches[1] - currentNode = &StoryNode{ - ID: nodeID, - Title: nodeID, - Choices: make([]Choice, 0), - FileSource: sourceName, - Metadata: make(map[string]string), - } - if graph.StartNodeID == "" || nodeID == "start" || nodeID == "node_start" { - graph.StartNodeID = nodeID - } + currentNode = &StoryNode{ ID: nodeID, Title: nodeID, Choices: make([]Choice, 0), FileSource: sourceName, Metadata: make(map[string]string) } + if graph.StartNodeID == "" || nodeID == "start" || nodeID == "node_start" { graph.StartNodeID = nodeID } continue } - if currentNode == nil { if strings.TrimSpace(line) != "" { baseID := "start" - if sourceName != "API_POST_Stream" { - baseID = strings.TrimSuffix(filepath.Base(sourceName), filepath.Ext(sourceName)) - } - currentNode = &StoryNode{ - ID: baseID, - Title: baseID, - Choices: make([]Choice, 0), - FileSource: sourceName, - Metadata: make(map[string]string), - } - if graph.StartNodeID == "" { - graph.StartNodeID = baseID - } - } else { - continue - } + if sourceName != "API_POST_Stream" { baseID = strings.TrimSuffix(filepath.Base(sourceName), filepath.Ext(sourceName)) } + currentNode = &StoryNode{ ID: baseID, Title: baseID, Choices: make([]Choice, 0), FileSource: sourceName, Metadata: make(map[string]string) } + if graph.StartNodeID == "" { graph.StartNodeID = baseID } + } else { continue } } - // Metadaten für das State-System extrahieren if mMatches := metaRegex.FindStringSubmatch(line); len(mMatches) > 2 { - currentNode.Metadata[strings.ToLower(mMatches[1])] = strings.TrimSpace(mMatches[2]) - continue + currentNode.Metadata[strings.ToLower(mMatches[1])] = strings.TrimSpace(mMatches[2]); continue } - - // Titel extrahieren if hMatches := headingRegex.FindStringSubmatch(line); len(hMatches) > 1 && currentNode.Title == currentNode.ID { currentNode.Title = strings.TrimSpace(hMatches[1]) } - - // Entscheidungen (AsciiDoc) if cMatches := adocChoiceRegex.FindAllStringSubmatch(line, -1); len(cMatches) > 0 { - for _, match := range cMatches { - currentNode.Choices = append(currentNode.Choices, Choice{TargetID: match[1], Text: strings.TrimSpace(match[2])}) - } + for _, match := range cMatches { currentNode.Choices = append(currentNode.Choices, Choice{TargetID: match[1], Text: strings.TrimSpace(match[2])}) } } - - // Entscheidungen (Markdown) if mMatches := mdChoiceRegex.FindAllStringSubmatch(line, -1); len(mMatches) > 0 { - for _, match := range mMatches { - currentNode.Choices = append(currentNode.Choices, Choice{TargetID: match[2], Text: strings.TrimSpace(match[1])}) - } + for _, match := range mMatches { currentNode.Choices = append(currentNode.Choices, Choice{TargetID: match[2], Text: strings.TrimSpace(match[1])}) } } - // Spezielle System-Tags prüfen lowerLine := strings.ToLower(line) - if strings.Contains(lowerLine, "[reboot]") || strings.Contains(lowerLine, "speicherabbild") { - currentNode.IsReboot = true - } - if strings.Contains(lowerLine, "[ende]") || strings.Contains(lowerLine, "[end]") { - currentNode.IsEnd = true - } + if strings.Contains(lowerLine, "[reboot]") || strings.Contains(lowerLine, "speicherabbild") { currentNode.IsReboot = true } + if strings.Contains(lowerLine, "[ende]") || strings.Contains(lowerLine, "[end]") { currentNode.IsEnd = true } currentBody.WriteString(line + "\n") } - if currentNode != nil { currentNode.Content = renderAsciidoctor(cleanBodyText(currentBody.String())) graph.Nodes[currentNode.ID] = currentNode @@ -199,10 +166,7 @@ func loadStoryGraphFromDir(rootDir string) *StoryGraph { ext := strings.ToLower(filepath.Ext(path)) if ext != ".adoc" && ext != ".md" { return nil } contentBytes, err := os.ReadFile(path) - if err == nil { - relPath, _ := filepath.Rel(rootDir, path) - parseContent(string(contentBytes), relPath, graph) - } + if err == nil { parseContent(string(contentBytes), filepath.Base(path), graph) } return nil }) return graph @@ -214,37 +178,30 @@ func validateGraph(g *StoryGraph) { for id, node := range g.Nodes { for _, choice := range node.Choices { referenced[choice.TargetID] = true - if _, exists := g.Nodes[choice.TargetID]; !exists { - g.BrokenLinks = append(g.BrokenLinks, fmt.Sprintf("%s -> %s (in %s)", id, choice.TargetID, node.FileSource)) - } - } - if len(node.Choices) == 0 && !node.IsReboot && !node.IsEnd { - g.DeadEndNodes = append(g.DeadEndNodes, id) + if _, exists := g.Nodes[choice.TargetID]; !exists { g.BrokenLinks = append(g.BrokenLinks, fmt.Sprintf("%s -> %s (in %s)", id, choice.TargetID, node.FileSource)) } } + if len(node.Choices) == 0 && !node.IsReboot && !node.IsEnd { g.DeadEndNodes = append(g.DeadEndNodes, id) } } - for id := range g.Nodes { - if !referenced[id] { g.OrphanNodes = append(g.OrphanNodes, id) } - } + for id := range g.Nodes { if !referenced[id] { g.OrphanNodes = append(g.OrphanNodes, id) } } } -// JSON Helper für das Template -func getTemplateFuncMap() template.FuncMap { - return template.FuncMap{ - "json": func(v interface{}) template.JS { - a, _ := json.Marshal(v) - return template.JS(a) - }, +func getContext(graph *StoryGraph, tmplPath string) TemplateContext { + graphJson, _ := json.Marshal(graph) + return TemplateContext{ + Graph: template.JS(graphJson), + CSS: template.CSS(readAsset(tmplPath, "style.css")), + Logic: template.JS(readAsset(tmplPath, "engine_logic.js")), + UI: template.JS(readAsset(tmplPath, "engine_ui.js")), } } func generateHTMLFile(g *StoryGraph, outPath string, tmplPath string) error { - fmt.Println("tmp path "+tmplPath) - tmpl, err := template.New(filepath.Base(tmplPath)).Funcs(getTemplateFuncMap()).ParseFiles(tmplPath) + tmpl, err := template.ParseFiles(tmplPath) if err != nil { return fmt.Errorf("Template-Fehler: %v", err) } f, err := os.Create(outPath) if err != nil { return err } defer f.Close() - return tmpl.Execute(f, g) + return tmpl.Execute(f, getContext(g, tmplPath)) } func startServer(port int, watchDir string, htmlPath string, tmplPath string) { @@ -256,33 +213,25 @@ func startServer(port int, watchDir string, htmlPath string, tmplPath string) { if r.Method == http.MethodPost { bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Daten defekt", http.StatusBadRequest) - return - } + if err != nil { http.Error(w, "Daten defekt", http.StatusBadRequest); return } graph := &StoryGraph{Nodes: make(map[string]*StoryNode)} - // Rohdaten durch den Parser und den Asciidoctor-Container schleusen parseContent(string(bodyBytes), "API_POST_Stream", graph) validateGraph(graph) w.Header().Set("Content-Type", "text/html; charset=utf-8") - tmpl, err := template.New(filepath.Base(tmplPath)).Funcs(getTemplateFuncMap()).ParseFiles(tmplPath) - if err != nil { - http.Error(w, "Template-Fehler: "+err.Error(), http.StatusInternalServerError) - return - } - tmpl.Execute(w, graph) - fmt.Println("\033[1;34m[POST-API]\033[0m Story inkl. Asciidoctor-HTML gerendert.") + tmpl, err := template.ParseFiles(tmplPath) + if err != nil { http.Error(w, "Template-Fehler: "+err.Error(), http.StatusInternalServerError); return } + + tmpl.Execute(w, getContext(graph, tmplPath)) + fmt.Println("\033[1;34m[POST-API]\033[0m Modularer Stack gerendert.") return } - if r.Method == http.MethodGet { freshGraph := loadStoryGraphFromDir(watchDir) _ = generateHTMLFile(freshGraph, htmlPath, tmplPath) http.ServeFile(w, r, htmlPath) } }) - http.ListenAndServe(addr, nil) -} +} \ No newline at end of file diff --git a/00_aMeta/toolz/CYOA/templates/template.html b/00_aMeta/toolz/CYOA/templates/template.html deleted file mode 100644 index 614438b..0000000 --- a/00_aMeta/toolz/CYOA/templates/template.html +++ /dev/null @@ -1,336 +0,0 @@ - - - - - - Computer Historyz :: Interactive Terminal Engine - - - - -
-
[COMPUTER HISTORYZ :: INSTANZ_TERMINAL]
-
- -
-
- -
-
-
System Initialisierung...
-
Verbinde mit Datenstrom...
-
-
-
- - - -
-
- === INTERAKTIVER VORHERSAGE-GRAPH === - -
- -
- - - - - - -