All checks were successful
Tests / unit-tests (push) Successful in 8s
- Dodat goldmark sa Table/Strikethrough/TaskList ekstenzijama (markdown.go) - Prepisana CSS tema na konzolni stil (JetBrains Mono, tamna pozadina, prompt prefix) - Prikaz troškova i trajanja posle svakog Claude odgovora (duration, cost, turns) - Ispravljen parsing result eventa (json.RawMessage + top-level polja) - Ispravljen concurrent write bug na WebSocket (write channel pattern) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
104 lines
2.2 KiB
Go
104 lines
2.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type FileInfo struct {
|
|
Name string
|
|
Path string
|
|
RelPath string
|
|
IsDir bool
|
|
Size int64
|
|
}
|
|
|
|
// ListMarkdownFiles returns all .md files in the project directory (non-recursive).
|
|
func ListMarkdownFiles(projectDir string) ([]FileInfo, error) {
|
|
entries, err := os.ReadDir(projectDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var files []FileInfo
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
if !strings.HasSuffix(strings.ToLower(e.Name()), ".md") {
|
|
continue
|
|
}
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
files = append(files, FileInfo{
|
|
Name: e.Name(),
|
|
Path: filepath.Join(projectDir, e.Name()),
|
|
RelPath: e.Name(),
|
|
Size: info.Size(),
|
|
})
|
|
}
|
|
|
|
// Also check docs/ subdirectory
|
|
docsDir := filepath.Join(projectDir, "docs")
|
|
if docEntries, err := os.ReadDir(docsDir); err == nil {
|
|
for _, e := range docEntries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
if !strings.HasSuffix(strings.ToLower(e.Name()), ".md") {
|
|
continue
|
|
}
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
files = append(files, FileInfo{
|
|
Name: "docs/" + e.Name(),
|
|
Path: filepath.Join(docsDir, e.Name()),
|
|
RelPath: "docs/" + e.Name(),
|
|
Size: info.Size(),
|
|
})
|
|
}
|
|
}
|
|
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].Name < files[j].Name
|
|
})
|
|
|
|
return files, nil
|
|
}
|
|
|
|
// ReadFileContent reads a file and returns its content.
|
|
// It validates that the file is within the project directory (path traversal protection).
|
|
func ReadFileContent(projectDir, relPath string) (string, error) {
|
|
absPath := filepath.Join(projectDir, relPath)
|
|
absPath = filepath.Clean(absPath)
|
|
|
|
// Path traversal protection
|
|
if !strings.HasPrefix(absPath, filepath.Clean(projectDir)) {
|
|
return "", fmt.Errorf("path traversal detected")
|
|
}
|
|
|
|
data, err := os.ReadFile(absPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(data), nil
|
|
}
|
|
|
|
// RenderMarkdownFile reads a markdown file and returns rendered HTML.
|
|
func RenderMarkdownFile(projectDir, relPath string) (string, error) {
|
|
content, err := ReadFileContent(projectDir, relPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return RenderMD(content), nil
|
|
}
|