From 3eae906b340578379b7c148cfa9e51b5fd67fa15 Mon Sep 17 00:00:00 2001 From: dexta Date: Mon, 27 Jul 2026 20:09:31 +0200 Subject: [PATCH] go to container --- 00_aMeta/toolz/CYOA/main.go | 864 +++++--------------- 00_aMeta/toolz/CYOA/templates/template.html | 336 ++++++++ 2 files changed, 548 insertions(+), 652 deletions(-) create mode 100644 00_aMeta/toolz/CYOA/templates/template.html diff --git a/00_aMeta/toolz/CYOA/main.go b/00_aMeta/toolz/CYOA/main.go index 310b817..7d26ed8 100644 --- a/00_aMeta/toolz/CYOA/main.go +++ b/00_aMeta/toolz/CYOA/main.go @@ -1,10 +1,11 @@ package main import ( + "encoding/json" "flag" "fmt" - "html" "html/template" + "io" "net/http" "os" "os/exec" @@ -13,24 +14,22 @@ import ( "strings" ) -// Choice beschreibt eine Entscheidungsmöglichkeit zu einem Ziel-Knoten type Choice struct { TargetID string `json:"targetId"` Text string `json:"text"` } -// StoryNode repräsentiert einen einzelnen Knotenpunkt in der Geschichte type StoryNode struct { - ID string `json:"id"` - Title string `json:"title"` - Content string `json:"content"` - Choices []Choice `json:"choices"` - IsReboot bool `json:"isReboot"` - IsEnd bool `json:"isEnd"` - FileSource string `json:"fileSource"` + ID string `json:"id"` + Title string `json:"title"` + Content template.HTML `json:"content"` // HTML für das Asciidoctor Rendering + Choices []Choice `json:"choices"` + IsReboot bool `json:"isReboot"` + IsEnd bool `json:"isEnd"` + FileSource string `json:"fileSource"` + Metadata map[string]string `json:"metadata"` } -// StoryGraph hält alle Knoten und Analyse-Daten type StoryGraph struct { Nodes map[string]*StoryNode `json:"nodes"` StartNodeID string `json:"startNodeId"` @@ -39,689 +38,250 @@ type StoryGraph struct { DeadEndNodes []string `json:"deadEndNodes"` } +var ( + adocNodeRegex = regexp.MustCompile(`\[\[([a-zA-Z0-9_\-]+)\]\]`) + adocChoiceRegex = regexp.MustCompile(`<<([a-zA-Z0-9_\-]+)\s*,\s*([^>]+)>>`) + mdChoiceRegex = regexp.MustCompile(`\[([^\]]+)\]\((?:#)?([a-zA-Z0-9_\-]+)\)`) + headingRegex = regexp.MustCompile(`^(?:={1,4}|#{1,4})\s+(.+)`) + metaRegex = regexp.MustCompile(`(?i)\[meta:\s*([a-zA-Z0-9_]+)\s*=\s*([^\]]+)\]`) +) + func main() { - dirPtr := flag.String("dir", ".", "Verzeichnis mit AsciiDoc oder Markdown Story-Dateien") - outPtr := flag.String("out", "export/story.html", "Zielpfad für die generierte HTML-Datei") - servePtr := flag.Bool("serve", false, "Startet einen lokalen Preview-Webserver auf -port") - portPtr := flag.Int("port", 8080, "Port für den Preview-Webserver") + 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") + 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 & Generator ===\033[0m") - - graph := loadStoryGraph(*dirPtr) - validateGraph(graph) - - // Export-Verzeichnis anlegen falls nicht vorhanden - outDir := filepath.Dir(*outPtr) - if err := os.MkdirAll(outDir, 0755); err != nil { - fmt.Printf("\033[1;31m[FEHLER]\033[0m Kann Ordner %s nicht erstellen: %v\n", outDir, err) - os.Exit(1) - } - - // Single-File HTML generieren - err := generateHTML(graph, *outPtr) - if err != nil { - fmt.Printf("\033[1;31m[FEHLER]\033[0m Fehler beim Schreiben der HTML-Datei: %v\n", err) - os.Exit(1) - } - - fmt.Printf("\033[1;32m[ERFOLG]\033[0m Interaktive Story generiert -> \033[1;33m%s\033[0m\n", *outPtr) + fmt.Println("\033[1;36m=== Computer Historyz :: CYOA Engine (Asciidoctor Bindings) ===\033[0m") if *servePtr { - startServer(graph, *outPtr, *portPtr, *dirPtr) + startServer(*portPtr, *dirPtr, *outPtr, *tmplPtr) + return + } + + graph := loadStoryGraphFromDir(*dirPtr) + validateGraph(graph) + + outDir := filepath.Dir(*outPtr) + os.MkdirAll(outDir, 0755) + + if err := generateHTMLFile(graph, *outPtr, *tmplPtr); err != nil { + 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) +} + +// renderAsciidoctor piped rohen AsciiDoc Text in den lokalen Container-Prozess +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)) +} + +func cleanBodyText(raw string) string { + cleaned := adocChoiceRegex.ReplaceAllString(raw, "") + cleaned = mdChoiceRegex.ReplaceAllString(cleaned, "") + return strings.TrimSpace(cleaned) +} + +func parseContent(rawContent string, sourceName string, graph *StoryGraph) { + lines := strings.Split(rawContent, "\n") + var currentNode *StoryNode = nil + 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 + } + 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 + } + } + + // 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 + } + + // 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])}) + } + } + + // 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])}) + } + } + + // 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 + } + + currentBody.WriteString(line + "\n") + } + + if currentNode != nil { + currentNode.Content = renderAsciidoctor(cleanBodyText(currentBody.String())) + graph.Nodes[currentNode.ID] = currentNode } } -// loadStoryGraph scannt das Verzeichnis nach .adoc und .md Dateien -func loadStoryGraph(rootDir string) *StoryGraph { - graph := &StoryGraph{ - Nodes: make(map[string]*StoryNode), - } - - // Regex-Muster für Knoten und Links - adocNodeRegex := regexp.MustCompile(`\[\[([a-zA-Z0-9_\-]+)\]\]`) - adocChoiceRegex := regexp.MustCompile(`<<([a-zA-Z0-9_\-]+)\s*,\s*([^>]+)>>`) - mdChoiceRegex := regexp.MustCompile(`\[([^\]]+)\]\((?:#)?([a-zA-Z0-9_\-]+)\)`) - headingRegex := regexp.MustCompile(`^(?:={1,4}|#{1,4})\s+(.+)`) - - err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() { - return nil - } - +func loadStoryGraphFromDir(rootDir string) *StoryGraph { + graph := &StoryGraph{Nodes: make(map[string]*StoryNode)} + filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { return nil } ext := strings.ToLower(filepath.Ext(path)) - if ext != ".adoc" && ext != ".md" { - return nil - } - + if ext != ".adoc" && ext != ".md" { return nil } contentBytes, err := os.ReadFile(path) - if err != nil { - return nil + if err == nil { + relPath, _ := filepath.Rel(rootDir, path) + parseContent(string(contentBytes), relPath, graph) } - - rawContent := string(contentBytes) - relPath, _ := filepath.Rel(rootDir, path) - - // Dateien anhand von [[node_id]]-Blöcken aufteilen - lines := strings.Split(rawContent, "\n") - var currentNode *StoryNode = nil - var currentBody strings.Builder - - for _, line := range lines { - // Prüfung auf neuen Knoten-Anker [[id]] - if matches := adocNodeRegex.FindStringSubmatch(line); len(matches) > 1 { - if currentNode != nil { - currentNode.Content = renderWithEchoBox(currentBody.String()) - graph.Nodes[currentNode.ID] = currentNode - currentBody.Reset() - } - - nodeID := matches[1] - currentNode = &StoryNode{ - ID: nodeID, - Title: nodeID, - Choices: make([]Choice, 0), - FileSource: relPath, - } - if graph.StartNodeID == "" || nodeID == "start" || nodeID == "node_start" { - graph.StartNodeID = nodeID - } - continue - } - - if currentNode == nil { - // Fallback: Erste Zeile oder Datei-Header als Standard-Start-Knoten verwenden - if strings.TrimSpace(line) != "" { - baseID := strings.TrimSuffix(filepath.Base(path), ext) - currentNode = &StoryNode{ - ID: baseID, - Title: baseID, - Choices: make([]Choice, 0), - FileSource: relPath, - } - if graph.StartNodeID == "" { - graph.StartNodeID = baseID - } - } else { - continue - } - } - - // Titel extrahieren - if hMatches := headingRegex.FindStringSubmatch(line); len(hMatches) > 1 && currentNode.Title == currentNode.ID { - currentNode.Title = strings.TrimSpace(hMatches[1]) - } - - // AsciiDoc Verzweigung finden: <> - 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]), - }) - } - } - - // Markdown Verzweigung finden: [Optionstext](#ziel_id) - 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]), - }) - } - } - - // Status-Kennzeichnungen erkennen - 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 - } - - currentBody.WriteString(line + "\n") - } - - if currentNode != nil { - currentNode.Content = renderWithEchoBox(currentBody.String()) - graph.Nodes[currentNode.ID] = currentNode - } - return nil }) - - if err != nil { - fmt.Printf("\033[1;31m[WARNUNG]\033[0m Fehler beim Durchsuchen der Dateien: %v\n", err) - } - return graph } -// renderWithEchoBox entfernt Syntax-Rauschen und nutzt den Debian 13 Docker Container als HTML Echo Box -func renderWithEchoBox(raw string) string { - fmt.Println("render with docker") - // Links entfernen (Diese sollen als Buttons, nicht als Text-Wirrwarr auftauchen) - adocChoiceRegex := regexp.MustCompile(`<<[a-zA-Z0-9_\-]+\s*,\s*([^>]+)>>`) - cleaned := adocChoiceRegex.ReplaceAllString(raw, "") - - mdChoiceRegex := regexp.MustCompile(`\[([^\]]+)\]\((?:#)?[a-zA-Z0-9_\-]+\)`) - cleaned = mdChoiceRegex.ReplaceAllString(cleaned, "") - - // Docker Echo Box aufrufen (Debian 13 Asciidoctor) - cmd := exec.Command("docker", "run", "--rm", "-i", "asciidoctor-echo") - cmd.Stdin = strings.NewReader(strings.TrimSpace(cleaned)) - - out, err := cmd.Output() - if err != nil { - // Fallback bei Docker-Fehler (z.B. wenn der Daemon nicht läuft) - fmt.Printf("\033[1;31m[DOCKER FEHLER]\033[0m Konnte Echo Box nicht nutzen. Fallback auf RAW Text.\n") - return html.EscapeString(strings.TrimSpace(cleaned)) - } - - return string(out) -} - -// validateGraph prüft Verzweigungen auf Konsistenz func validateGraph(g *StoryGraph) { - fmt.Printf("Gefundene Knotenpunkte: \033[1;32m%d\033[0m | Start-Knoten: \033[1;33m%s\033[0m\n", len(g.Nodes), g.StartNodeID) - referenced := make(map[string]bool) - if g.StartNodeID != "" { - referenced[g.StartNodeID] = true - } - + if g.StartNodeID != "" { referenced[g.StartNodeID] = true } for id, node := range g.Nodes { - // Tote Links prüfen 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)) } } - - // Sackgassen ohne Choices & ohne End-Markierung if len(node.Choices) == 0 && !node.IsReboot && !node.IsEnd { g.DeadEndNodes = append(g.DeadEndNodes, id) } } - - // Waisen-Knoten ermitteln for id := range g.Nodes { - if !referenced[id] { - g.OrphanNodes = append(g.OrphanNodes, id) - } - } - - // Konsolen-Bericht - if len(g.BrokenLinks) > 0 { - fmt.Println("\n\033[1;31m[!] DEFEKTE ZIEL-LINKS (Fehlende Knoten):\033[0m") - for _, link := range g.BrokenLinks { - fmt.Printf(" - %s\n", link) - } - } - - if len(g.OrphanNodes) > 0 { - fmt.Println("\n\033[1;33m[?] UNERREICHBARE KNOTEN (Waisen):\033[0m") - for _, orphan := range g.OrphanNodes { - fmt.Printf(" - %s\n", orphan) - } - } - - if len(g.DeadEndNodes) > 0 { - fmt.Println("\n\033[1;35m[*] SACKGASSEN OHNE OPTIONEN:\033[0m") - for _, dead := range g.DeadEndNodes { - fmt.Printf(" - %s\n", dead) - } - } - - if len(g.BrokenLinks) == 0 && len(g.OrphanNodes) == 0 && len(g.DeadEndNodes) == 0 { - fmt.Println("\033[1;32m[✓] Graph-Check bestanden: Alle Verzweigungen sind konsistent!\033[0m") + if !referenced[id] { g.OrphanNodes = append(g.OrphanNodes, id) } } } -// generateHTML erzeugt das eigenständige HTML5 CRT-Terminal Schreibdeck -func generateHTML(g *StoryGraph, outPath string) error { - tmpl, err := template.New("story").Parse(htmlTemplate) - if err != nil { - return err +// 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 generateHTMLFile(g *StoryGraph, outPath string, tmplPath string) error { + tmpl, err := template.New(filepath.Base(tmplPath)).Funcs(getTemplateFuncMap()).ParseFiles(tmplPath) + if err != nil { return fmt.Errorf("Template-Fehler: %v", err) } f, err := os.Create(outPath) - if err != nil { - return err - } + if err != nil { return err } defer f.Close() - return tmpl.Execute(f, g) } -// startServer führt den Live-Preview-Webserver aus -func startServer(g *StoryGraph, htmlPath string, port int, watchDir string) { +func startServer(port int, watchDir string, htmlPath string, tmplPath string) { addr := fmt.Sprintf(":%d", port) - fmt.Printf("\n\033[1;34m[SERVER]\033[0m Live Terminal Web-Engine läuft unter: \033[1;32mhttp://localhost:%d\033[0m\n", port) - fmt.Println("Drücke Strg+C zum Beenden.") + fmt.Printf("\n\033[1;32m[CONTAINER-API LÄUFT]\033[0m Port: %d | Nutzt Template: %s\n", port, tmplPath) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // Bei jedem Aufruf Graph neu laden für Hot-Reload Feeling - freshGraph := loadStoryGraph(watchDir) - _ = generateHTML(freshGraph, htmlPath) - http.ServeFile(w, r, htmlPath) + w.Header().Set("Access-Control-Allow-Origin", "*") + + if r.Method == http.MethodPost { + bodyBytes, err := io.ReadAll(r.Body) + 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.") + return + } + + if r.Method == http.MethodGet { + freshGraph := loadStoryGraphFromDir(watchDir) + _ = generateHTMLFile(freshGraph, htmlPath, tmplPath) + http.ServeFile(w, r, htmlPath) + } }) - err := http.ListenAndServe(addr, nil) - if err != nil { - fmt.Printf("Server-Fehler: %v\n", err) - } -} - -// Integrierte Retro-CRT Engine Templates (HTML, CSS, JS) -const htmlTemplate = ` - - - - - Computer Historyz :: Interactive Terminal Engine - - - - -
-
[COMPUTER HISTORYZ :: INSTANZ_TERMINAL]
-
-
SPEICHERABBILD: #001
-
HYDRAULIK: 100%
-
KNOTEN: -
-
-
- -
-
-
System Initialisierung...
-
Verbinde mit Datenstrom...
-
-
-
- -
-
[HOTKEYS: 1-9 Auswahl | R Reboot | G Graph]
-
- - -
-
- -
-
- === INTERAKTIVER VORHERSAGE-GRAPH === - -
- -
- - - -` \ No newline at end of file + 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 new file mode 100644 index 0000000..cfad7ea --- /dev/null +++ b/00_aMeta/toolz/CYOA/templates/template.html @@ -0,0 +1,336 @@ + + + + + + Computer Historyz :: Interactive Terminal Engine + + + + +
+
[COMPUTER HISTORYZ :: INSTANZ_TERMINAL]
+
+ +
+
+ +
+
+
System Initialisierung...
+
Verbinde mit Datenstrom...
+
+
+
+ +
+
[HOTKEYS: 1-9 Auswahl | R Reboot | G Graph]
+
+ + +
+
+ +
+
+ === INTERAKTIVER VORHERSAGE-GRAPH === + +
+ +
+ + + + + + + \ No newline at end of file