fix: systematic element matching bug in enhancement pipeline

- Problem: Element ID collisions between similar elements (logo h1 vs hero h1)
  causing content to be injected into wrong elements
- Root cause: Enhancer used naive tag+class matching instead of parser's
  sophisticated semantic analysis for element identification

Systematic solution:
- Enhanced parser architecture with exported utilities (GetClasses, ContainsClass)
- Added FindElementInDocument() with content-based semantic matching
- Replaced naive findAndInjectNodes() with parser-based element matching
- Removed code duplication between parser and enhancer packages

Backend improvements:
- Moved ID generation to backend for single source of truth
- Added ElementContext struct for frontend-backend communication
- Updated API handlers to support context-based content ID generation

Frontend improvements:
- Enhanced getElementMetadata() to extract semantic context
- Updated save flow to handle both enhanced and non-enhanced elements
- Improved API client to use backend-generated content IDs

Result:
- Unique content IDs: navbar-logo-200530 vs hero-title-a1de7b
- Precise element matching using content validation
- Single source of truth for DOM utilities in parser package
- Eliminated 40+ lines of duplicate code while fixing core bug
This commit is contained in:
2025-09-11 14:14:57 +02:00
parent f73e21ce6e
commit ef1d1083ce
12 changed files with 575 additions and 314 deletions

View File

@@ -98,52 +98,32 @@ func (e *Enhancer) injectElementContent(doc *html.Node, elem parser.Element) err
return nil
}
// findAndInjectNodes recursively finds nodes and injects content
func (e *Enhancer) findAndInjectNodes(node *html.Node, elem parser.Element, contentItem *ContentItem) {
if node.Type == html.ElementNode {
// Check if this node matches our element criteria
classes := getClasses(node)
if containsClass(classes, "insertr") && node.Data == elem.Tag {
// This might be our target node - inject content
e.injector.addContentAttributes(node, elem.ContentID, string(elem.Type))
if contentItem != nil {
switch elem.Type {
case parser.ContentText:
e.injector.injectTextContent(node, contentItem.Value)
case parser.ContentMarkdown:
e.injector.injectMarkdownContent(node, contentItem.Value)
case parser.ContentLink:
e.injector.injectLinkContent(node, contentItem.Value)
}
}
}
// findAndInjectNodes finds the specific node for this element and injects content
func (e *Enhancer) findAndInjectNodes(rootNode *html.Node, elem parser.Element, contentItem *ContentItem) {
// Use parser-based element matching to find the correct specific node
targetNode := e.findNodeInDocument(rootNode, elem)
if targetNode == nil {
// Element not found - this is normal for elements without content in database
return
}
// Recursively process children
for child := node.FirstChild; child != nil; child = child.NextSibling {
e.findAndInjectNodes(child, elem, contentItem)
// Inject content attributes for the correctly matched node
e.injector.addContentAttributes(targetNode, elem.ContentID, string(elem.Type))
// Inject content if available
if contentItem != nil {
switch elem.Type {
case parser.ContentText:
e.injector.injectTextContent(targetNode, contentItem.Value)
case parser.ContentMarkdown:
e.injector.injectMarkdownContent(targetNode, contentItem.Value)
case parser.ContentLink:
e.injector.injectLinkContent(targetNode, contentItem.Value)
}
}
}
// Helper functions from parser package
func getClasses(node *html.Node) []string {
for _, attr := range node.Attr {
if attr.Key == "class" {
return strings.Fields(attr.Val)
}
}
return []string{}
}
func containsClass(classes []string, target string) bool {
for _, class := range classes {
if class == target {
return true
}
}
return false
}
// Helper functions are now provided by the parser package
// EnhanceDirectory processes all HTML files in a directory
func (e *Enhancer) EnhanceDirectory(inputDir, outputDir string) error {
@@ -295,28 +275,10 @@ func (e *Enhancer) enhanceFileInPlace(filePath string, elements []parser.Element
return e.writeHTML(doc, filePath)
}
// findNodeInDocument finds a specific node in the HTML document tree
// findNodeInDocument finds a specific node in the HTML document tree using parser utilities
func (e *Enhancer) findNodeInDocument(doc *html.Node, elem parser.Element) *html.Node {
// This is a simplified approach - in a production system we might need
// more sophisticated node matching based on attributes, position, etc.
return e.findNodeByTagAndClass(doc, elem.Tag, "insertr")
// Use parser's sophisticated matching logic
return parser.FindElementInDocument(doc, elem)
}
// findNodeByTagAndClass recursively searches for a node with specific tag and class
func (e *Enhancer) findNodeByTagAndClass(node *html.Node, targetTag, targetClass string) *html.Node {
if node.Type == html.ElementNode && node.Data == targetTag {
classes := getClasses(node)
if containsClass(classes, targetClass) {
return node
}
}
// Search children
for child := node.FirstChild; child != nil; child = child.NextSibling {
if result := e.findNodeByTagAndClass(child, targetTag, targetClass); result != nil {
return result
}
}
return nil
}
// All element matching functions are now provided by the parser package