Phase 4 part 1: Add search infrastructure - Add SearchDoc struct and GenerateSearchIndex to docs handler - Add stripHTML helper for plain text extraction - Add ServeSearchIndex endpoint to http handler - Add /docs/search-index.json route in main.go - Search index includes all documentation files with ID, title, content, URL
346 lines
9.2 KiB
Go
346 lines
9.2 KiB
Go
package docs
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"bookhoard/templates"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// HTTPHandler wraps the docs handler for Echo framework
|
|
type HTTPHandler struct {
|
|
docs *DocsHandler
|
|
}
|
|
|
|
// NewHTTPHandler creates a new docs HTTP handler
|
|
func NewHTTPHandler(docsPath string) *HTTPHandler {
|
|
return &HTTPHandler{
|
|
docs: NewDocsHandler(docsPath),
|
|
}
|
|
}
|
|
|
|
// ShowDocumentation renders a documentation page
|
|
func (h *HTTPHandler) ShowDocumentation(c echo.Context) error {
|
|
// Get the document path from URL parameter
|
|
docPath := c.Param("*")
|
|
|
|
// Load the document
|
|
doc, err := h.docs.LoadDocument(docPath)
|
|
if err != nil {
|
|
// Try to load as API endpoint
|
|
if strings.HasPrefix(docPath, "api/") {
|
|
return h.ShowAPIEndpoint(c, strings.TrimPrefix(docPath, "api/"))
|
|
}
|
|
|
|
// Return error page
|
|
errorHTML := fmt.Sprintf(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Document Not Found - Bookhoard</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
</head>
|
|
<body class="bg-gray-900 text-gray-100 min-h-screen flex items-center justify-center">
|
|
<div class="text-center">
|
|
<h1 class="text-2xl font-bold mb-4">Document Not Found</h1>
|
|
<p class="mb-4">%s</p>
|
|
<a href="/docs" class="text-blue-400 hover:underline">Return to Documentation</a>
|
|
</div>
|
|
</body>
|
|
</html>`, err.Error())
|
|
|
|
return c.HTML(http.StatusNotFound, errorHTML)
|
|
}
|
|
|
|
// Get navigation
|
|
nav := h.docs.BuildNavigation()
|
|
|
|
// Create empty user (docs are public, no auth required)
|
|
user := templates.User{
|
|
ID: "",
|
|
Username: "",
|
|
Email: "",
|
|
Role: "",
|
|
Theme: "tokyo-night",
|
|
}
|
|
|
|
// Render documentation page
|
|
var buf bytes.Buffer
|
|
err = templates.DocsLayout(*nav, *doc, user).Render(c.Request().Context(), &buf)
|
|
if err != nil {
|
|
fmt.Printf("DOCS RENDER ERROR: %v\n", err)
|
|
return c.HTML(http.StatusInternalServerError, "Failed to render page: "+err.Error())
|
|
}
|
|
fmt.Printf("DOCS: Successfully rendered %s (%d bytes)\n", docPath, buf.Len())
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
}
|
|
|
|
// ShowAPIEndpoint shows a specific API endpoint documentation
|
|
func (h *HTTPHandler) ShowAPIEndpoint(c echo.Context, endpointPath string) error {
|
|
// Check if user is logged in
|
|
isLoggedIn := false
|
|
if userID := c.Get("user_id"); userID != nil {
|
|
isLoggedIn = true
|
|
}
|
|
|
|
// Get endpoint data for the explorer
|
|
endpointInfo, err := h.docs.GetAPIEndpointData(endpointPath)
|
|
if err != nil {
|
|
// Endpoint not found in explorer data, fall back to old behavior
|
|
return h.showLegacyAPIEndpoint(c, endpointPath, isLoggedIn)
|
|
}
|
|
|
|
// Load the markdown documentation for this endpoint
|
|
docPath := "api/" + endpointPath + ".md"
|
|
doc, err := h.docs.LoadDocument(docPath)
|
|
if err != nil {
|
|
errorHTML := `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>API Endpoint Not Found - Bookhoard</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
</head>
|
|
<body class="bg-gray-900 text-gray-100 min-h-screen flex items-center justify-center">
|
|
<div class="text-center">
|
|
<h1 class="text-2xl font-bold mb-4">API Endpoint Not Found</h1>
|
|
<a href="/docs/api" class="text-blue-400 hover:underline">Return to API Documentation</a>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
return c.HTML(http.StatusNotFound, errorHTML)
|
|
}
|
|
|
|
// Get navigation
|
|
nav := h.docs.BuildNavigation()
|
|
|
|
// Create empty user (docs are public)
|
|
user := templates.User{
|
|
ID: "",
|
|
Username: "",
|
|
Email: "",
|
|
Role: "",
|
|
Theme: "tokyo-night",
|
|
}
|
|
|
|
// Create API explorer data
|
|
explorerData := templates.APIExplorerData{
|
|
Endpoint: *endpointInfo,
|
|
IsLoggedIn: isLoggedIn,
|
|
}
|
|
|
|
// Render API endpoint page with explorer
|
|
var buf bytes.Buffer
|
|
err = templates.DocsLayoutWithExplorer(*nav, *doc, user, explorerData).Render(c.Request().Context(), &buf)
|
|
|
|
if err != nil {
|
|
return c.HTML(http.StatusInternalServerError, "Failed to render page")
|
|
}
|
|
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
}
|
|
|
|
// showLegacyAPIEndpoint shows API endpoint documentation without explorer data
|
|
func (h *HTTPHandler) showLegacyAPIEndpoint(c echo.Context, endpointPath string, isLoggedIn bool) error {
|
|
endpoints := h.docs.GetAPIEndpoints()
|
|
|
|
// Find the endpoint
|
|
var foundEndpoint *APIEndpoint
|
|
for _, ep := range endpoints {
|
|
if strings.HasSuffix(endpointPath, strings.TrimPrefix(ep.Path, "/api/")) {
|
|
foundEndpoint = &ep
|
|
break
|
|
}
|
|
}
|
|
|
|
if foundEndpoint == nil {
|
|
errorHTML := `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>API Endpoint Not Found - Bookhoard</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
</head>
|
|
<body class="bg-gray-900 text-gray-100 min-h-screen flex items-center justify-center">
|
|
<div class="text-center">
|
|
<h1 class="text-2xl font-bold mb-4">API Endpoint Not Found</h1>
|
|
<a href="/docs/api" class="text-blue-400 hover:underline">Return to API Documentation</a>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
return c.HTML(http.StatusNotFound, errorHTML)
|
|
}
|
|
|
|
// Get navigation
|
|
nav := h.docs.BuildNavigation()
|
|
|
|
// Create empty user
|
|
user := templates.User{
|
|
ID: "",
|
|
Username: "",
|
|
Email: "",
|
|
Role: "",
|
|
Theme: "tokyo-night",
|
|
}
|
|
|
|
// Render API endpoint page
|
|
var buf bytes.Buffer
|
|
err := templates.DocsLayout(*nav, templates.Document{
|
|
Title: foundEndpoint.Title,
|
|
Content: fmt.Sprintf("<p>Documentation for <code>%s %s</code></p>", foundEndpoint.Method, foundEndpoint.Path),
|
|
TOC: []templates.TOCItem{},
|
|
Breadcrumb: []templates.BreadcrumbItem{
|
|
{Title: "Docs", URL: "/docs"},
|
|
{Title: "API Reference", URL: "/docs/api"},
|
|
{Title: foundEndpoint.Category, URL: "/docs/api"},
|
|
{Title: foundEndpoint.Title, URL: ""},
|
|
},
|
|
Category: "API Reference",
|
|
SourceFile: "api",
|
|
}, user).Render(c.Request().Context(), &buf)
|
|
|
|
if err != nil {
|
|
return c.HTML(http.StatusInternalServerError, "Failed to render page")
|
|
}
|
|
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
}
|
|
|
|
// DocsHome redirects to the documentation index
|
|
func (h *HTTPHandler) DocsHome(c echo.Context) error {
|
|
return c.Redirect(http.StatusFound, "/docs/INDEX.md")
|
|
}
|
|
|
|
// APIHome shows the API documentation home page
|
|
func (h *HTTPHandler) APIHome(c echo.Context) error {
|
|
endpoints := h.docs.GetAPIEndpoints()
|
|
nav := h.docs.BuildNavigation()
|
|
|
|
user := templates.User{
|
|
ID: "",
|
|
Username: "",
|
|
Email: "",
|
|
Role: "",
|
|
Theme: "tokyo-night",
|
|
}
|
|
|
|
// Build API documentation content
|
|
content := "<h2>API Endpoints</h2><p>Complete API reference for Bookhoard v1.0.</p><ul>"
|
|
for _, ep := range endpoints {
|
|
content += fmt.Sprintf(`<li><a href="/docs/api%s"><strong>%s %s</strong></a> - %s</li>`,
|
|
strings.TrimPrefix(ep.Path, "/api"),
|
|
ep.Method,
|
|
ep.Path,
|
|
ep.Title,
|
|
)
|
|
}
|
|
content += "</ul>"
|
|
|
|
apiDoc := &templates.Document{
|
|
Title: "API Reference",
|
|
Content: content,
|
|
TOC: []templates.TOCItem{},
|
|
Breadcrumb: []templates.BreadcrumbItem{
|
|
{Title: "Docs", URL: "/docs"},
|
|
{Title: "API Reference", URL: ""},
|
|
},
|
|
Category: "API Reference",
|
|
SourceFile: "API_REFERENCE.md",
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err := templates.DocsLayout(*nav, *apiDoc, user).Render(c.Request().Context(), &buf)
|
|
|
|
if err != nil {
|
|
return c.HTML(http.StatusInternalServerError, "Failed to render page")
|
|
}
|
|
|
|
return c.HTML(http.StatusOK, buf.String())
|
|
}
|
|
|
|
// Search searches through documentation
|
|
func (h *HTTPHandler) Search(c echo.Context) error {
|
|
query := c.QueryParam("q")
|
|
if query == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{
|
|
"error": "query parameter 'q' is required",
|
|
})
|
|
}
|
|
|
|
// Get all documents
|
|
docs, err := h.docs.ListDocuments()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to list documents",
|
|
})
|
|
}
|
|
|
|
// Simple search implementation
|
|
var results []SearchResult
|
|
queryLower := strings.ToLower(query)
|
|
|
|
for _, docPath := range docs {
|
|
doc, err := h.docs.LoadDocument(docPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Check if title matches
|
|
if strings.Contains(strings.ToLower(doc.Title), queryLower) {
|
|
results = append(results, SearchResult{
|
|
Title: doc.Title,
|
|
URL: "/docs/" + strings.TrimSuffix(doc.SourceFile, ".md"),
|
|
Type: "Documentation",
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Check if content matches (simple substring search)
|
|
contentLower := strings.ToLower(doc.Content)
|
|
if strings.Contains(contentLower, queryLower) {
|
|
results = append(results, SearchResult{
|
|
Title: doc.Title,
|
|
URL: "/docs/" + strings.TrimSuffix(doc.SourceFile, ".md"),
|
|
Type: "Documentation",
|
|
})
|
|
}
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"results": results,
|
|
"count": len(results),
|
|
})
|
|
}
|
|
|
|
// SearchResult represents a search result
|
|
type SearchResult struct {
|
|
Title string
|
|
URL string
|
|
Type string
|
|
}
|
|
|
|
// TryEndpoint executes an API endpoint (for the interactive explorer)
|
|
func (h *HTTPHandler) TryEndpoint(c echo.Context) error {
|
|
// This would execute the actual API request
|
|
// For now, return a placeholder
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"message": "API explorer not yet implemented",
|
|
})
|
|
}
|
|
|
|
// ServeSearchIndex serves the Lunr.js search index
|
|
func (h *HTTPHandler) ServeSearchIndex(c echo.Context) error {
|
|
index, err := h.docs.GenerateSearchIndex()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to generate search index",
|
|
})
|
|
}
|
|
return c.JSON(http.StatusOK, index)
|
|
}
|