package engine import ( "fmt" "strings" "golang.org/x/net/html" ) // AuthProvider represents authentication provider information type AuthProvider struct { Type string // "mock", "jwt", "authentik" } // ContentEngine is the unified content processing engine type ContentEngine struct { idGenerator *IDGenerator client ContentClient authProvider *AuthProvider injector *Injector } // NewContentEngine creates a new content processing engine func NewContentEngine(client ContentClient) *ContentEngine { authProvider := &AuthProvider{Type: "mock"} // default return &ContentEngine{ idGenerator: NewIDGenerator(), client: client, authProvider: authProvider, injector: NewInjector(client, ""), // siteID will be set per operation } } // NewContentEngineWithAuth creates a new content processing engine with auth config func NewContentEngineWithAuth(client ContentClient, authProvider *AuthProvider) *ContentEngine { if authProvider == nil { authProvider = &AuthProvider{Type: "mock"} } return &ContentEngine{ idGenerator: NewIDGenerator(), client: client, authProvider: authProvider, injector: NewInjectorWithAuth(client, "", authProvider), // siteID will be set per operation } } // ProcessContent processes HTML content according to the specified mode func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, error) { // 1. Parse HTML doc, err := html.Parse(strings.NewReader(string(input.HTML))) if err != nil { return nil, fmt.Errorf("parsing HTML: %w", err) } // 2. Find insertr and collection elements insertrElements, collectionElements := e.findEditableElements(doc) // 3. Process regular .insertr elements generatedIDs := make(map[string]string) processedElements := make([]ProcessedElement, len(insertrElements)) for i, elem := range insertrElements { // Generate structural ID (always deterministic) id := e.idGenerator.Generate(elem.Node, input.FilePath) // Database-first approach: Check if content already exists existingContent, err := e.client.GetContent(input.SiteID, id) contentExists := (err == nil && existingContent != nil) generatedIDs[fmt.Sprintf("element_%d", i)] = id processedElements[i] = ProcessedElement{ Node: elem.Node, ID: id, Generated: !contentExists, // Mark as generated only if new to database Tag: elem.Node.Data, Classes: GetClasses(elem.Node), } // Add/update content attributes to the node (only content-id now) e.addContentAttributes(elem.Node, id) // Store content only for truly new elements (database-first check) if !contentExists && (input.Mode == Enhancement || input.Mode == ContentInjection) { // Extract content and template from the unprocessed element htmlContent := e.extractHTMLContent(elem.Node) originalTemplate := e.extractOriginalTemplate(elem.Node) // Store in database via content client _, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, "system") if err != nil { // Log error but don't fail the enhancement - content just won't be stored fmt.Printf("⚠️ Failed to store content for %s: %v\n", id, err) } else { fmt.Printf("✅ Created new content: %s (html)\n", id) } } } // 4. Process .insertr-add collection elements for _, collectionElem := range collectionElements { // Generate structural ID for the collection container collectionID := e.idGenerator.Generate(collectionElem.Node, input.FilePath) // Add data-content-id attribute to the collection container e.setAttribute(collectionElem.Node, "data-content-id", collectionID) // Process collection during enhancement or content injection if input.Mode == Enhancement || input.Mode == ContentInjection { err := e.processCollection(collectionElem.Node, collectionID, input.SiteID) if err != nil { fmt.Printf("⚠️ Failed to process collection %s: %v\n", collectionID, err) } else { fmt.Printf("✅ Processed collection: %s\n", collectionID) } } } // 5. Inject content if required by mode if input.Mode == Enhancement || input.Mode == ContentInjection { err = e.injectContent(processedElements, input.SiteID) if err != nil { return nil, fmt.Errorf("injecting content: %w", err) } } // TODO: Implement collection-specific content injection here if needed // 6. Inject editor assets for enhancement mode (development) if input.Mode == Enhancement { injector := NewInjectorWithAuth(e.client, input.SiteID, e.authProvider) injector.InjectEditorAssets(doc, true, "") } return &ContentResult{ Document: doc, Elements: processedElements, GeneratedIDs: generatedIDs, }, nil } // InsertrElement represents an insertr element found in HTML type InsertrElement struct { Node *html.Node } // CollectionElement represents an insertr-add collection element found in HTML type CollectionElement struct { Node *html.Node } // findEditableElements finds all editable elements (.insertr and .insertr-add) func (e *ContentEngine) findEditableElements(doc *html.Node) ([]InsertrElement, []CollectionElement) { var insertrElements []InsertrElement var collectionElements []CollectionElement var containersToTransform []*html.Node // First pass: find all .insertr and .insertr-add elements e.walkNodes(doc, func(n *html.Node) { if n.Type == html.ElementNode { if e.hasInsertrClass(n) { if isContainer(n) { // Container element - mark for transformation containersToTransform = append(containersToTransform, n) } else { // Regular element - add directly insertrElements = append(insertrElements, InsertrElement{ Node: n, }) } } else if e.hasInsertrAddClass(n) { // Collection element - add directly (no container transformation for collections) collectionElements = append(collectionElements, CollectionElement{ Node: n, }) } } }) // Second pass: transform .insertr containers (remove .insertr from container, add to children) for _, container := range containersToTransform { // Remove .insertr class from container e.removeClass(container, "insertr") // Find viable children and add .insertr class to them viableChildren := FindViableChildren(container) for _, child := range viableChildren { e.addClass(child, "insertr") insertrElements = append(insertrElements, InsertrElement{ Node: child, }) } } return insertrElements, collectionElements } // findInsertrElements finds all elements with class="insertr" and applies container transformation // This implements the "syntactic sugar transformation" from CLASSES.md: // - Containers with .insertr get their .insertr class removed // - Viable children of those containers get .insertr class added // - Regular elements with .insertr are kept as-is func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement { insertrElements, _ := e.findEditableElements(doc) return insertrElements } // walkNodes walks through all nodes in the document func (e *ContentEngine) walkNodes(n *html.Node, fn func(*html.Node)) { fn(n) for c := n.FirstChild; c != nil; c = c.NextSibling { e.walkNodes(c, fn) } } // hasInsertrClass checks if node has class="insertr" func (e *ContentEngine) hasInsertrClass(node *html.Node) bool { classes := GetClasses(node) for _, class := range classes { if class == "insertr" { return true } } return false } // hasInsertrAddClass checks if node has class="insertr-add" (collection) func (e *ContentEngine) hasInsertrAddClass(node *html.Node) bool { classes := GetClasses(node) for _, class := range classes { if class == "insertr-add" { return true } } return false } // addContentAttributes adds data-content-id attribute only // HTML-first approach: no content-type attribute needed func (e *ContentEngine) addContentAttributes(node *html.Node, contentID string) { // Add data-content-id attribute e.setAttribute(node, "data-content-id", contentID) } // getAttribute gets an attribute value from an HTML node func (e *ContentEngine) getAttribute(node *html.Node, key string) string { for _, attr := range node.Attr { if attr.Key == key { return attr.Val } } return "" } // setAttribute sets an attribute on an HTML node func (e *ContentEngine) setAttribute(node *html.Node, key, value string) { // Remove existing attribute if it exists for i, attr := range node.Attr { if attr.Key == key { node.Attr[i].Val = value return } } // Add new attribute node.Attr = append(node.Attr, html.Attribute{ Key: key, Val: value, }) } // addClass safely adds a class to an HTML node func (e *ContentEngine) addClass(node *html.Node, className string) { var classAttr *html.Attribute var classIndex int = -1 // Find existing class attribute for idx, attr := range node.Attr { if attr.Key == "class" { classAttr = &attr classIndex = idx break } } var classes []string if classAttr != nil { classes = strings.Fields(classAttr.Val) } // Check if class already exists for _, class := range classes { if class == className { return // Class already exists } } // Add new class classes = append(classes, className) newClassValue := strings.Join(classes, " ") if classIndex >= 0 { // Update existing class attribute node.Attr[classIndex].Val = newClassValue } else { // Add new class attribute node.Attr = append(node.Attr, html.Attribute{ Key: "class", Val: newClassValue, }) } } // removeClass safely removes a class from an HTML node func (e *ContentEngine) removeClass(node *html.Node, className string) { var classIndex int = -1 // Find existing class attribute for idx, attr := range node.Attr { if attr.Key == "class" { classIndex = idx break } } if classIndex == -1 { return // No class attribute found } // Parse existing classes classes := strings.Fields(node.Attr[classIndex].Val) // Filter out the target class var newClasses []string for _, class := range classes { if class != className { newClasses = append(newClasses, class) } } // Update or remove class attribute if len(newClasses) == 0 { // Remove class attribute entirely if no classes remain node.Attr = append(node.Attr[:classIndex], node.Attr[classIndex+1:]...) } else { // Update class attribute with remaining classes node.Attr[classIndex].Val = strings.Join(newClasses, " ") } } // injectContent injects content from database into elements func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string) error { for i := range elements { elem := &elements[i] // Try to get content from database contentItem, err := e.client.GetContent(siteID, elem.ID) if err != nil { // Content not found is not an error - element just won't have injected content continue } if contentItem != nil { // Inject the content into the element elem.Content = contentItem.HTMLContent // Update injector siteID for this operation e.injector.siteID = siteID e.injector.injectHTMLContent(elem.Node, contentItem.HTMLContent) } } return nil } // extractHTMLContent extracts the inner HTML content from a node func (e *ContentEngine) extractHTMLContent(node *html.Node) string { var content strings.Builder // Render all child nodes in order to preserve HTML structure for child := node.FirstChild; child != nil; child = child.NextSibling { if err := html.Render(&content, child); err == nil { // All nodes (text and element) rendered in correct order } } return strings.TrimSpace(content.String()) } // extractOriginalTemplate extracts the outer HTML of the element (including the element itself) func (e *ContentEngine) extractOriginalTemplate(node *html.Node) string { var buf strings.Builder if err := html.Render(&buf, node); err != nil { return "" } return buf.String() } // processCollection handles collection detection, persistence and reconstruction func (e *ContentEngine) processCollection(collectionNode *html.Node, collectionID, siteID string) error { // 1. Check if collection exists in database existingCollection, err := e.client.GetCollection(siteID, collectionID) collectionExists := (err == nil && existingCollection != nil) if !collectionExists { // 2. New collection: extract container HTML and create collection record containerHTML := e.extractOriginalTemplate(collectionNode) _, err := e.client.CreateCollection(siteID, collectionID, containerHTML, "system") if err != nil { return fmt.Errorf("failed to create collection %s: %w", collectionID, err) } // 3. Extract templates and store initial items from existing children err = e.extractAndStoreTemplatesAndItems(collectionNode, collectionID, siteID) if err != nil { return fmt.Errorf("failed to extract templates and items for collection %s: %w", collectionID, err) } fmt.Printf("✅ Created new collection: %s with templates and initial items\n", collectionID) } else { // 4. Database-first approach: Check if collection items already exist existingItems, err := e.client.GetCollectionItems(siteID, collectionID) if err != nil { return fmt.Errorf("failed to check existing collection items: %w", err) } if len(existingItems) == 0 { // 5. Collection exists but no items - store original children as initial items err = e.storeInitialCollectionItems(collectionNode, collectionID, siteID) if err != nil { return fmt.Errorf("failed to store initial collection items for %s: %w", collectionID, err) } fmt.Printf("✅ Stored initial items for existing collection: %s\n", collectionID) } else { // 6. Items exist: reconstruct from database (normal case) err = e.reconstructCollectionItems(collectionNode, collectionID, siteID) if err != nil { return fmt.Errorf("failed to reconstruct collection %s: %w", collectionID, err) } fmt.Printf("✅ Reconstructed collection: %s from database (%d items)\n", collectionID, len(existingItems)) } } return nil } // extractAndStoreTemplatesAndItems extracts templates and stores initial items from existing collection children func (e *ContentEngine) extractAndStoreTemplatesAndItems(collectionNode *html.Node, collectionID, siteID string) error { // Find existing children elements to use as templates var templateElements []*html.Node // Walk through direct children of the collection for child := collectionNode.FirstChild; child != nil; child = child.NextSibling { if child.Type == html.ElementNode { templateElements = append(templateElements, child) } } if len(templateElements) == 0 { // No existing children - create a default empty template _, err := e.client.CreateCollectionTemplate(siteID, collectionID, "default", "