heroagent/pkg/data/doctree/doctree.go
2025-04-23 04:18:28 +02:00

307 lines
7.9 KiB
Go

package doctree
import (
"bytes"
"context"
"fmt"
"github.com/freeflowuniverse/heroagent/pkg/tools"
"github.com/redis/go-redis/v9"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/renderer/html"
)
// Redis client for the doctree package
var redisClient *redis.Client
var ctx = context.Background()
var currentCollection *Collection
// Initialize the Redis client
func init() {
redisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
}
// DocTree represents a manager for multiple collections
type DocTree struct {
Collections map[string]*Collection
defaultCollection string
// For backward compatibility
Name string
Path string
}
// New creates a new DocTree instance
// For backward compatibility, it also accepts path and name parameters
// to create a DocTree with a single collection
func New(args ...string) (*DocTree, error) {
dt := &DocTree{
Collections: make(map[string]*Collection),
}
// Set the global currentDocTree variable
// This ensures that all DocTree instances can access each other's collections
if currentDocTree == nil {
currentDocTree = dt
}
// For backward compatibility with existing code
if len(args) == 2 {
path, name := args[0], args[1]
// Apply namefix for compatibility with tests
nameFixed := tools.NameFix(name)
// Use the fixed name for the collection
_, err := dt.AddCollection(path, nameFixed)
if err != nil {
return nil, fmt.Errorf("failed to initialize DocTree: %w", err)
}
// For backward compatibility
dt.defaultCollection = nameFixed
dt.Path = path
dt.Name = nameFixed
// Register this collection in the global currentDocTree as well
// This ensures that includes can find collections across different DocTree instances
if currentDocTree != dt && !containsCollection(currentDocTree.Collections, nameFixed) {
currentDocTree.Collections[nameFixed] = dt.Collections[nameFixed]
}
}
return dt, nil
}
// Helper function to check if a collection exists in a map
func containsCollection(collections map[string]*Collection, name string) bool {
_, exists := collections[name]
return exists
}
// AddCollection adds a new collection to the DocTree
func (dt *DocTree) AddCollection(path string, name string) (*Collection, error) {
// Create a new collection
collection := NewCollection(path, name)
// Scan the collection
err := collection.Scan()
if err != nil {
return nil, fmt.Errorf("failed to scan collection: %w", err)
}
// Add to the collections map
dt.Collections[collection.Name] = collection
return collection, nil
}
// GetCollection retrieves a collection by name
func (dt *DocTree) GetCollection(name string) (*Collection, error) {
// For compatibility with tests, apply namefix
namefixed := tools.NameFix(name)
// Check if the collection exists
collection, exists := dt.Collections[namefixed]
if !exists {
return nil, fmt.Errorf("collection not found: %s", name)
}
return collection, nil
}
// DeleteCollection removes a collection from the DocTree
func (dt *DocTree) DeleteCollection(name string) error {
// For compatibility with tests, apply namefix
namefixed := tools.NameFix(name)
// Check if the collection exists
_, exists := dt.Collections[namefixed]
if !exists {
return fmt.Errorf("collection not found: %s", name)
}
// Delete from Redis
collectionKey := fmt.Sprintf("collections:%s", namefixed)
redisClient.Del(ctx, collectionKey)
// Remove from the collections map
delete(dt.Collections, namefixed)
return nil
}
// ListCollections returns a list of all collections
func (dt *DocTree) ListCollections() []string {
collections := make([]string, 0, len(dt.Collections))
for name := range dt.Collections {
collections = append(collections, name)
}
return collections
}
// PageGet gets a page by name from a specific collection
// For backward compatibility, if only one argument is provided, it uses the default collection
func (dt *DocTree) PageGet(args ...string) (string, error) {
var collectionName, pageName string
if len(args) == 1 {
// Backward compatibility mode
if dt.defaultCollection == "" {
return "", fmt.Errorf("no default collection set")
}
collectionName = dt.defaultCollection
pageName = args[0]
} else if len(args) == 2 {
collectionName = args[0]
pageName = args[1]
} else {
return "", fmt.Errorf("invalid number of arguments")
}
// Get the collection
collection, err := dt.GetCollection(collectionName)
if err != nil {
return "", err
}
// Set the current collection for include processing
currentCollection = collection
// Get the page content
content, err := collection.PageGet(pageName)
if err != nil {
return "", err
}
// Process includes for PageGet as well
// This is needed for the tests that check the content directly
processedContent := processIncludes(content, collectionName, dt)
return processedContent, nil
}
// PageGetHtml gets a page by name from a specific collection and returns its HTML content
// For backward compatibility, if only one argument is provided, it uses the default collection
func (dt *DocTree) PageGetHtml(args ...string) (string, error) {
var collectionName, pageName string
if len(args) == 1 {
// Backward compatibility mode
if dt.defaultCollection == "" {
return "", fmt.Errorf("no default collection set")
}
collectionName = dt.defaultCollection
pageName = args[0]
} else if len(args) == 2 {
collectionName = args[0]
pageName = args[1]
} else {
return "", fmt.Errorf("invalid number of arguments")
}
// Get the collection
collection, err := dt.GetCollection(collectionName)
if err != nil {
return "", err
}
// Get the HTML
return collection.PageGetHtml(pageName)
}
// FileGetUrl returns the URL for a file in a specific collection
// For backward compatibility, if only one argument is provided, it uses the default collection
func (dt *DocTree) FileGetUrl(args ...string) (string, error) {
var collectionName, fileName string
if len(args) == 1 {
// Backward compatibility mode
if dt.defaultCollection == "" {
return "", fmt.Errorf("no default collection set")
}
collectionName = dt.defaultCollection
fileName = args[0]
} else if len(args) == 2 {
collectionName = args[0]
fileName = args[1]
} else {
return "", fmt.Errorf("invalid number of arguments")
}
// Get the collection
collection, err := dt.GetCollection(collectionName)
if err != nil {
return "", err
}
// Get the URL
return collection.FileGetUrl(fileName)
}
// PageGetPath returns the path to a page in the default collection
// For backward compatibility
func (dt *DocTree) PageGetPath(pageName string) (string, error) {
if dt.defaultCollection == "" {
return "", fmt.Errorf("no default collection set")
}
collection, err := dt.GetCollection(dt.defaultCollection)
if err != nil {
return "", err
}
return collection.PageGetPath(pageName)
}
// Info returns information about the DocTree
// For backward compatibility
func (dt *DocTree) Info() map[string]string {
return map[string]string{
"name": dt.Name,
"path": dt.Path,
"collections": fmt.Sprintf("%d", len(dt.Collections)),
}
}
// Scan scans the default collection
// For backward compatibility
func (dt *DocTree) Scan() error {
if dt.defaultCollection == "" {
return fmt.Errorf("no default collection set")
}
collection, err := dt.GetCollection(dt.defaultCollection)
if err != nil {
return err
}
return collection.Scan()
}
// markdownToHtml converts markdown content to HTML using the goldmark library
func markdownToHtml(markdown string) string {
var buf bytes.Buffer
// Create a new goldmark instance with default extensions
converter := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.Table,
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
)
// Convert markdown to HTML
if err := converter.Convert([]byte(markdown), &buf); err != nil {
// If conversion fails, return the original markdown
return markdown
}
return buf.String()
}