package web import ( "bytes" "embed" "html/template" "io/fs" "net/http" "path" "strings" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting/v2" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer/html" ) //go:embed docs/* var docsFS embed.FS // DocsHandler serves documentation pages type DocsHandler struct { md goldmark.Markdown template *template.Template docs fs.FS } // NewDocsHandler creates a new documentation handler func NewDocsHandler() (*DocsHandler, error) { // Configure goldmark with extensions md := goldmark.New( goldmark.WithExtensions( extension.GFM, // GitHub Flavored Markdown extension.Table, extension.Strikethrough, extension.TaskList, highlighting.NewHighlighting( highlighting.WithStyle("monokai"), ), ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions( html.WithUnsafe(), // Allow raw HTML ), ) // Create the page template tmpl, err := template.New("doc").Parse(docPageTemplate) if err != nil { return nil, err } // Get embedded docs filesystem docs, err := fs.Sub(docsFS, "docs") if err != nil { return nil, err } return &DocsHandler{ md: md, template: tmpl, docs: docs, }, nil } // ServeHTTP handles documentation requests func (h *DocsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Get the requested path docPath := strings.TrimPrefix(r.URL.Path, "/docs") if docPath == "" || docPath == "/" { docPath = "/index" } // Clean the path and add .md extension docPath = path.Clean(docPath) if !strings.HasSuffix(docPath, ".md") { docPath = docPath + ".md" } docPath = strings.TrimPrefix(docPath, "/") // Read the markdown file content, err := fs.ReadFile(h.docs, docPath) if err != nil { // Try index.md in directory if !strings.HasSuffix(docPath, "/index.md") { dirPath := strings.TrimSuffix(docPath, ".md") + "/index.md" content, err = fs.ReadFile(h.docs, dirPath) } if err != nil { http.NotFound(w, r) return } } // Extract title from first heading title := extractTitle(content) if title == "" { title = "Documentation" } // Convert markdown to HTML var buf bytes.Buffer if err := h.md.Convert(content, &buf); err != nil { http.Error(w, "Failed to render documentation", http.StatusInternalServerError) return } // Build navigation nav := h.buildNavigation(docPath) // Render page data := docPageData{ Title: title, Content: template.HTML(buf.String()), Navigation: nav, CurrentPath: "/" + strings.TrimSuffix(docPath, ".md"), } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := h.template.Execute(w, data); err != nil { http.Error(w, "Template error", http.StatusInternalServerError) } } type docPageData struct { Title string Content template.HTML Navigation []navSection CurrentPath string } type navSection struct { Title string Items []navItem } type navItem struct { Title string Path string Active bool } // buildNavigation creates the documentation navigation structure func (h *DocsHandler) buildNavigation(currentPath string) []navSection { currentPath = "/" + strings.TrimSuffix(currentPath, ".md") return []navSection{ { Title: "Getting Started", Items: []navItem{ {Title: "Introduction", Path: "/docs", Active: currentPath == "/index"}, {Title: "Quick Start", Path: "/docs/getting-started", Active: currentPath == "/getting-started"}, {Title: "FAQ", Path: "/docs/faq", Active: currentPath == "/faq"}, }, }, { Title: "Guides", Items: []navItem{ {Title: "UI Design", Path: "/docs/guides/ui-design", Active: currentPath == "/guides/ui-design"}, {Title: "Lua Scripting", Path: "/docs/guides/lua-scripting", Active: currentPath == "/guides/lua-scripting"}, {Title: "Permissions", Path: "/docs/guides/permissions", Active: currentPath == "/guides/permissions"}, {Title: "Best Practices", Path: "/docs/guides/best-practices", Active: currentPath == "/guides/best-practices"}, }, }, { Title: "Reference", Items: []navItem{ {Title: "Lua API", Path: "/docs/api/lua-api", Active: currentPath == "/api/lua-api"}, {Title: "Manifest", Path: "/docs/api/manifest", Active: currentPath == "/api/manifest"}, {Title: "CLI", Path: "/docs/cli", Active: currentPath == "/cli"}, }, }, { Title: "Help", Items: []navItem{ {Title: "Troubleshooting", Path: "/docs/troubleshooting", Active: currentPath == "/troubleshooting"}, }, }, } } // extractTitle extracts the first H1 heading from markdown func extractTitle(content []byte) string { lines := bytes.Split(content, []byte("\n")) for _, line := range lines { line = bytes.TrimSpace(line) if bytes.HasPrefix(line, []byte("# ")) { return string(bytes.TrimPrefix(line, []byte("# "))) } } return "" } // docPageTemplate is the HTML template for documentation pages const docPageTemplate = ` {{.Title}} - Mosis Docs
Mosis Docs
{{.Content}}
`