- Add SiteManager for registering and managing static sites with file-based enhancement - Implement EnhanceInPlace method for in-place file modification using database content - Integrate automatic file enhancement triggers in UpdateContent API handler - Add comprehensive site configuration support in insertr.yaml with auto-enhancement - Extend serve command to automatically register and manage configured sites - Add backup system for original files before enhancement - Support multi-site hosting with individual auto-enhancement settings - Update documentation for server-hosted enhancement workflow This enables real-time content deployment where database content changes immediately update static files without requiring rebuilds or redeployment. The database remains the single source of truth while maintaining static file performance benefits.
323 lines
8.9 KiB
Go
323 lines
8.9 KiB
Go
package content
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/net/html"
|
|
|
|
"github.com/insertr/insertr/internal/parser"
|
|
)
|
|
|
|
// Enhancer combines parsing and content injection
|
|
type Enhancer struct {
|
|
parser *parser.Parser
|
|
injector *Injector
|
|
}
|
|
|
|
// NewEnhancer creates a new HTML enhancer
|
|
func NewEnhancer(client ContentClient, siteID string) *Enhancer {
|
|
return &Enhancer{
|
|
parser: parser.New(),
|
|
injector: NewInjector(client, siteID),
|
|
}
|
|
}
|
|
|
|
// EnhanceFile processes an HTML file and injects content
|
|
func (e *Enhancer) EnhanceFile(inputPath, outputPath string) error {
|
|
// Use parser to get elements from file
|
|
result, err := e.parser.ParseDirectory(filepath.Dir(inputPath))
|
|
if err != nil {
|
|
return fmt.Errorf("parsing file: %w", err)
|
|
}
|
|
|
|
// Filter elements for this specific file
|
|
var fileElements []parser.Element
|
|
inputBaseName := filepath.Base(inputPath)
|
|
for _, elem := range result.Elements {
|
|
elemBaseName := filepath.Base(elem.FilePath)
|
|
if elemBaseName == inputBaseName {
|
|
fileElements = append(fileElements, elem)
|
|
}
|
|
}
|
|
|
|
if len(fileElements) == 0 {
|
|
// No insertr elements found, copy file as-is
|
|
return e.copyFile(inputPath, outputPath)
|
|
}
|
|
|
|
// Read and parse HTML for modification
|
|
htmlContent, err := os.ReadFile(inputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("reading file %s: %w", inputPath, err)
|
|
}
|
|
|
|
doc, err := html.Parse(strings.NewReader(string(htmlContent)))
|
|
if err != nil {
|
|
return fmt.Errorf("parsing HTML: %w", err)
|
|
}
|
|
|
|
// Find and inject content for each element
|
|
for _, elem := range fileElements {
|
|
// Find the node in the parsed document
|
|
// Note: This is a simplified approach - in production we'd need more robust node matching
|
|
if err := e.injectElementContent(doc, elem); err != nil {
|
|
fmt.Printf("⚠️ Warning: failed to inject content for %s: %v\n", elem.ContentID, err)
|
|
}
|
|
}
|
|
|
|
// Inject editor assets for development
|
|
libraryScript := GetLibraryScript(false) // Use non-minified for development debugging
|
|
e.injector.InjectEditorAssets(doc, true, libraryScript)
|
|
|
|
// Write enhanced HTML
|
|
if err := e.writeHTML(doc, outputPath); err != nil {
|
|
return fmt.Errorf("writing enhanced HTML: %w", err)
|
|
}
|
|
|
|
fmt.Printf("✅ Enhanced: %s → %s (%d elements)\n",
|
|
filepath.Base(inputPath),
|
|
filepath.Base(outputPath),
|
|
len(fileElements))
|
|
|
|
return nil
|
|
}
|
|
|
|
// injectElementContent finds and injects content for a specific element
|
|
func (e *Enhancer) injectElementContent(doc *html.Node, elem parser.Element) error {
|
|
// Fetch content from database
|
|
contentItem, err := e.injector.client.GetContent(e.injector.siteID, elem.ContentID)
|
|
if err != nil {
|
|
return fmt.Errorf("fetching content: %w", err)
|
|
}
|
|
|
|
// Find nodes with insertr class and inject content
|
|
e.findAndInjectNodes(doc, elem, contentItem)
|
|
return nil
|
|
}
|
|
|
|
// findAndInjectNodes recursively finds nodes and injects content
|
|
func (e *Enhancer) findAndInjectNodes(node *html.Node, elem parser.Element, contentItem *ContentItem) {
|
|
if node.Type == html.ElementNode {
|
|
// Check if this node matches our element criteria
|
|
classes := getClasses(node)
|
|
if containsClass(classes, "insertr") && node.Data == elem.Tag {
|
|
// This might be our target node - inject content
|
|
e.injector.addContentAttributes(node, elem.ContentID, string(elem.Type))
|
|
|
|
if contentItem != nil {
|
|
switch elem.Type {
|
|
case parser.ContentText:
|
|
e.injector.injectTextContent(node, contentItem.Value)
|
|
case parser.ContentMarkdown:
|
|
e.injector.injectMarkdownContent(node, contentItem.Value)
|
|
case parser.ContentLink:
|
|
e.injector.injectLinkContent(node, contentItem.Value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recursively process children
|
|
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
e.findAndInjectNodes(child, elem, contentItem)
|
|
}
|
|
}
|
|
|
|
// Helper functions from parser package
|
|
func getClasses(node *html.Node) []string {
|
|
for _, attr := range node.Attr {
|
|
if attr.Key == "class" {
|
|
return strings.Fields(attr.Val)
|
|
}
|
|
}
|
|
return []string{}
|
|
}
|
|
|
|
func containsClass(classes []string, target string) bool {
|
|
for _, class := range classes {
|
|
if class == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EnhanceDirectory processes all HTML files in a directory
|
|
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())
|
|
}
|
|
|
|
// Handle HTML files
|
|
if strings.HasSuffix(strings.ToLower(path), ".html") {
|
|
return e.EnhanceFile(path, outputPath)
|
|
}
|
|
|
|
// Copy other files as-is
|
|
return e.copyFile(path, outputPath)
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// writeHTML writes an HTML document to a file
|
|
func (e *Enhancer) writeHTML(doc *html.Node, outputPath string) error {
|
|
// Create directory for output
|
|
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create output file
|
|
file, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
// Write HTML
|
|
return html.Render(file, doc)
|
|
}
|
|
|
|
// EnhanceInPlace performs in-place enhancement of static site files
|
|
func (e *Enhancer) EnhanceInPlace(sitePath string, siteID string) error {
|
|
// Update the injector with the correct siteID
|
|
e.injector.siteID = siteID
|
|
|
|
// Use existing parser logic to discover elements
|
|
result, err := e.parser.ParseDirectory(sitePath)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing directory: %w", err)
|
|
}
|
|
|
|
if len(result.Elements) == 0 {
|
|
fmt.Printf("📄 No insertr elements found in %s\n", sitePath)
|
|
return nil
|
|
}
|
|
|
|
// Group elements by file for efficient processing
|
|
fileElements := make(map[string][]parser.Element)
|
|
for _, elem := range result.Elements {
|
|
fileElements[elem.FilePath] = append(fileElements[elem.FilePath], elem)
|
|
}
|
|
|
|
// Process each file in-place
|
|
enhancedCount := 0
|
|
for filePath, elements := range fileElements {
|
|
if err := e.enhanceFileInPlace(filePath, elements); err != nil {
|
|
fmt.Printf("⚠️ Failed to enhance %s: %v\n", filepath.Base(filePath), err)
|
|
} else {
|
|
enhancedCount++
|
|
}
|
|
}
|
|
|
|
fmt.Printf("✅ Enhanced %d files with %d elements in site %s\n",
|
|
enhancedCount, len(result.Elements), siteID)
|
|
|
|
return nil
|
|
}
|
|
|
|
// enhanceFileInPlace modifies an HTML file in-place with database content
|
|
func (e *Enhancer) enhanceFileInPlace(filePath string, elements []parser.Element) error {
|
|
// Read original file
|
|
htmlContent, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("reading file: %w", err)
|
|
}
|
|
|
|
// Parse HTML
|
|
doc, err := html.Parse(strings.NewReader(string(htmlContent)))
|
|
if err != nil {
|
|
return fmt.Errorf("parsing HTML: %w", err)
|
|
}
|
|
|
|
// Convert parser elements to injector format with content IDs
|
|
elementIDs := make([]ElementWithID, 0, len(elements))
|
|
for _, elem := range elements {
|
|
// Find the corresponding node in the parsed document
|
|
node := e.findNodeInDocument(doc, elem)
|
|
if node != nil {
|
|
elementIDs = append(elementIDs, ElementWithID{
|
|
Element: &Element{
|
|
Node: node,
|
|
Type: string(elem.Type),
|
|
Tag: elem.Tag,
|
|
},
|
|
ContentID: elem.ContentID,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Use existing bulk injection logic for efficiency
|
|
if len(elementIDs) > 0 {
|
|
if err := e.injector.InjectBulkContent(elementIDs); err != nil {
|
|
return fmt.Errorf("injecting content: %w", err)
|
|
}
|
|
}
|
|
|
|
// Write enhanced HTML back to the same file (in-place update)
|
|
return e.writeHTML(doc, filePath)
|
|
}
|
|
|
|
// findNodeInDocument finds a specific node in the HTML document tree
|
|
func (e *Enhancer) findNodeInDocument(doc *html.Node, elem parser.Element) *html.Node {
|
|
// This is a simplified approach - in a production system we might need
|
|
// more sophisticated node matching based on attributes, position, etc.
|
|
return e.findNodeByTagAndClass(doc, elem.Tag, "insertr")
|
|
}
|
|
|
|
// findNodeByTagAndClass recursively searches for a node with specific tag and class
|
|
func (e *Enhancer) findNodeByTagAndClass(node *html.Node, targetTag, targetClass string) *html.Node {
|
|
if node.Type == html.ElementNode && node.Data == targetTag {
|
|
classes := getClasses(node)
|
|
if containsClass(classes, targetClass) {
|
|
return node
|
|
}
|
|
}
|
|
|
|
// Search children
|
|
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
if result := e.findNodeByTagAndClass(child, targetTag, targetClass); result != nil {
|
|
return result
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|