package content import ( "fmt" "golang.org/x/net/html" "os" "path/filepath" "strings" "github.com/insertr/insertr/internal/config" "github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/engine" ) // EnhancementConfig configures the enhancement pipeline type EnhancementConfig struct { Discovery DiscoveryConfig ContentInjection bool GenerateIDs bool } // Type alias for backward compatibility type DiscoveryConfig = config.DiscoveryConfig // Enhancer combines discovery, ID generation, and content injection in unified pipeline type Enhancer struct { engine *engine.ContentEngine discoverer *Discoverer config EnhancementConfig siteID string } // NewEnhancer creates a new HTML enhancer with unified pipeline func NewEnhancer(client db.ContentRepository, siteID string, config EnhancementConfig) *Enhancer { return &Enhancer{ engine: engine.NewContentEngine(client), discoverer: NewDiscoverer(), config: config, siteID: siteID, } } // NewEnhancerWithAuth creates a new HTML enhancer with auth provider func NewEnhancerWithAuth(client db.ContentRepository, siteID string, config EnhancementConfig, authProvider *engine.AuthProvider) *Enhancer { return &Enhancer{ engine: engine.NewContentEngineWithAuth(client, authProvider), discoverer: NewDiscoverer(), config: config, siteID: siteID, } } // NewDefaultEnhancer creates an enhancer with default configuration func NewDefaultEnhancer(client db.ContentRepository, siteID string) *Enhancer { defaultConfig := EnhancementConfig{ Discovery: DiscoveryConfig{ Enabled: true, Aggressive: false, Containers: true, Individual: true, }, ContentInjection: true, GenerateIDs: true, } return NewEnhancer(client, siteID, defaultConfig) } // EnhanceFile processes a single HTML file through the complete pipeline func (e *Enhancer) EnhanceFile(inputPath, outputPath string) error { // Read HTML file htmlContent, err := os.ReadFile(inputPath) if err != nil { return fmt.Errorf("reading file %s: %w", inputPath, err) } // Process through unified pipeline processedHTML, err := e.processHTML(htmlContent, filepath.Base(inputPath)) if err != nil { return fmt.Errorf("processing HTML %s: %w", inputPath, err) } // Create output directory if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil { return fmt.Errorf("creating output directory: %w", err) } // Write processed HTML return os.WriteFile(outputPath, processedHTML, 0644) } // EnhanceDirectory processes all files in a directory through the unified pipeline func (e *Enhancer) EnhanceDirectory(inputDir, outputDir string) error { // Create output directory if err := os.MkdirAll(outputDir, 0755); err != nil { return fmt.Errorf("creating output directory: %w", err) } // Walk input directory return filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Calculate relative path and output path relPath, err := filepath.Rel(inputDir, path) if err != nil { return err } outputPath := filepath.Join(outputDir, relPath) // Handle directories if info.IsDir() { return os.MkdirAll(outputPath, info.Mode()) } // Process HTML files through enhancement pipeline if strings.HasSuffix(strings.ToLower(path), ".html") { return e.EnhanceFile(path, outputPath) } // Copy non-HTML files as-is return e.copyFile(path, outputPath) }) } // processHTML implements the unified enhancement pipeline func (e *Enhancer) processHTML(htmlContent []byte, filePath string) ([]byte, error) { var processedHTML []byte = htmlContent // Phase 1: Element Discovery (if enabled) if e.config.Discovery.Enabled { discoveredHTML, err := e.discoverElements(processedHTML, filePath) if err != nil { return nil, fmt.Errorf("element discovery: %w", err) } processedHTML = discoveredHTML } // Phase 2 & 3: ID Generation + Content Injection (via engine) if e.config.GenerateIDs || e.config.ContentInjection { enhancedHTML, err := e.enhanceWithEngine(processedHTML, filePath) if err != nil { return nil, fmt.Errorf("engine enhancement: %w", err) } processedHTML = enhancedHTML } return processedHTML, nil } // discoverElements adds insertr classes to viable elements func (e *Enhancer) discoverElements(htmlContent []byte, filePath string) ([]byte, error) { // Parse HTML doc, err := html.Parse(strings.NewReader(string(htmlContent))) if err != nil { return nil, fmt.Errorf("parsing HTML: %w", err) } // Find and mark viable elements result := &FileDiscoveryResult{Document: doc} e.discoverer.discoverNode(doc, result, e.config.Discovery.Aggressive) // Render back to HTML var buf strings.Builder if err := html.Render(&buf, doc); err != nil { return nil, fmt.Errorf("rendering HTML: %w", err) } return []byte(buf.String()), nil } // enhanceWithEngine uses the unified engine for ID generation and content injection func (e *Enhancer) enhanceWithEngine(htmlContent []byte, filePath string) ([]byte, error) { // Determine processing mode var mode engine.ProcessMode if e.config.ContentInjection { mode = engine.Enhancement // ID generation + content injection } else { mode = engine.IDGeneration // ID generation only } // Process with engine result, err := e.engine.ProcessContent(engine.ContentInput{ HTML: htmlContent, FilePath: filePath, SiteID: e.siteID, Mode: mode, }) if err != nil { return nil, fmt.Errorf("engine processing: %w", err) } // Render enhanced document var buf strings.Builder if err := html.Render(&buf, result.Document); err != nil { return nil, fmt.Errorf("rendering enhanced HTML: %w", err) } return []byte(buf.String()), nil } // SetSiteID sets the site ID for the enhancer func (e *Enhancer) SetSiteID(siteID string) { e.siteID = siteID } // EnhanceInPlace performs in-place enhancement of static site files func (e *Enhancer) EnhanceInPlace(sitePath string, siteID string) error { // Use the provided siteID (derivation should happen at CLI level) e.siteID = siteID // Use EnhanceDirectory with same input and output (in-place) return e.EnhanceDirectory(sitePath, sitePath) } // DeriveOrValidateSiteID automatically derives site_id for demo paths or validates for production func DeriveOrValidateSiteID(sitePath string, configSiteID string) string { // Check if this is a demo path if strings.Contains(sitePath, "/demos/") || strings.Contains(sitePath, "./demos/") { return deriveDemoSiteID(sitePath) } // For non-demo paths, return the configured site_id // Validation of non-demo site_id will be handled at the CLI level return configSiteID } // deriveDemoSiteID extracts site_id from demo directory structure func deriveDemoSiteID(sitePath string) string { // Convert to absolute path and clean it absPath, err := filepath.Abs(sitePath) if err != nil { absPath = sitePath } absPath = filepath.Clean(absPath) // Flattened structure - just use directory name after demos/ // demos/default -> "default" // demos/simple -> "simple" // demos/dan-eden-portfolio -> "dan-eden-portfolio" parts := strings.Split(absPath, string(filepath.Separator)) // Find the demos directory index demosIndex := -1 for i, part := range parts { if part == "demos" { demosIndex = i break } } if demosIndex == -1 || demosIndex >= len(parts)-1 { // Fallback if demos not found in path return "default" } // Get the segment after demos/ and clean it dirName := parts[demosIndex+1] dirName = strings.TrimSuffix(dirName, "_enhanced") return dirName } // copyFile copies a file from src to dst func (e *Enhancer) copyFile(src, dst string) error { // Create directory for destination if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } // Read source data, err := os.ReadFile(src) if err != nil { return err } // Write destination return os.WriteFile(dst, data, 0644) }