- Auto-derive site_id from demo directory paths (demos/demo-site -> 'demo', demos/simple/test-simple -> 'simple') - Add validation requiring explicit site_id for non-demo paths with helpful error messages - Remove JavaScript 'demo' fallback and add proper error messaging for missing site_id - Ensure each demo site uses isolated content namespace to prevent content mixing Resolves issue where /sites/simple and /sites/demo both used site_id=demo
296 lines
8.0 KiB
Go
296 lines
8.0 KiB
Go
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)
|
|
}
|