305 lines
10 KiB
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>© 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>`
|