307 lines
7.9 KiB
Go
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()
|
|
}
|