package engine import ( "fmt" "strings" "golang.org/x/net/html" ) // ContentEngine is the unified content processing engine type ContentEngine struct { idGenerator *IDGenerator client ContentClient } // NewContentEngine creates a new content processing engine func NewContentEngine(client ContentClient) *ContentEngine { return &ContentEngine{ idGenerator: NewIDGenerator(), client: client, } } // 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 elements elements := e.findInsertrElements(doc) // 3. Generate IDs for elements generatedIDs := make(map[string]string) processedElements := make([]ProcessedElement, len(elements)) for i, elem := range elements { // Check if element already has a data-content-id existingID := e.getAttribute(elem.Node, "data-content-id") var id string var wasGenerated bool if existingID != "" { // Use existing ID from enhanced element id = existingID wasGenerated = false } else { // Generate new ID for unprocessed element id = e.idGenerator.Generate(elem.Node, input.FilePath) wasGenerated = true } generatedIDs[fmt.Sprintf("element_%d", i)] = id processedElements[i] = ProcessedElement{ Node: elem.Node, ID: id, Type: elem.Type, Generated: wasGenerated, Tag: elem.Node.Data, Classes: GetClasses(elem.Node), } // Add/update content attributes to the node e.addContentAttributes(elem.Node, id, elem.Type) } // 4. 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) } } // 5. Inject editor assets for enhancement mode (development) if input.Mode == Enhancement { injector := NewInjector(e.client, input.SiteID) 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 Type string } // findInsertrElements finds all elements with class="insertr" func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement { var elements []InsertrElement e.walkNodes(doc, func(n *html.Node) { if n.Type == html.ElementNode && e.hasInsertrClass(n) { elementType := e.determineContentType(n) elements = append(elements, InsertrElement{ Node: n, Type: elementType, }) } }) return elements } // 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 } // determineContentType determines the content type based on element func (e *ContentEngine) determineContentType(node *html.Node) string { tag := strings.ToLower(node.Data) switch tag { case "a", "button": return "link" case "h1", "h2", "h3", "h4", "h5", "h6": return "text" case "p", "div", "section", "article", "span": return "markdown" default: return "text" } } // addContentAttributes adds data-content-id and data-content-type attributes func (e *ContentEngine) addContentAttributes(node *html.Node, contentID, contentType string) { // Add data-content-id attribute e.setAttribute(node, "data-content-id", contentID) // Add data-content-type attribute e.setAttribute(node, "data-content-type", contentType) } // 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, }) } // 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.Value e.injectContentIntoNode(elem.Node, contentItem.Value, contentItem.Type) } } return nil } // injectContentIntoNode injects content value into an HTML node func (e *ContentEngine) injectContentIntoNode(node *html.Node, content, contentType string) { // Clear existing text content for child := node.FirstChild; child != nil; { next := child.NextSibling if child.Type == html.TextNode { node.RemoveChild(child) } child = next } // Add new text content textNode := &html.Node{ Type: html.TextNode, Data: content, } node.AppendChild(textNode) }