package main import ( "encoding/json" "flag" "fmt" "io" "net/http" "os" "path/filepath" "strings" ) func main() { projDir := flag.String("project", "project", "path to project directory") portInt := flag.Int("port", 8080, "port to run on") flag.Parse() absProjectDir, err := filepath.Abs(*projDir) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: unable to resolve project directory: %v\n", err) os.Exit(1) } info, statErr := os.Stat(absProjectDir) if statErr != nil || !info.IsDir() { fmt.Fprintf(os.Stderr, "ERROR: project directory %q does not exist\n", *projDir) os.Exit(1) } port := fmt.Sprintf(":%d", uint16(*portInt)) mux := http.NewServeMux() mux.HandleFunc("/", handler(absProjectDir)) fmt.Printf("Listening on http://localhost%s\n", port) http.ListenAndServe(port, mux) } // isHidden checks if any path component starts with an underscore func isHidden(path string) bool { for _, part := range strings.Split(strings.Trim(path, "/"), "/") { if strings.HasPrefix(part, "_") { return true } } return false } // handler determines request type based on file extension and delegates accordingly func handler(projectDir string) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path // Block access to underscore-prefixed files/directories if isHidden(path) { http.NotFound(w, r) return } projectPath := filepath.Join(projectDir, path) // Root path serves index.jinja2 if path == "/" { indexPath := filepath.Join(projectDir, "index.jinja2") if fileExists(indexPath) { renderTemplate(w, r, indexPath, projectDir) return } } // Check for .jinja2 template if strings.HasSuffix(path, ".jinja2") { renderTemplate(w, r, projectPath, projectDir) return } // Check for .lua script if strings.HasSuffix(path, ".lua") { executeLua(w, r, projectPath, projectDir) return } // Check for .md Markdown file if strings.HasSuffix(path, ".md") { renderMarkdown(w, r, projectPath, projectDir) return } // Try exact match first, then check for .jinja2 fallback, then serve static if fileExists(projectPath) { serveStatic(w, r, projectPath) return } // Check if URL maps to a .jinja2 template (e.g., /contact -> /project/contact.jinja2) templatePath := projectPath + ".jinja2" if fileExists(templatePath) { renderTemplate(w, r, templatePath, projectDir) return } // Check if URL maps to a .lua script (e.g., /contact/submit -> /project/contact/submit.lua) luaPath := projectPath + ".lua" if fileExists(luaPath) { executeLua(w, r, luaPath, projectDir) return } // Check if URL maps to a .md file (e.g., /about -> /project/about.md) mdPath := projectPath + ".md" if fileExists(mdPath) { renderMarkdown(w, r, mdPath, projectDir) return } http.NotFound(w, r) } } // fileExists checks if a file exists at the given path func fileExists(path string) bool { info, err := os.Stat(path) return err == nil && !info.IsDir() } // serveStatic serves a static file with appropriate content type func serveStatic(w http.ResponseWriter, r *http.Request, path string) { // Clean path to prevent directory traversal cleanPath := filepath.Clean(path) if strings.Contains(cleanPath, "..") { http.Error(w, "Forbidden", http.StatusForbidden) return } ext := strings.ToLower(filepath.Ext(path)) contentType := "application/octet-stream" switch ext { case ".html": contentType = "text/html; charset=utf-8" case ".css": contentType = "text/css; charset=utf-8" case ".js": contentType = "application/javascript; charset=utf-8" case ".json": contentType = "application/json; charset=utf-8" case ".txt": contentType = "text/plain; charset=utf-8" case ".png": contentType = "image/png" case ".jpg", ".jpeg": contentType = "image/jpeg" case ".gif": contentType = "image/gif" case ".svg": contentType = "image/svg+xml" case ".ico": contentType = "image/x-icon" case ".pdf": contentType = "application/pdf" } w.Header().Set("Content-Type", contentType) http.ServeFile(w, r, path) } // renderTemplate renders a Jinja2 template using Gonja func renderTemplate(w http.ResponseWriter, r *http.Request, templatePath string, projectDir string) { content, err := os.ReadFile(templatePath) if err != nil { http.Error(w, "Template not found", http.StatusNotFound) return } // Check for companion data file: e.g., index.jinja2 -> index_data.lua dataFile := strings.TrimSuffix(templatePath, ".jinja2") + "_data.lua" extraData := loadTemplateData(dataFile, r, projectDir) html, err := renderGonja(string(content), templatePath, r, extraData) if err != nil { http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) } // loadTemplateData runs a companion Lua data file and returns its output as a context map func loadTemplateData(dataPath string, r *http.Request, projectDir string) map[string]any { if !fileExists(dataPath) { return nil } result, err := runLuaScript(dataPath, r, projectDir) if err != nil { return nil } if result.Body == "" { return nil } var data map[string]any if err := json.Unmarshal([]byte(result.Body), &data); err != nil { return nil } return data } // executeLua executes a Lua script and returns the result func executeLua(w http.ResponseWriter, r *http.Request, luaPath, projectDir string) { result, err := runLuaScript(luaPath, r, projectDir) if err != nil { http.Error(w, fmt.Sprintf("Script error: %v", err), http.StatusInternalServerError) return } // If the Lua handler returned a table with a "template" key, render that template if result.TemplateName != "" { templatePath := filepath.Join(projectDir, filepath.Clean(result.TemplateName)) if !fileExists(templatePath) { http.Error(w, fmt.Sprintf("Template not found: %s", result.TemplateName), http.StatusNotFound) return } content, err := os.ReadFile(templatePath) if err != nil { http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError) return } html, err := renderGonja(string(content), templatePath, r, result.TemplateContext) if err != nil { http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError) return } for key, value := range result.Headers { w.Header().Set(key, value) } w.WriteHeader(result.StatusCode) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) return } // Write result to response for key, value := range result.Headers { w.Header().Set(key, value) } if result.ContentType != "" { w.Header().Set("Content-Type", result.ContentType) } w.WriteHeader(result.StatusCode) io.WriteString(w, result.Body) }