209 lines
5.0 KiB
Go
209 lines
5.0 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
htmltpl "html/template"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/extension"
|
|
"github.com/yuin/goldmark/renderer/html"
|
|
)
|
|
|
|
// docFile represents a markdown file in the file listing.
|
|
type docFile struct {
|
|
Path string // relative path from project root
|
|
Name string // display name
|
|
}
|
|
|
|
// docsListData holds data for the docs listing page.
|
|
type docsListData struct {
|
|
Files []docFile
|
|
}
|
|
|
|
// docsViewData holds data for viewing a single doc.
|
|
type docsViewData struct {
|
|
Path string
|
|
Breadcrumbs []breadcrumb
|
|
HTML htmltpl.HTML
|
|
}
|
|
|
|
// breadcrumb represents one segment of the navigation path.
|
|
type breadcrumb struct {
|
|
Name string
|
|
Path string
|
|
}
|
|
|
|
// md is the goldmark markdown renderer.
|
|
var md = goldmark.New(
|
|
goldmark.WithExtensions(extension.Table, extension.Strikethrough),
|
|
goldmark.WithRendererOptions(html.WithUnsafe()),
|
|
)
|
|
|
|
// projectRoot returns the KAOS project root derived from TasksDir.
|
|
func (s *Server) projectRoot() string {
|
|
return filepath.Dir(s.Config.TasksDir)
|
|
}
|
|
|
|
// handleDocsList serves the docs listing page.
|
|
func (s *Server) handleDocsList(c *gin.Context) {
|
|
root := s.projectRoot()
|
|
files := scanMarkdownFiles(root)
|
|
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
c.String(http.StatusOK, renderDocsList(docsListData{Files: files}))
|
|
}
|
|
|
|
// handleDocsView serves a rendered markdown file.
|
|
func (s *Server) handleDocsView(c *gin.Context) {
|
|
relPath := c.Param("path")
|
|
// Strip leading slash from wildcard param
|
|
relPath = strings.TrimPrefix(relPath, "/")
|
|
|
|
root := s.projectRoot()
|
|
|
|
// Security: only .md files
|
|
if !strings.HasSuffix(relPath, ".md") {
|
|
c.String(http.StatusForbidden, "Samo .md fajlovi su dozvoljeni")
|
|
return
|
|
}
|
|
|
|
// Security: resolve and check path traversal
|
|
absPath := filepath.Join(root, relPath)
|
|
absPath = filepath.Clean(absPath)
|
|
if !strings.HasPrefix(absPath, filepath.Clean(root)+string(filepath.Separator)) {
|
|
c.String(http.StatusForbidden, "Pristup odbijen")
|
|
return
|
|
}
|
|
|
|
content, err := os.ReadFile(absPath)
|
|
if err != nil {
|
|
c.String(http.StatusNotFound, "Fajl nije pronađen: %s", relPath)
|
|
return
|
|
}
|
|
|
|
// Render markdown to HTML
|
|
rendered := renderMarkdown(content, relPath)
|
|
|
|
data := docsViewData{
|
|
Path: relPath,
|
|
Breadcrumbs: buildBreadcrumbs(relPath),
|
|
HTML: htmltpl.HTML(rendered),
|
|
}
|
|
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
c.String(http.StatusOK, renderDocsView(data))
|
|
}
|
|
|
|
// scanMarkdownFiles walks the project root and returns all .md files.
|
|
func scanMarkdownFiles(root string) []docFile {
|
|
var files []docFile
|
|
|
|
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Skip hidden dirs, code/vendor, .git
|
|
name := info.Name()
|
|
if info.IsDir() {
|
|
if strings.HasPrefix(name, ".") || name == "vendor" || name == "node_modules" {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if !strings.HasSuffix(name, ".md") {
|
|
return nil
|
|
}
|
|
|
|
rel, _ := filepath.Rel(root, path)
|
|
files = append(files, docFile{
|
|
Path: rel,
|
|
Name: rel,
|
|
})
|
|
return nil
|
|
})
|
|
|
|
return files
|
|
}
|
|
|
|
// renderMarkdown converts markdown bytes to HTML, rewriting relative .md links to /docs/ links.
|
|
func renderMarkdown(source []byte, docPath string) string {
|
|
var buf bytes.Buffer
|
|
if err := md.Convert(source, &buf); err != nil {
|
|
return "<p>Greška pri renderovanju: " + err.Error() + "</p>"
|
|
}
|
|
|
|
html := buf.String()
|
|
|
|
// Rewrite relative .md links to /docs/ links
|
|
// Find href="something.md" or href="path/to/file.md" patterns
|
|
html = rewriteMarkdownLinks(html, docPath)
|
|
|
|
return html
|
|
}
|
|
|
|
// rewriteMarkdownLinks converts relative .md hrefs to /docs/ prefixed paths.
|
|
func rewriteMarkdownLinks(htmlContent, currentPath string) string {
|
|
dir := filepath.Dir(currentPath)
|
|
return rewriteLinksSimple(htmlContent, dir)
|
|
}
|
|
|
|
// rewriteLinksSimple does a single pass to rewrite .md links.
|
|
func rewriteLinksSimple(htmlContent, baseDir string) string {
|
|
var result strings.Builder
|
|
remaining := htmlContent
|
|
|
|
for {
|
|
idx := strings.Index(remaining, `href="`)
|
|
if idx == -1 {
|
|
result.WriteString(remaining)
|
|
break
|
|
}
|
|
|
|
result.WriteString(remaining[:idx+6])
|
|
remaining = remaining[idx+6:]
|
|
|
|
endQuote := strings.Index(remaining, `"`)
|
|
if endQuote == -1 {
|
|
result.WriteString(remaining)
|
|
break
|
|
}
|
|
|
|
href := remaining[:endQuote]
|
|
remaining = remaining[endQuote:]
|
|
|
|
if strings.HasSuffix(href, ".md") && !strings.HasPrefix(href, "http") && !strings.HasPrefix(href, "/") {
|
|
if baseDir == "." || baseDir == "" {
|
|
result.WriteString("/docs/" + href)
|
|
} else {
|
|
result.WriteString("/docs/" + filepath.Join(baseDir, href))
|
|
}
|
|
} else {
|
|
result.WriteString(href)
|
|
}
|
|
}
|
|
|
|
return result.String()
|
|
}
|
|
|
|
// buildBreadcrumbs creates navigation breadcrumbs from a file path.
|
|
func buildBreadcrumbs(path string) []breadcrumb {
|
|
parts := strings.Split(path, "/")
|
|
crumbs := make([]breadcrumb, len(parts))
|
|
|
|
for i, part := range parts {
|
|
crumbs[i] = breadcrumb{
|
|
Name: part,
|
|
Path: strings.Join(parts[:i+1], "/"),
|
|
}
|
|
}
|
|
|
|
return crumbs
|
|
}
|