Files
MosisService/portal/internal/web/docs.go

305 lines
10 KiB
Go

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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} - Mosis Docs</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Code highlighting */
pre {
background-color: #1e1e2e;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 16px 0;
}
code {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 14px;
}
:not(pre) > code {
background-color: #1e1e2e;
padding: 2px 6px;
border-radius: 4px;
color: #f8f8f2;
}
/* Typography */
.prose h1 { font-size: 2rem; font-weight: 700; margin-top: 2rem; margin-bottom: 1rem; color: #f8fafc; }
.prose h2 { font-size: 1.5rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; color: #f8fafc; border-bottom: 1px solid #334155; padding-bottom: 0.5rem; }
.prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #f8fafc; }
.prose h4 { font-size: 1rem; font-weight: 600; margin-top: 1rem; margin-bottom: 0.5rem; color: #f8fafc; }
.prose p { margin-bottom: 1rem; line-height: 1.7; }
.prose a { color: #38bdf8; text-decoration: none; }
.prose a:hover { text-decoration: underline; }
.prose ul, .prose ol { margin-bottom: 1rem; padding-left: 1.5rem; }
.prose li { margin-bottom: 0.5rem; }
.prose ul { list-style-type: disc; }
.prose ol { list-style-type: decimal; }
.prose table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.prose th, .prose td { border: 1px solid #334155; padding: 8px 12px; text-align: left; }
.prose th { background-color: #1e293b; font-weight: 600; }
.prose blockquote { border-left: 4px solid #38bdf8; padding-left: 1rem; margin: 1rem 0; color: #94a3b8; }
.prose hr { border: none; border-top: 1px solid #334155; margin: 2rem 0; }
.prose strong { color: #f8fafc; }
/* Task lists */
.prose input[type="checkbox"] { margin-right: 8px; }
</style>
</head>
<body class="bg-slate-900 text-slate-300 min-h-screen">
<!-- Header -->
<header class="bg-slate-800 border-b border-slate-700 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<a href="/docs" class="text-xl font-bold text-white flex items-center gap-2">
<svg class="w-8 h-8 text-sky-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
Mosis Docs
</a>
<nav class="flex items-center gap-6">
<a href="/" class="text-slate-300 hover:text-white transition">Home</a>
<a href="/dashboard" class="text-slate-300 hover:text-white transition">Dashboard</a>
<a href="https://github.com/omixlab/mosis" class="text-slate-300 hover:text-white transition">GitHub</a>
</nav>
</div>
</header>
<div class="max-w-7xl mx-auto flex">
<!-- Sidebar -->
<aside class="w-64 flex-shrink-0 border-r border-slate-700 min-h-[calc(100vh-73px)] sticky top-[73px] self-start hidden lg:block">
<nav class="p-4 space-y-6">
{{range .Navigation}}
<div>
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">{{.Title}}</h3>
<ul class="space-y-1">
{{range .Items}}
<li>
<a href="{{.Path}}" class="block px-3 py-2 rounded-md text-sm transition {{if .Active}}bg-sky-500/10 text-sky-400{{else}}text-slate-300 hover:bg-slate-800{{end}}">
{{.Title}}
</a>
</li>
{{end}}
</ul>
</div>
{{end}}
</nav>
</aside>
<!-- Main content -->
<main class="flex-1 min-w-0">
<article class="prose max-w-4xl mx-auto px-8 py-12">
{{.Content}}
</article>
<!-- Footer -->
<footer class="border-t border-slate-700 px-8 py-6 mt-12">
<div class="max-w-4xl mx-auto flex items-center justify-between text-sm text-slate-400">
<p>&copy; 2024 OmixLab LTD. All rights reserved.</p>
<div class="flex gap-4">
<a href="/privacy" class="hover:text-white transition">Privacy</a>
<a href="/terms" class="hover:text-white transition">Terms</a>
</div>
</div>
</footer>
</main>
</div>
</body>
</html>`