- Move all ContentItem, ContentClient, ContentResponse types to engine/types.go as single source of truth - Remove duplicate type definitions from content/types.go - Update all imports across codebase to use engine types - Enhance engine to extract existing data-content-id from HTML markup - Simplify frontend to always send html_markup, let server handle ID extraction/generation - Fix contentId reference errors in frontend error handling - Add getAttribute helper method to engine for ID extraction - Add GetAllContent method to engine.DatabaseClient - Update enhancer to use engine.ContentClient interface - All builds and API endpoints verified working This resolves the 400 Bad Request errors and creates a unified architecture where the server is the single source of truth for all ID generation and content type management.
214 lines
5.4 KiB
Go
214 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|