package content import ( "fmt" "golang.org/x/net/html" "os" "path/filepath" "strings" "github.com/insertr/insertr/internal/engine" ) // EnhancementConfig configures the enhancement pipeline type EnhancementConfig struct { Discovery DiscoveryConfig ContentInjection bool GenerateIDs bool } // DiscoveryConfig configures element discovery type DiscoveryConfig struct { Enabled bool Aggressive bool Containers bool Individual bool } // 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 engine.ContentClient, siteID string, config EnhancementConfig) *Enhancer { return &Enhancer{ engine: engine.NewContentEngine(client), discoverer: NewDiscoverer(), config: config, siteID: siteID, } } // NewDefaultEnhancer creates an enhancer with default configuration func NewDefaultEnhancer(client engine.ContentClient, 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 } // 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/") { 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) // Look for patterns in demo paths: // demos/demo-site_enhanced -> "demo" // demos/simple/test-simple_enhanced -> "simple" // demos/simple/dan-eden-portfolio -> "dan-eden" 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 "demo" } // Get the segment after demos/ nextSegment := parts[demosIndex+1] // Handle different demo directory patterns: if strings.HasPrefix(nextSegment, "demo-site") { return "demo" } if nextSegment == "simple" && len(parts) > demosIndex+2 { // For demos/simple/something, use the something part finalSegment := parts[demosIndex+2] // Extract meaningful name from directory if strings.HasPrefix(finalSegment, "test-") { // test-simple_enhanced -> simple return "simple" } if strings.Contains(finalSegment, "dan-eden") { return "dan-eden" } // Generic case: use the directory name as-is return strings.TrimSuffix(finalSegment, "_enhanced") } // Default case: use the immediate subdirectory of demos/ result := strings.TrimSuffix(nextSegment, "_enhanced") // Clean up common suffixes result = strings.TrimSuffix(result, "-site") return result } // 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) }