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 }