...
This commit is contained in:
306
pkg/data/doctree/doctree.go
Normal file
306
pkg/data/doctree/doctree.go
Normal file
@@ -0,0 +1,306 @@
|
||||
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()
|
||||
}
|
Reference in New Issue
Block a user