Implement complete content injection and enhancement pipeline

- Add content API client with HTTP and mock implementations
- Implement HTML content injection with database content replacement
- Create enhance command for build-time content injection
- Integrate enhancement with servedev for live development workflow
- Add editor asset injection and serving (/_insertr/ endpoints)
- Support on-the-fly HTML enhancement during development
- Enable complete 'Tailwind of CMS' workflow: parse → inject → serve
This commit is contained in:
2025-09-03 12:35:54 +02:00
parent 1f97acc1bf
commit 4407f84bbc
8 changed files with 1017 additions and 20 deletions

View File

@@ -0,0 +1,215 @@
package content
import (
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/net/html"
"github.com/insertr/cli/pkg/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
e.injector.InjectEditorAssets(doc, true)
// 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)
}