package handlers import ( "net/http" "os" "path/filepath" "regexp" "strings" "github.com/gin-gonic/gin" ) // DocsHandler serves documentation files. type DocsHandler struct { docsDir string } // NewDocsHandler creates a new DocsHandler. func NewDocsHandler(docsDir string) *DocsHandler { return &DocsHandler{docsDir: docsDir} } // docEntry describes a documentation file. type docEntry struct { Slug string `json:"slug"` Title string `json:"title"` Description string `json:"description,omitempty"` Category string `json:"category,omitempty"` } // docsOrder defines the display order and metadata for documentation files. var docsOrder = []docEntry{ // Getting Started {Slug: "GETTING-STARTED", Title: "Getting Started", Description: "Quick start guide", Category: "Getting Started"}, {Slug: "TUTORIAL-FIRST-EXTENSION", Title: "Tutorial: First Extension", Description: "Step-by-step extension walkthrough", Category: "Getting Started"}, // Platform {Slug: "ARCHITECTURE", Title: "Architecture", Description: "Kernel design and components", Category: "Platform"}, {Slug: "PERMISSIONS-AND-GROUPS", Title: "Permissions & Groups", Description: "RBAC model and settings cascade", Category: "Platform"}, {Slug: "WORKFLOWS", Title: "Workflows", Description: "Multi-stage processes with team validation", Category: "Platform"}, {Slug: "DEPLOYMENT", Title: "Deployment", Description: "Production deployment guide", Category: "Platform"}, // Extension Development {Slug: "EXTENSION-GUIDE", Title: "Extension Guide", Description: "How to author extensions", Category: "Extension Development"}, {Slug: "STARLARK-REFERENCE", Title: "Starlark Reference", Description: "Sandbox scripting API", Category: "Extension Development"}, {Slug: "FRONTEND-JS-GUIDE", Title: "Frontend JS Guide", Description: "Preact SDK and shell contract", Category: "Extension Development"}, {Slug: "EXTENSION-CSS", Title: "Extension CSS", Description: "CSS isolation and primitives", Category: "Extension Development"}, {Slug: "PACKAGE-FORMAT", Title: "Package Format", Description: "The .pkg archive format", Category: "Extension Development"}, // Operations {Slug: "API-REFERENCE", Title: "API Reference", Description: "REST API overview", Category: "Operations"}, {Slug: "DISTRIBUTION", Title: "Distribution", Description: "Docker image and bundled packages", Category: "Operations"}, {Slug: "PACKAGE-REGISTRY", Title: "Package Registry", Description: "Registry API and operations", Category: "Operations"}, } // ListDocs returns the list of available documentation files. // GET /api/v1/docs func (h *DocsHandler) ListDocs(c *gin.Context) { if h.docsDir == "" { c.JSON(http.StatusOK, gin.H{"data": []docEntry{}}) return } var available []docEntry for _, d := range docsOrder { path := filepath.Join(h.docsDir, d.Slug+".md") if _, err := os.Stat(path); err == nil { available = append(available, d) } } // Also add any .md files not in the ordered list entries, err := os.ReadDir(h.docsDir) if err == nil { known := make(map[string]bool) for _, d := range docsOrder { known[d.Slug] = true } for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") { continue } slug := strings.TrimSuffix(e.Name(), ".md") if known[slug] { continue } // Skip design docs and other non-user-facing files if strings.HasPrefix(slug, "DESIGN-") || strings.HasPrefix(slug, "DEMO-") || strings.HasPrefix(slug, "AUDIT-") || strings.HasPrefix(slug, "USABILITY-") { continue } available = append(available, docEntry{ Slug: slug, Title: slugToTitle(slug), }) } } if available == nil { available = []docEntry{} } c.JSON(http.StatusOK, gin.H{"data": available}) } // GetDoc returns the raw markdown content of a documentation file. // GET /api/v1/docs/:name func (h *DocsHandler) GetDoc(c *gin.Context) { name := c.Param("name") if !isValidDocName(name) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid document name"}) return } if h.docsDir == "" { c.JSON(http.StatusNotFound, gin.H{"error": "docs not configured"}) return } path := filepath.Join(h.docsDir, name+".md") data, err := os.ReadFile(path) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "document not found"}) return } c.JSON(http.StatusOK, gin.H{ "data": gin.H{ "slug": name, "title": slugToTitle(name), "content": string(data), }, }) } var validDocNameRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) func isValidDocName(name string) bool { return validDocNameRe.MatchString(name) && !strings.Contains(name, "..") } func slugToTitle(slug string) string { // Convert "GETTING-STARTED" → "Getting Started" parts := strings.Split(slug, "-") for i, p := range parts { if len(p) > 0 { parts[i] = strings.ToUpper(p[:1]) + strings.ToLower(p[1:]) } } return strings.Join(parts, " ") }