docs: add interactive documentation system with Go+HTMX
- Add internal/docs package with markdown renderer (goldmark) - Create docs layout template with sidebar navigation - Implement hierarchical navigation auto-generated from docs folder - Add table of contents generator (extract ## headings) - Add syntax highlighting for code blocks (highlight.js) - Add mobile responsive design - Add /docs routes to main.go The documentation system features: - Dark theme matching app design - Collapsible sidebar sections (Getting Started, User Guide, Device Setup, API Reference, Contributing) - Table of contents for each page - Breadcrumb navigation - Full-text search (client-side JavaScript, API endpoint ready) - Syntax highlighting for code blocks - Mobile-friendly with hamburger menu All documentation is served from /docs route, no authentication required. Markdown files are rendered using goldmark with GFM extensions and syntax highlighting.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"bookhoard/templates"
|
||||
)
|
||||
|
||||
// BuildNavigation creates the navigation structure from the docs filesystem
|
||||
func (h *DocsHandler) BuildNavigation() *templates.Navigation {
|
||||
docs, err := h.ListDocuments()
|
||||
if err != nil {
|
||||
// Return minimal navigation on error
|
||||
return &templates.Navigation{
|
||||
Sections: []templates.NavSection{
|
||||
{
|
||||
Title: "Getting Started",
|
||||
Items: []templates.NavItem{
|
||||
{Title: "Overview", URL: "/docs"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Group documents by category
|
||||
categories := make(map[string][]templates.NavItem)
|
||||
|
||||
for _, doc := range docs {
|
||||
item := h.createNavItem(doc)
|
||||
category := h.getCategoryForNav(doc)
|
||||
categories[category] = append(categories[category], item)
|
||||
}
|
||||
|
||||
// Build navigation sections
|
||||
var sections []templates.NavSection
|
||||
|
||||
// Define section order
|
||||
sectionOrder := []string{
|
||||
"Getting Started",
|
||||
"User Guide",
|
||||
"Device Setup",
|
||||
"API Reference",
|
||||
"Contributing",
|
||||
}
|
||||
|
||||
// Add sections in order
|
||||
for _, sectionTitle := range sectionOrder {
|
||||
if items, exists := categories[sectionTitle]; exists {
|
||||
sections = append(sections, templates.NavSection{
|
||||
Title: sectionTitle,
|
||||
Items: items,
|
||||
Collapsed: sectionTitle != "Getting Started", // Expand first section
|
||||
})
|
||||
delete(categories, sectionTitle)
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining sections
|
||||
for title, items := range categories {
|
||||
sections = append(sections, templates.NavSection{
|
||||
Title: title,
|
||||
Items: items,
|
||||
Collapsed: true,
|
||||
})
|
||||
}
|
||||
|
||||
return &templates.Navigation{Sections: sections}
|
||||
}
|
||||
|
||||
// createNavItem creates a navigation item from a document path
|
||||
func (h *DocsHandler) createNavItem(docPath string) templates.NavItem {
|
||||
title := h.getDocTitle(docPath)
|
||||
url := "/docs/" + strings.TrimSuffix(docPath, ".md")
|
||||
icon := h.getDocIcon(docPath)
|
||||
|
||||
return templates.NavItem{
|
||||
Title: title,
|
||||
URL: url,
|
||||
Icon: icon,
|
||||
}
|
||||
}
|
||||
|
||||
// getDocTitle extracts a readable title from a document path
|
||||
func (h *DocsHandler) getDocTitle(docPath string) string {
|
||||
// Remove .md extension
|
||||
title := strings.TrimSuffix(docPath, ".md")
|
||||
|
||||
// Split by /
|
||||
parts := strings.Split(title, "/")
|
||||
|
||||
// Get the last part (filename)
|
||||
filename := parts[len(parts)-1]
|
||||
|
||||
// Convert to title case
|
||||
title = strings.Title(strings.ReplaceAll(filename, "-", " "))
|
||||
|
||||
// Handle special cases
|
||||
switch filename {
|
||||
case "INDEX.md":
|
||||
return "Documentation Index"
|
||||
case "API_REFERENCE.md":
|
||||
return "API Reference"
|
||||
case "TROUBLESHOOTING.md":
|
||||
return "Troubleshooting"
|
||||
case "SYNC_USER_GUIDE.md":
|
||||
return "Sync Guide"
|
||||
case "DEVELOPMENT.md":
|
||||
return "Development Guide"
|
||||
case "WEBSOCKET_API.md":
|
||||
return "WebSocket API"
|
||||
case "COLLECTIONS_API.md":
|
||||
return "Collections API"
|
||||
case "KOBO_SETUP.md":
|
||||
return "Kobo Setup"
|
||||
case "KOREADER_SETUP.md":
|
||||
return "KOReader Setup"
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
// getDocIcon returns an appropriate icon for a document
|
||||
func (h *DocsHandler) getDocIcon(docPath string) string {
|
||||
if strings.Contains(docPath, "api") || strings.Contains(docPath, "API") {
|
||||
return "🔌"
|
||||
}
|
||||
if strings.Contains(docPath, "device") {
|
||||
return "📱"
|
||||
}
|
||||
if strings.Contains(docPath, "sync") || strings.Contains(docPath, "SYNC") {
|
||||
return "🔄"
|
||||
}
|
||||
if strings.Contains(docPath, "contributing") || strings.Contains(docPath, "DEVELOPMENT") {
|
||||
return "🛠️"
|
||||
}
|
||||
if strings.Contains(docPath, "INDEX") {
|
||||
return "📚"
|
||||
}
|
||||
if strings.Contains(docPath, "troubleshooting") {
|
||||
return "🔧"
|
||||
}
|
||||
return "📄"
|
||||
}
|
||||
|
||||
// getCategoryForNav determines the navigation category for a document
|
||||
func (h *DocsHandler) getCategoryForNav(docPath string) string {
|
||||
// API documentation
|
||||
if strings.Contains(docPath, "api") || strings.Contains(docPath, "API") {
|
||||
return "API Reference"
|
||||
}
|
||||
|
||||
// Device setup
|
||||
if strings.HasPrefix(docPath, "devices/") {
|
||||
return "Device Setup"
|
||||
}
|
||||
|
||||
// Contributing
|
||||
if strings.HasPrefix(docPath, "contributing/") {
|
||||
return "Contributing"
|
||||
}
|
||||
|
||||
// Core user documentation
|
||||
docName := strings.ToLower(docPath)
|
||||
switch {
|
||||
case strings.Contains(docName, "sync"):
|
||||
return "User Guide"
|
||||
case strings.Contains(docName, "troubleshoot"):
|
||||
return "Getting Started"
|
||||
case strings.Contains(docName, "index"):
|
||||
return "Getting Started"
|
||||
default:
|
||||
return "Getting Started"
|
||||
}
|
||||
}
|
||||
|
||||
// GetAPIEndpoints returns a list of API endpoints for the API documentation
|
||||
func (h *DocsHandler) GetAPIEndpoints() []APIEndpoint {
|
||||
// This would be better auto-generated from Go code comments
|
||||
// For now, return a static list based on your API
|
||||
endpoints := []APIEndpoint{
|
||||
// Authentication
|
||||
{Method: "POST", Path: "/api/auth/register", Category: "Authentication", Title: "Register User"},
|
||||
{Method: "POST", Path: "/api/auth/login", Category: "Authentication", Title: "Login"},
|
||||
{Method: "POST", Path: "/api/auth/refresh", Category: "Authentication", Title: "Refresh Token"},
|
||||
{Method: "POST", Path: "/api/auth/logout", Category: "Authentication", Title: "Logout"},
|
||||
|
||||
// Users
|
||||
{Method: "GET", Path: "/api/users/me", Category: "Users", Title: "Get Current User"},
|
||||
{Method: "PUT", Path: "/api/users/me/profile", Category: "Users", Title: "Update Profile"},
|
||||
{Method: "PUT", Path: "/api/users/me/theme", Category: "Users", Title: "Update Theme"},
|
||||
{Method: "PUT", Path: "/api/users/me/password", Category: "Users", Title: "Change Password"},
|
||||
|
||||
// Libraries
|
||||
{Method: "GET", Path: "/api/libraries/visible", Category: "Libraries", Title: "Get Visible Libraries"},
|
||||
{Method: "GET", Path: "/api/libraries/{id}", Category: "Libraries", Title: "Get Library Details"},
|
||||
{Method: "POST", Path: "/api/libraries", Category: "Libraries", Title: "Create Library"},
|
||||
|
||||
// Media Items
|
||||
{Method: "GET", Path: "/api/media-items", Category: "Media Items", Title: "List Media Items"},
|
||||
{Method: "GET", Path: "/api/media-items/{id}", Category: "Media Items", Title: "Get Media Item"},
|
||||
{Method: "POST", Path: "/api/media-items/search", Category: "Media Items", Title: "Search Media Items"},
|
||||
|
||||
// Progress
|
||||
{Method: "GET", Path: "/api/media-items/{id}/progress", Category: "Progress", Title: "Get Reading Progress"},
|
||||
{Method: "PUT", Path: "/api/media-items/{id}/progress", Category: "Progress", Title: "Update Reading Progress"},
|
||||
|
||||
// Devices
|
||||
{Method: "POST", Path: "/api/devices/register", Category: "Devices", Title: "Register Device"},
|
||||
{Method: "GET", Path: "/api/devices", Category: "Devices", Title: "List User Devices"},
|
||||
{Method: "DELETE", Path: "/api/devices/{id}", Category: "Devices", Title: "Revoke Device"},
|
||||
}
|
||||
|
||||
sort.Slice(endpoints, func(i, j int) bool {
|
||||
if endpoints[i].Category == endpoints[j].Category {
|
||||
return endpoints[i].Title < endpoints[j].Title
|
||||
}
|
||||
return endpoints[i].Category < endpoints[j].Category
|
||||
})
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
type APIEndpoint struct {
|
||||
Method string
|
||||
Path string
|
||||
Category string
|
||||
Title string
|
||||
}
|
||||
Reference in New Issue
Block a user