feat: implement Phase 3 container transformation with CLASSES.md compliance

- Add backend container transformation in engine.go following syntactic sugar specification
- Containers with .insertr get class removed and viable children get .insertr added
- Remove incorrect frontend container expansion - frontend only finds enhanced elements
- Fix StyleAwareEditor hasMultiPropertyElements runtime error
- Add addClass/removeClass methods to ContentEngine for class manipulation
- Update frontend to match HTML-first approach with no runtime container logic
- Test verified: container <section class='insertr'> transforms to individual h1.insertr, p.insertr, button.insertr

This completes the container expansion functionality per CLASSES.md:
Developer convenience (one .insertr enables section editing) + granular control (individual element editing)
This commit is contained in:
2025-09-21 19:17:12 +02:00
parent 4ef032cad6
commit b5e601d09f
16 changed files with 568 additions and 1407 deletions

View File

@@ -72,14 +72,13 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
processedElements[i] = ProcessedElement{
Node: elem.Node,
ID: id,
Type: elem.Type,
Generated: !contentExists, // Mark as generated only if new to database
Tag: elem.Node.Data,
Classes: GetClasses(elem.Node),
}
// Add/update content attributes to the node
e.addContentAttributes(elem.Node, id, elem.Type)
// Add/update content attributes to the node (only content-id now)
e.addContentAttributes(elem.Node, id)
// Store content only for truly new elements (database-first check)
if !contentExists && (input.Mode == Enhancement || input.Mode == ContentInjection) {
@@ -88,12 +87,12 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
originalTemplate := e.extractOriginalTemplate(elem.Node)
// Store in database via content client
_, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, elem.Type, "system")
_, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, "system")
if err != nil {
// Log error but don't fail the enhancement - content just won't be stored
fmt.Printf("⚠️ Failed to store content for %s: %v\n", id, err)
} else {
fmt.Printf("✅ Created new content: %s (%s)\n", id, elem.Type)
fmt.Printf("✅ Created new content: %s (html)\n", id)
}
}
}
@@ -122,21 +121,47 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
// InsertrElement represents an insertr element found in HTML
type InsertrElement struct {
Node *html.Node
Type string
}
// findInsertrElements finds all elements with class="insertr"
// findInsertrElements finds all elements with class="insertr" and applies container transformation
// This implements the "syntactic sugar transformation" from CLASSES.md:
// - Containers with .insertr get their .insertr class removed
// - Viable children of those containers get .insertr class added
// - Regular elements with .insertr are kept as-is
func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement {
var elements []InsertrElement
var containersToTransform []*html.Node
// First pass: find all .insertr elements and identify containers
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,
})
if isContainer(n) {
// Container element - mark for transformation
containersToTransform = append(containersToTransform, n)
} else {
// Regular element - add directly
elements = append(elements, InsertrElement{
Node: n,
})
}
}
})
// Second pass: transform containers (remove .insertr from container, add to children)
for _, container := range containersToTransform {
// Remove .insertr class from container
e.removeClass(container, "insertr")
// Find viable children and add .insertr class to them
viableChildren := FindViableChildren(container)
for _, child := range viableChildren {
e.addClass(child, "insertr")
elements = append(elements, InsertrElement{
Node: child,
})
}
}
return elements
}
@@ -159,28 +184,11 @@ func (e *ContentEngine) hasInsertrClass(node *html.Node) bool {
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 "text"
default:
return "text"
}
}
// addContentAttributes adds data-content-id and data-content-type attributes
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID, contentType string) {
// addContentAttributes adds data-content-id attribute only
// HTML-first approach: no content-type attribute needed
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID 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
@@ -209,6 +217,85 @@ func (e *ContentEngine) setAttribute(node *html.Node, key, value string) {
})
}
// addClass safely adds a class to an HTML node
func (e *ContentEngine) 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,
})
}
}
// removeClass safely removes a class from an HTML node
func (e *ContentEngine) removeClass(node *html.Node, className string) {
var classIndex int = -1
// Find existing class attribute
for idx, attr := range node.Attr {
if attr.Key == "class" {
classIndex = idx
break
}
}
if classIndex == -1 {
return // No class attribute found
}
// Parse existing classes
classes := strings.Fields(node.Attr[classIndex].Val)
// Filter out the target class
var newClasses []string
for _, class := range classes {
if class != className {
newClasses = append(newClasses, class)
}
}
// Update or remove class attribute
if len(newClasses) == 0 {
// Remove class attribute entirely if no classes remain
node.Attr = append(node.Attr[:classIndex], node.Attr[classIndex+1:]...)
} else {
// Update class attribute with remaining classes
node.Attr[classIndex].Val = strings.Join(newClasses, " ")
}
}
// injectContent injects content from database into elements
func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string) error {
for i := range elements {