Files
insertr/internal/engine/engine.go
Joakim 1ae4176f23 Implement atomic collection item creation API with unified content engine approach
Updates collection creation to use database-first atomic operations for reliable collection item management. Replaces manual database calls with unified content engine methods that handle content extraction, storage, and structural template generation consistently.

Key changes:
- Replace manual database operations in CreateCollectionItem handler with DatabaseClient.CreateCollectionItemAtomic()
- Implement unified content engine approach for API-based collection item creation
- Add atomic collection item creation methods across all content clients
- Enhance reconstruction to use stored structural templates with content ID hydration
- Add comprehensive collection management API methods in JavaScript client
- Implement collection manager UI with create, delete, and reorder functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 18:39:37 +02:00

871 lines
28 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package engine
import (
"fmt"
"strings"
"time"
"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())
}
// extractTextContent extracts only the text content from a node (for individual content storage)
func (e *ContentEngine) extractTextContent(node *html.Node) string {
var text strings.Builder
// Walk through all text nodes to extract content
e.walkNodes(node, func(n *html.Node) {
if n.Type == html.TextNode {
text.WriteString(n.Data)
}
})
return strings.TrimSpace(text.String())
}
// getAttributeValue gets an attribute value from an HTML node
func (e *ContentEngine) getAttributeValue(n *html.Node, attrKey string) string {
for _, attr := range n.Attr {
if attr.Key == attrKey {
return attr.Val
}
}
return ""
}
// 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()
}
// extractCleanTemplate extracts a clean template without data-content-id attributes and with placeholder content
func (e *ContentEngine) extractCleanTemplate(node *html.Node) string {
// Clone the node to avoid modifying the original
clonedNode := e.cloneNode(node)
// Remove all data-content-id attributes and replace content with placeholders
e.walkNodes(clonedNode, func(n *html.Node) {
if n.Type == html.ElementNode {
// Remove data-content-id attribute
e.removeAttribute(n, "data-content-id")
// If this is an .insertr element, replace content with placeholder
if e.hasClass(n, "insertr") {
placeholderText := e.getPlaceholderForElement(n.Data)
// Clear existing children and add placeholder text
for child := n.FirstChild; child != nil; {
next := child.NextSibling
n.RemoveChild(child)
child = next
}
n.AppendChild(&html.Node{
Type: html.TextNode,
Data: placeholderText,
})
}
}
})
var buf strings.Builder
if err := html.Render(&buf, clonedNode); err != nil {
return ""
}
return buf.String()
}
// removeAttribute removes an attribute from an HTML node
func (e *ContentEngine) removeAttribute(n *html.Node, key string) {
for i, attr := range n.Attr {
if attr.Key == key {
n.Attr = append(n.Attr[:i], n.Attr[i+1:]...)
break
}
}
}
// hasClass checks if an HTML node has a specific class
func (e *ContentEngine) hasClass(n *html.Node, className string) bool {
for _, attr := range n.Attr {
if attr.Key == "class" {
classes := strings.Fields(attr.Val)
for _, class := range classes {
if class == className {
return true
}
}
}
}
return false
}
// getPlaceholderForElement returns appropriate placeholder text for an element type
func (e *ContentEngine) getPlaceholderForElement(elementType string) string {
placeholders := map[string]string{
"blockquote": "Enter your quote here...",
"cite": "Enter author name...",
"h1": "Enter heading...",
"h2": "Enter heading...",
"h3": "Enter heading...",
"p": "Enter text...",
"span": "Enter text...",
"div": "Enter content...",
"a": "Enter link text...",
}
if placeholder, exists := placeholders[elementType]; exists {
return placeholder
}
return "Enter content..."
}
// 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. 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)
}
// Get final item count for logging
existingItems, _ := e.client.GetCollectionItems(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 {
// 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", "<div>New item</div>", 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
}
// Extract templates from existing children and store them
var templateIDs []int
for i, templateElement := range templateElements {
templateHTML := e.extractCleanTemplate(templateElement)
templateName := fmt.Sprintf("template-%d", i+1)
isDefault := (i == 0) // First template is default
template, err := e.client.CreateCollectionTemplate(siteID, collectionID, templateName, templateHTML, isDefault)
if err != nil {
return fmt.Errorf("failed to create template %s: %w", templateName, err)
}
templateIDs = append(templateIDs, template.TemplateID)
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)
}
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(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(siteID, collectionID)
if err != nil {
return fmt.Errorf("failed to get collection templates: %w", err)
}
// Create a template map for quick lookup
templateMap := make(map[int]string)
for _, template := range templates {
templateMap[template.TemplateID] = template.HTMLTemplate
}
// Clear existing children (they will be replaced with database items)
for child := collectionNode.FirstChild; child != nil; {
next := child.NextSibling
collectionNode.RemoveChild(child)
child = next
}
// Add items from database in position order using unified .insertr approach
for _, item := range items {
// Parse the stored structural HTML with content IDs (no template needed for reconstruction)
structuralDoc, err := html.Parse(strings.NewReader(item.HTMLContent))
if err != nil {
fmt.Printf("⚠️ Failed to parse stored HTML for %s: %v\n", item.ItemID, err)
continue
}
var structuralBody *html.Node
e.walkNodes(structuralDoc, func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "body" {
structuralBody = n
}
})
if structuralBody != nil {
// Process each .insertr element using Injector pattern (unified approach)
injector := NewInjector(e.client, siteID)
// Walk through structural elements and hydrate with content from content table
e.walkNodes(structuralBody, func(n *html.Node) {
if n.Type == html.ElementNode && e.hasClass(n, "insertr") {
// Get content ID from data attribute
contentID := e.getAttributeValue(n, "data-content-id")
if contentID != "" {
// Use Injector to hydrate content (unified .insertr approach)
element := &Element{Node: n, Type: "html"}
err := injector.InjectContent(element, contentID)
if err != nil {
fmt.Printf("⚠️ Failed to inject content for %s: %v\n", contentID, err)
}
}
}
})
// Add hydrated structural elements directly to collection (stored HTML has complete structure)
for structuralChild := structuralBody.FirstChild; structuralChild != nil; {
next := structuralChild.NextSibling
structuralBody.RemoveChild(structuralChild)
collectionNode.AppendChild(structuralChild)
structuralChild = next
}
}
}
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
elementIndex := 0
// Walk through child element to find .insertr elements
e.walkNodes(childElement, func(n *html.Node) {
if n.Type == html.ElementNode && e.hasClass(n, "insertr") {
// Use core IDGenerator for unified ID generation (like individual .insertr elements)
contentID := e.idGenerator.Generate(n, "collection-item")
// Extract actual content from the element
actualContent := e.extractTextContent(n)
// Store as individual content entry (unified .insertr approach)
_, err := e.client.CreateContent(siteID, contentID, actualContent, "", "system")
if err != nil {
fmt.Printf("⚠️ Failed to create content %s: %v\n", contentID, err)
return
}
// Add to content entries for structural template generation
contentEntries = append(contentEntries, ContentEntry{
ID: contentID,
SiteID: siteID,
HTMLContent: actualContent,
Template: e.extractOriginalTemplate(n),
})
elementIndex++
}
})
return contentEntries, nil
}
// generateStructuralTemplateFromChild creates structure-only HTML from child element with content IDs
func (e *ContentEngine) generateStructuralTemplateFromChild(childElement *html.Node, contentEntries []ContentEntry) (string, error) {
// Clone the child element to avoid modifying original
clonedChild := e.cloneNode(childElement)
entryIndex := 0
// Walk through cloned element and replace content with content IDs
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
e.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++
}
}
})
// Render the complete structural template including container
var sb strings.Builder
html.Render(&sb, clonedChild)
return sb.String(), nil
}
// createVirtualElementFromTemplate creates a virtual DOM element from template HTML for API usage
// This allows API path to use the same structure extraction as enhancement path
func (e *ContentEngine) createVirtualElementFromTemplate(templateHTML string) (*html.Node, error) {
// Parse the template HTML
templateDoc, err := html.Parse(strings.NewReader(templateHTML))
if err != nil {
return nil, fmt.Errorf("failed to parse template HTML: %w", err)
}
// Find the body element and extract the template structure
var templateBody *html.Node
e.walkNodes(templateDoc, func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "body" {
templateBody = n
}
})
if templateBody != nil && templateBody.FirstChild != nil {
// Return the first child of body (the actual template element)
return templateBody.FirstChild, nil
}
return nil, fmt.Errorf("template does not contain valid structure")
}
// CreateCollectionItemFromTemplate creates a collection item using the unified engine approach
// This replaces TemplateProcessor with engine-native functionality
func (e *ContentEngine) CreateCollectionItemFromTemplate(
siteID, collectionID string,
templateID int,
templateHTML string,
lastEditedBy string,
) (*CollectionItemWithTemplate, error) {
// Generate unique item ID
itemID := fmt.Sprintf("%s-item-%d", collectionID, time.Now().Unix())
// Create virtual element from template (like enhancement path)
virtualElement, err := e.createVirtualElementFromTemplate(templateHTML)
if err != nil {
return nil, fmt.Errorf("failed to create virtual element: %w", err)
}
// Process .insertr elements and create content entries (unified approach)
contentEntries, err := e.processChildElementsAsContent(virtualElement, siteID, itemID)
if err != nil {
return nil, fmt.Errorf("failed to process content entries: %w", err)
}
// Generate structural template using unified engine method
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(
siteID, collectionID, itemID, templateID, structuralTemplate, 0, lastEditedBy,
)
if err != nil {
return nil, fmt.Errorf("failed to create collection item: %w", err)
}
return collectionItem, nil
}
// 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
}
// storeChildrenAsCollectionItems stores HTML children as collection items in database
func (e *ContentEngine) storeChildrenAsCollectionItems(collectionNode *html.Node, collectionID, siteID string, templateIDs []int) error {
// Find existing children elements to store as items
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 using unified .insertr approach (content table + structural template)
for i, childElement := range childElements {
// Generate item ID (like content ID generation)
itemID := fmt.Sprintf("%s-initial-%d", collectionID, i+1)
// Process .insertr elements within this child (unified approach)
contentEntries, err := e.processChildElementsAsContent(childElement, siteID, itemID)
if err != nil {
return fmt.Errorf("failed to process content for item %s: %w", itemID, err)
}
// Generate structural template with content IDs (no actual content)
structuralTemplate, err := e.generateStructuralTemplateFromChild(childElement, contentEntries)
if err != nil {
return fmt.Errorf("failed to generate structural template for item %s: %w", itemID, err)
}
// Use appropriate template ID (cycle through available templates)
templateID := templateIDs[i%len(templateIDs)]
// Store structural template in collection_items (content lives in content table)
_, err = e.client.CreateCollectionItem(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
}