package content import ( "fmt" "log" "strings" "golang.org/x/net/html" ) // Injector handles content injection into HTML elements type Injector struct { client ContentClient siteID string mdProcessor *MarkdownProcessor } // NewInjector creates a new content injector func NewInjector(client ContentClient, siteID string) *Injector { return &Injector{ client: client, siteID: siteID, mdProcessor: NewMarkdownProcessor(), } } // InjectContent replaces element content with database values and adds content IDs func (i *Injector) InjectContent(element *Element, contentID string) error { // Fetch content from database/API contentItem, err := i.client.GetContent(i.siteID, contentID) if err != nil { return fmt.Errorf("fetching content for %s: %w", contentID, err) } // If no content found, keep original content but add data attributes if contentItem == nil { i.AddContentAttributes(element.Node, contentID, element.Type) return nil } // Replace element content based on type switch element.Type { case "text": i.injectTextContent(element.Node, contentItem.Value) case "markdown": i.injectMarkdownContent(element.Node, contentItem.Value) case "link": i.injectLinkContent(element.Node, contentItem.Value) default: i.injectTextContent(element.Node, contentItem.Value) } // Add data attributes for editor functionality i.AddContentAttributes(element.Node, contentID, element.Type) return nil } // InjectBulkContent efficiently injects multiple content items func (i *Injector) InjectBulkContent(elements []ElementWithID) error { // Extract content IDs for bulk fetch contentIDs := make([]string, len(elements)) for idx, elem := range elements { contentIDs[idx] = elem.ContentID } // Bulk fetch content contentMap, err := i.client.GetBulkContent(i.siteID, contentIDs) if err != nil { return fmt.Errorf("bulk fetching content: %w", err) } // Inject each element for _, elem := range elements { contentItem, exists := contentMap[elem.ContentID] // Add content attributes regardless i.AddContentAttributes(elem.Element.Node, elem.ContentID, elem.Element.Type) if !exists { // Keep original content if not found in database continue } // Replace content based on type switch elem.Element.Type { case "text": i.injectTextContent(elem.Element.Node, contentItem.Value) case "markdown": i.injectMarkdownContent(elem.Element.Node, contentItem.Value) case "link": i.injectLinkContent(elem.Element.Node, contentItem.Value) default: i.injectTextContent(elem.Element.Node, contentItem.Value) } } return nil } // injectTextContent replaces text content in an element func (i *Injector) injectTextContent(node *html.Node, content string) { // Remove all child nodes for child := node.FirstChild; child != nil; { next := child.NextSibling node.RemoveChild(child) child = next } // Add new text content textNode := &html.Node{ Type: html.TextNode, Data: content, } node.AppendChild(textNode) } // injectMarkdownContent handles markdown content - converts markdown to HTML func (i *Injector) injectMarkdownContent(node *html.Node, content string) { if content == "" { i.injectTextContent(node, "") return } // Convert markdown to HTML using server processor htmlContent, err := i.mdProcessor.ToHTML(content) if err != nil { log.Printf("⚠️ Markdown conversion failed for content '%s': %v, falling back to text", content, err) i.injectTextContent(node, content) return } // Inject the HTML content i.injectHTMLContent(node, htmlContent) } // injectLinkContent handles link/button content with URL extraction func (i *Injector) injectLinkContent(node *html.Node, content string) { // For now, just inject the text content // TODO: Parse content for URL and text components i.injectTextContent(node, content) } // injectHTMLContent safely injects HTML content into a DOM node // Preserves the original element and only replaces its content func (i *Injector) injectHTMLContent(node *html.Node, htmlContent string) { // Clear existing content but preserve the element itself i.clearNode(node) if htmlContent == "" { return } // Wrap content for safe parsing wrappedHTML := "
" + htmlContent + "
" // Parse HTML string doc, err := html.Parse(strings.NewReader(wrappedHTML)) if err != nil { log.Printf("Failed to parse HTML content '%s': %v, falling back to text", htmlContent, err) i.injectTextContent(node, htmlContent) return } // Find the wrapper div and move its children to target node wrapper := i.findElementByTag(doc, "div") if wrapper == nil { log.Printf("Could not find wrapper div in parsed HTML") return } // Move parsed nodes to target element (preserving original element) for child := wrapper.FirstChild; child != nil; { next := child.NextSibling wrapper.RemoveChild(child) node.AppendChild(child) child = next } } // clearNode removes all child nodes from a given node func (i *Injector) clearNode(node *html.Node) { for child := node.FirstChild; child != nil; { next := child.NextSibling node.RemoveChild(child) child = next } } // findElementByTag finds the first element with the specified tag name func (i *Injector) findElementByTag(node *html.Node, tag string) *html.Node { if node.Type == html.ElementNode && node.Data == tag { return node } for child := node.FirstChild; child != nil; child = child.NextSibling { if found := i.findElementByTag(child, tag); found != nil { return found } } return nil } // AddContentAttributes adds necessary data attributes and insertr class for editor functionality func (i *Injector) AddContentAttributes(node *html.Node, contentID string, contentType string) { i.setAttribute(node, "data-content-id", contentID) i.setAttribute(node, "data-content-type", contentType) i.addClass(node, "insertr") } // InjectEditorAssets adds editor JavaScript to HTML document func (i *Injector) InjectEditorAssets(doc *html.Node, isDevelopment bool, libraryScript string) { // TODO: Implement script injection strategy when we have CDN hosting // For now, script injection is disabled since HTML files should include their own script tags // Future options: // 1. Inject CDN script tag: // 2. Inject local script tag for development: // 3. Continue with inline injection for certain use cases // Currently disabled to avoid duplicate scripts return } // findHeadElement finds the element in the document func (i *Injector) findHeadElement(node *html.Node) *html.Node { if node.Type == html.ElementNode && node.Data == "head" { return node } for child := node.FirstChild; child != nil; child = child.NextSibling { if result := i.findHeadElement(child); result != nil { return result } } return nil } // setAttribute safely sets an attribute on an HTML node func (i *Injector) setAttribute(node *html.Node, key, value string) { // Remove existing attribute if present for idx, attr := range node.Attr { if attr.Key == key { node.Attr = append(node.Attr[:idx], node.Attr[idx+1:]...) break } } // Add new attribute node.Attr = append(node.Attr, html.Attribute{ Key: key, Val: value, }) } // addClass safely adds a class to an HTML node func (i *Injector) 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, }) } } // Element represents a parsed HTML element with metadata type Element struct { Node *html.Node Type string Tag string Classes []string Content string } // ElementWithID combines an element with its generated content ID type ElementWithID struct { Element *Element ContentID string }