package engine import ( "context" "fmt" "strings" "github.com/insertr/insertr/internal/db" "golang.org/x/net/html" ) // CollectionElement represents an insertr-add collection element found in HTML type CollectionElement struct { Node *html.Node } // hasInsertrAddClass checks if node has class="insertr-add" (collection) func (e *ContentEngine) hasInsertrAddClass(node *html.Node) bool { classes := GetClasses(node) return ContainsClass(classes, "insertr-add") } // 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(context.Background(), 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(context.Background(), 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. Existing collection: Always reconstruct from database (database is source of truth) err = e.reconstructCollectionItems(collectionNode, collectionID, siteID) if err != nil { return fmt.Errorf("failed to reconstruct collection %s: %w", collectionID, err) } // Optional: Show item count for feedback existingItems, _ := e.client.GetCollectionItems(context.Background(), siteID, collectionID) 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 { var templateIDs []int templateCount := 0 // Walk through direct children of the collection for child := collectionNode.FirstChild; child != nil; child = child.NextSibling { if child.Type == html.ElementNode { templateCount++ } } // If no templates found, create a default template if templateCount == 0 { _, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, "default", "
New item
", true) if err != nil { return fmt.Errorf("failed to create default template: %w", err) } fmt.Printf("✅ Created default template for collection %s\n", collectionID) return nil } // Create templates for each unique child structure templateIndex := 0 for child := collectionNode.FirstChild; child != nil; child = child.NextSibling { if child.Type == html.ElementNode { templateName := fmt.Sprintf("template_%d", templateIndex+1) templateHTML := e.extractCleanTemplate(child) isDefault := templateIndex == 0 template, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, templateName, templateHTML, isDefault) if err != nil { return fmt.Errorf("failed to create template %s: %w", templateName, err) } templateIDs = append(templateIDs, template.TemplateID) templateIndex++ fmt.Printf("✅ Created template '%s' for collection %s\n", templateName, collectionID) } } // Store original children as initial collection items (database-first approach) err := e.storeChildrenAsCollectionItems(collectionNode, collectionID, siteID, templateIDs) if err != nil { return fmt.Errorf("failed to store initial collection items: %w", err) } // Clear HTML children and reconstruct from database (ensures consistency) err = e.reconstructCollectionItems(collectionNode, collectionID, siteID) if err != nil { return fmt.Errorf("failed to reconstruct initial collection items: %w", err) } return nil } // reconstructCollectionItems rebuilds collection items from database and adds them to DOM func (e *ContentEngine) reconstructCollectionItems(collectionNode *html.Node, collectionID, siteID string) error { // Get all items for this collection from database items, err := e.client.GetCollectionItems(context.Background(), siteID, collectionID) if err != nil { return fmt.Errorf("failed to get collection items: %w", err) } // Get templates for this collection templates, err := e.client.GetCollectionTemplates(context.Background(), siteID, collectionID) if err != nil { return fmt.Errorf("failed to get collection templates: %w", err) } // Build template lookup for efficiency templateLookup := make(map[int]*db.CollectionTemplateItem) for _, template := range templates { templateLookup[template.TemplateID] = &template } // Clear existing children from the collection node for child := collectionNode.FirstChild; child != nil; { next := child.NextSibling collectionNode.RemoveChild(child) child = next } // Reconstruct items in order from database for _, item := range items { _, exists := templateLookup[item.TemplateID] if !exists { fmt.Printf("⚠️ Template %d not found for item %s, skipping\n", item.TemplateID, item.ItemID) continue } // Parse the stored structural template HTML structuralDoc, err := html.Parse(strings.NewReader(item.HTMLContent)) if err != nil { fmt.Printf("⚠️ Failed to parse structural template for item %s: %v\n", item.ItemID, err) continue } // Find the body and extract its children (stored as complete structure) var structuralChild *html.Node e.walkNodes(structuralDoc, func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "body" { // Get the first element child of body for child := n.FirstChild; child != nil; child = child.NextSibling { if child.Type == html.ElementNode { structuralChild = child break } } } }) if structuralChild != nil { // Remove from its current parent before adding to collection if structuralChild.Parent != nil { structuralChild.Parent.RemoveChild(structuralChild) } // Add hydrated structural elements directly to collection (stored HTML has complete structure) // The structural template already contains hydrated content from database // Inject data-item-id attribute for collection item identification if structuralChild.Type == html.ElementNode { SetAttribute(structuralChild, "data-item-id", item.ItemID) } collectionNode.AppendChild(structuralChild) } } fmt.Printf("✅ Reconstructed %d items for collection %s\n", len(items), collectionID) return nil } // processChildElementsAsContent processes .insertr elements within a collection child and stores them as individual content func (e *ContentEngine) processChildElementsAsContent(childElement *html.Node, siteID, itemID string) ([]ContentEntry, error) { var contentEntries []ContentEntry // Walk through the child element and find .insertr elements e.walkNodes(childElement, func(n *html.Node) { if n.Type == html.ElementNode && e.hasClass(n, "insertr") { // Generate content ID for this .insertr element contentID := e.idGenerator.Generate(n, "collection-item") // Extract the content htmlContent := e.extractHTMLContent(n) template := e.extractCleanTemplate(n) // Store content entry contentEntries = append(contentEntries, ContentEntry{ ID: contentID, SiteID: siteID, HTMLContent: htmlContent, Template: template, }) // Set the data-content-id attribute SetAttribute(n, "data-content-id", contentID) // Clear content - this will be hydrated during reconstruction for child := n.FirstChild; child != nil; { next := child.NextSibling n.RemoveChild(child) child = next } } }) return contentEntries, nil } // generateStructuralTemplateFromChild creates a structural template with placeholders for content func (e *ContentEngine) generateStructuralTemplateFromChild(childElement *html.Node, contentEntries []ContentEntry) (string, error) { // Clone the child to avoid modifying the original clonedChild := e.cloneNode(childElement) // Walk through and replace .insertr content with data-content-id attributes entryIndex := 0 e.walkNodes(clonedChild, func(n *html.Node) { if n.Type == html.ElementNode && e.hasClass(n, "insertr") { if entryIndex < len(contentEntries) { // Set the data-content-id attribute SetAttribute(n, "data-content-id", contentEntries[entryIndex].ID) // Clear content - this will be hydrated during reconstruction for child := n.FirstChild; child != nil; { next := child.NextSibling n.RemoveChild(child) child = next } entryIndex++ } } }) // Generate HTML for the structural template var buf strings.Builder if err := html.Render(&buf, clonedChild); err != nil { return "", fmt.Errorf("failed to render structural template: %w", err) } return buf.String(), nil } // createVirtualElementFromTemplate creates a virtual DOM element from template HTML func (e *ContentEngine) createVirtualElementFromTemplate(templateHTML string) (*html.Node, error) { // Parse template HTML into a virtual DOM templateDoc, err := html.Parse(strings.NewReader(templateHTML)) if err != nil { return nil, fmt.Errorf("failed to parse template HTML: %w", err) } // Find the first element in the body var templateElement *html.Node e.walkNodes(templateDoc, func(n *html.Node) { if templateElement == nil && n.Type == html.ElementNode && n.Data != "html" && n.Data != "head" && n.Data != "body" { templateElement = n } }) if templateElement == nil { return nil, fmt.Errorf("no valid element found in template HTML") } return templateElement, nil } // CreateCollectionItemFromTemplate creates a collection item using the unified engine approach func (e *ContentEngine) CreateCollectionItemFromTemplate( siteID, collectionID string, templateID int, templateHTML string, lastEditedBy string, ) (*db.CollectionItemWithTemplate, error) { // Create virtual element from template for ID generation virtualElement, err := e.createVirtualElementFromTemplate(templateHTML) if err != nil { return nil, fmt.Errorf("failed to create virtual element: %w", err) } // Generate unique item ID using unified generator with collection context itemID := e.idGenerator.Generate(virtualElement, "collection-item") // Process any .insertr elements within the template and store as content contentEntries, err := e.processChildElementsAsContent(virtualElement, siteID, itemID) if err != nil { return nil, fmt.Errorf("failed to process child elements: %w", err) } // Store individual content entries in content table for _, entry := range contentEntries { _, err := e.client.CreateContent(context.Background(), entry.SiteID, entry.ID, entry.HTMLContent, entry.Template, lastEditedBy) if err != nil { return nil, fmt.Errorf("failed to create content entry %s: %w", entry.ID, err) } } // Generate structural template for the collection item structuralTemplate, err := e.generateStructuralTemplateFromChild(virtualElement, contentEntries) if err != nil { return nil, fmt.Errorf("failed to generate structural template: %w", err) } // Create collection item with structural template collectionItem, err := e.client.CreateCollectionItem(context.Background(), siteID, collectionID, itemID, templateID, structuralTemplate, 0, lastEditedBy, ) if err != nil { return nil, fmt.Errorf("failed to create collection item: %w", err) } return collectionItem, nil } // storeChildrenAsCollectionItems stores HTML children as collection items in database func (e *ContentEngine) storeChildrenAsCollectionItems(collectionNode *html.Node, collectionID, siteID string, templateIDs []int) error { var childElements []*html.Node // Walk through direct children of the collection for child := collectionNode.FirstChild; child != nil; child = child.NextSibling { if child.Type == html.ElementNode { childElements = append(childElements, child) } } if len(childElements) == 0 { fmt.Printf("ℹ️ No children found to store as collection items for %s\n", collectionID) return nil } // Store each child as a collection item for i, childElement := range childElements { // Use corresponding template ID, or default to first template templateID := templateIDs[0] // Default to first template if i < len(templateIDs) { templateID = templateIDs[i] } // Generate item ID using unified generator with collection context itemID := e.idGenerator.Generate(childElement, "collection-item") // Process any .insertr elements within this child and store as content contentEntries, err := e.processChildElementsAsContent(childElement, siteID, itemID) if err != nil { return fmt.Errorf("failed to process child elements: %w", err) } // Store individual content entries in content table for _, entry := range contentEntries { _, err := e.client.CreateContent(context.Background(), entry.SiteID, entry.ID, entry.HTMLContent, entry.Template, "system") if err != nil { return fmt.Errorf("failed to create content entry %s: %w", entry.ID, err) } } // Generate structural template for this collection item structuralTemplate, err := e.generateStructuralTemplateFromChild(childElement, contentEntries) if err != nil { return fmt.Errorf("failed to generate structural template: %w", err) } // Store structural template in collection_items (content lives in content table) _, err = e.client.CreateCollectionItem(context.Background(), siteID, collectionID, itemID, templateID, structuralTemplate, i+1, "system") if err != nil { return fmt.Errorf("failed to create collection item %s: %w", itemID, err) } fmt.Printf("✅ Stored initial collection item: %s (template %d) with %d content entries\n", itemID, templateID, len(contentEntries)) } return nil } // collectionProcessor handles collection-specific processing logic type collectionProcessor struct { engine *ContentEngine } // newCollectionProcessor creates a collection processor func (e *ContentEngine) newCollectionProcessor() *collectionProcessor { return &collectionProcessor{engine: e} } // process handles the full collection processing workflow func (cp *collectionProcessor) process(collectionNode *html.Node, collectionID, siteID string) error { return cp.engine.processCollection(collectionNode, collectionID, siteID) } // extractAndStoreTemplatesAndItems delegates to engine method func (cp *collectionProcessor) extractAndStoreTemplatesAndItems(collectionNode *html.Node, collectionID, siteID string) error { return cp.engine.extractAndStoreTemplatesAndItems(collectionNode, collectionID, siteID) } // reconstructItems delegates to engine method func (cp *collectionProcessor) reconstructItems(collectionNode *html.Node, collectionID, siteID string) error { return cp.engine.reconstructCollectionItems(collectionNode, collectionID, siteID) } // cloneNode creates a deep copy of an HTML node func (e *ContentEngine) cloneNode(node *html.Node) *html.Node { cloned := &html.Node{ Type: node.Type, Data: node.Data, DataAtom: node.DataAtom, Namespace: node.Namespace, } // Clone attributes for _, attr := range node.Attr { cloned.Attr = append(cloned.Attr, html.Attribute{ Namespace: attr.Namespace, Key: attr.Key, Val: attr.Val, }) } // Clone children recursively for child := node.FirstChild; child != nil; child = child.NextSibling { clonedChild := e.cloneNode(child) cloned.AppendChild(clonedChild) } return cloned }