This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/docs.go
Jeffrey Smith a887b4c78b
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m46s
CI/CD / test-sqlite (push) Successful in 2m49s
CI/CD / build-and-deploy (push) Successful in 28s
V0.6.2 docs openapi (#37)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 12:01:51 +00:00

135 lines
3.5 KiB
Go

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"`
}
// docsOrder defines the display order and metadata for documentation files.
var docsOrder = []docEntry{
{Slug: "GETTING-STARTED", Title: "Getting Started", Description: "Quick start guide"},
{Slug: "ARCHITECTURE", Title: "Architecture", Description: "Kernel design and components"},
{Slug: "EXTENSION-GUIDE", Title: "Extension Guide", Description: "How to author extensions"},
{Slug: "PACKAGE-FORMAT", Title: "Package Format", Description: "The .pkg archive format"},
{Slug: "API-REFERENCE", Title: "API Reference", Description: "REST API overview"},
{Slug: "DEPLOYMENT", Title: "Deployment", Description: "Production deployment guide"},
{Slug: "DISTRIBUTION", Title: "Distribution", Description: "Docker image and bundled packages"},
}
// 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-") {
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, " ")
}