Critical fixes: - Fixed HTML injection to preserve original element attributes, classes, and styling - Updated markdown processor to generate inline content instead of wrapped paragraphs - Enhanced content type handling: database type now takes precedence over parser detection - Eliminated nested <p> tags issue that was causing invalid HTML Key improvements: - Elements like <p class='lead insertr' style='color: blue;'> now maintain all attributes - Markdown **bold**, *italic*, [links](url) inject as inline formatted content - Database content type (markdown/text/link) overrides parser auto-detection - Clean HTML output without structural corruption Before: <p class='lead'><p>**bold**</p></p> (broken) After: <p class='lead'>**bold text**</p> (clean) Server remains source of truth for markdown processing with zero runtime overhead.
313 lines
8.4 KiB
Go
313 lines
8.4 KiB
Go
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 := "<div>" + htmlContent + "</div>"
|
|
|
|
// 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: <script src="https://cdn.jsdelivr.net/npm/@insertr/lib@1.0.0/dist/insertr.js"></script>
|
|
// 2. Inject local script tag for development: <script src="/insertr/insertr.js"></script>
|
|
// 3. Continue with inline injection for certain use cases
|
|
|
|
// Currently disabled to avoid duplicate scripts
|
|
return
|
|
}
|
|
|
|
// findHeadElement finds the <head> 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
|
|
}
|