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:
163
cmd/restore.go
Normal file
163
cmd/restore.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"github.com/insertr/insertr/internal/content"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
timestamp string
|
||||
latest bool
|
||||
clean bool
|
||||
)
|
||||
|
||||
var restoreCmd = &cobra.Command{
|
||||
Use: "restore [site-id]",
|
||||
Short: "Restore a site from backup",
|
||||
Long: `Restore a registered site from a timestamped backup.
|
||||
|
||||
Examples:
|
||||
insertr restore demo # List available backups
|
||||
insertr restore demo --clean # Restore from oldest backup (cleanest)
|
||||
insertr restore demo --latest # Restore from newest backup
|
||||
insertr restore demo --timestamp 20250910-224704 # Restore from specific backup`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runRestore,
|
||||
}
|
||||
|
||||
func init() {
|
||||
restoreCmd.Flags().StringVarP(×tamp, "timestamp", "t", "", "specific backup timestamp to restore from")
|
||||
restoreCmd.Flags().BoolVar(&latest, "latest", false, "restore from most recent backup")
|
||||
restoreCmd.Flags().BoolVar(&clean, "clean", false, "restore from oldest backup (cleanest state)")
|
||||
|
||||
// Bind flags to viper
|
||||
viper.BindPFlag("restore.timestamp", restoreCmd.Flags().Lookup("timestamp"))
|
||||
viper.BindPFlag("restore.latest", restoreCmd.Flags().Lookup("latest"))
|
||||
viper.BindPFlag("restore.clean", restoreCmd.Flags().Lookup("clean"))
|
||||
}
|
||||
|
||||
func runRestore(cmd *cobra.Command, args []string) {
|
||||
siteID := args[0]
|
||||
|
||||
// Initialize content client (we don't actually need it for restore, but SiteManager expects it)
|
||||
contentClient := content.NewMockClient()
|
||||
|
||||
// Initialize site manager
|
||||
siteManager := content.NewSiteManager(contentClient, "./insertr-backups", false)
|
||||
|
||||
// Load sites from configuration to register them
|
||||
if siteConfigs := viper.Get("server.sites"); siteConfigs != nil {
|
||||
if configs, ok := siteConfigs.([]interface{}); ok {
|
||||
var sites []*content.SiteConfig
|
||||
for _, configInterface := range configs {
|
||||
if configMap, ok := configInterface.(map[string]interface{}); ok {
|
||||
site := &content.SiteConfig{}
|
||||
if id, ok := configMap["site_id"].(string); ok {
|
||||
site.SiteID = id
|
||||
}
|
||||
if path, ok := configMap["path"].(string); ok {
|
||||
site.Path = path
|
||||
}
|
||||
if domain, ok := configMap["domain"].(string); ok {
|
||||
site.Domain = domain
|
||||
}
|
||||
if autoEnhance, ok := configMap["auto_enhance"].(bool); ok {
|
||||
site.AutoEnhance = autoEnhance
|
||||
}
|
||||
if backupOriginals, ok := configMap["backup_originals"].(bool); ok {
|
||||
site.BackupOriginals = backupOriginals
|
||||
}
|
||||
sites = append(sites, site)
|
||||
}
|
||||
}
|
||||
|
||||
if err := siteManager.RegisterSites(sites); err != nil {
|
||||
log.Fatalf("Failed to register sites: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List available backups
|
||||
backups, err := siteManager.ListBackups(siteID)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list backups: %v", err)
|
||||
}
|
||||
|
||||
if len(backups) == 0 {
|
||||
fmt.Printf("❌ No backups found for site '%s'\n", siteID)
|
||||
fmt.Printf("💡 Backups are created automatically during enhancement when backup_originals is enabled\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Sort backups chronologically
|
||||
sort.Strings(backups)
|
||||
|
||||
// Handle different restore modes
|
||||
var targetTimestamp string
|
||||
|
||||
if timestamp != "" {
|
||||
// Specific timestamp provided
|
||||
targetTimestamp = timestamp
|
||||
found := false
|
||||
for _, backup := range backups {
|
||||
if backup == targetTimestamp {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
fmt.Printf("❌ Backup timestamp '%s' not found for site '%s'\n", targetTimestamp, siteID)
|
||||
fmt.Printf("📋 Available backups:\n")
|
||||
for i, backup := range backups {
|
||||
if i == 0 {
|
||||
fmt.Printf(" %s (oldest/cleanest)\n", backup)
|
||||
} else if i == len(backups)-1 {
|
||||
fmt.Printf(" %s (newest)\n", backup)
|
||||
} else {
|
||||
fmt.Printf(" %s\n", backup)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if clean {
|
||||
// Restore from oldest backup (cleanest)
|
||||
targetTimestamp = backups[0]
|
||||
fmt.Printf("🧹 Restoring from oldest backup (cleanest state): %s\n", targetTimestamp)
|
||||
} else if latest {
|
||||
// Restore from newest backup
|
||||
targetTimestamp = backups[len(backups)-1]
|
||||
fmt.Printf("🔄 Restoring from newest backup: %s\n", targetTimestamp)
|
||||
} else {
|
||||
// No specific option - list available backups
|
||||
fmt.Printf("📋 Available backups for site '%s':\n", siteID)
|
||||
for i, backup := range backups {
|
||||
if i == 0 {
|
||||
fmt.Printf(" %s (oldest/cleanest) ← use --clean\n", backup)
|
||||
} else if i == len(backups)-1 {
|
||||
fmt.Printf(" %s (newest) ← use --latest\n", backup)
|
||||
} else {
|
||||
fmt.Printf(" %s\n", backup)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\nUsage:\n")
|
||||
fmt.Printf(" insertr restore %s --clean # restore from oldest backup\n", siteID)
|
||||
fmt.Printf(" insertr restore %s --latest # restore from newest backup\n", siteID)
|
||||
fmt.Printf(" insertr restore %s --timestamp %s # restore from specific backup\n", siteID, backups[0])
|
||||
return
|
||||
}
|
||||
|
||||
// Perform restore
|
||||
fmt.Printf("🔄 Restoring site '%s' from backup %s...\n", siteID, targetTimestamp)
|
||||
|
||||
if err := siteManager.RestoreFromBackup(siteID, targetTimestamp); err != nil {
|
||||
log.Fatalf("❌ Restore failed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Successfully restored site '%s' from backup %s\n", siteID, targetTimestamp)
|
||||
fmt.Printf("💡 Site files have been restored to their state from %s\n", targetTimestamp)
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!DOCTYPE html><html lang="en"><head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>About - Acme Consulting Services</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<link rel="stylesheet" href="assets/style.css"/>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<h1 class="logo insertr">Acme Consulting</h1>
|
||||
<h1 class="logo insertr" >Acme Consulting</h1>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html">Home</a></li>
|
||||
<li><a href="about.html">About</a></li>
|
||||
@@ -24,21 +22,21 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1 class="insertr">About Acme Consulting</h1>
|
||||
<p class="lead insertr">We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
|
||||
<h1 class="insertr" >About Acme Consulting</h1>
|
||||
<p class="lead insertr" >We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Story Section -->
|
||||
<section class="services">
|
||||
<div class="container">
|
||||
<h2 class="insertr">Our Story</h2>
|
||||
<h2 class="insertr" >Our Story</h2>
|
||||
<div class="insertr-group">
|
||||
<p>Founded in 2020, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.</p>
|
||||
|
||||
<p>Our founders, with combined experience of over 30 years in business strategy, operations, and technology, recognized that the traditional consulting model wasn't serving the needs of growing businesses. We set out to change that.</p>
|
||||
<p>Our founders, with combined experience of over 30 years in business strategy, operations, and technology, recognized that the traditional consulting model wasn't serving the needs of growing businesses. We set out to change that.</p>
|
||||
|
||||
<p>Today, we've helped over **200 businesses** streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
|
||||
<p>Today, we've helped over **200 businesses** streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -46,14 +44,14 @@
|
||||
<!-- Team Section -->
|
||||
<section class="cta">
|
||||
<div class="container">
|
||||
<h2 class="insertr">Our Team</h2>
|
||||
<p class="insertr">We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
|
||||
<h2 class="insertr" >Our Team</h2>
|
||||
<p class="insertr" >We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
|
||||
|
||||
<div class="services-grid" style="margin-top: 3rem;">
|
||||
<div class="service-card">
|
||||
<div class="insertr">
|
||||
<h3>Sarah Chen</h3>
|
||||
<p><strong>Founder & CEO</strong></p>
|
||||
<p><strong>Founder & CEO</strong></p>
|
||||
<p>Former <strong>McKinsey consultant</strong> with 15 years of experience in strategy and operations. MBA from Stanford.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,19 +76,19 @@
|
||||
<!-- Values Section -->
|
||||
<section class="testimonial">
|
||||
<div class="container">
|
||||
<h2 class="insertr" style="margin-bottom: 2rem;">Our Values</h2>
|
||||
<h2 class="insertr" style="margin-bottom: 2rem;" >Our Values</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; text-align: left;">
|
||||
<div>
|
||||
<h3 class="insertr">Client-First</h3>
|
||||
<p class="insertr">Every recommendation we make is designed with your specific business context and goals in mind.</p>
|
||||
<h3 class="insertr" >Client-First</h3>
|
||||
<p class="insertr" >Every recommendation we make is designed with your specific business context and goals in mind.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="insertr">Practical Solutions</h3>
|
||||
<p class="insertr">We believe in strategies that you can actually implement with your current resources and capabilities.</p>
|
||||
<h3 class="insertr" >Practical Solutions</h3>
|
||||
<p class="insertr" >We believe in strategies that you can actually implement with your current resources and capabilities.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="insertr">Long-term Partnership</h3>
|
||||
<p class="insertr">We're not just consultants; we're partners in your business success for the long haul.</p>
|
||||
<h3 class="insertr" >Long-term Partnership</h3>
|
||||
<p class="insertr" >We're not just consultants; we're partners in your business success for the long haul.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,7 +97,7 @@
|
||||
<!-- Test Section for Insertr Features -->
|
||||
<section class="testimonial">
|
||||
<div class="container">
|
||||
<h2 class="insertr">Feature Tests</h2>
|
||||
<h2 class="insertr" >Feature Tests</h2>
|
||||
|
||||
<!-- Test 1: .insertr container expansion (should make each p individually editable) -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
@@ -126,13 +124,14 @@
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="insertr">© 2024 Acme Consulting Services. All rights reserved.</p>
|
||||
<p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <button class="insertr-gate" style="background: none; border: 1px solid #ccc; padding: 4px 8px; margin-left: 10px; border-radius: 3px; font-size: 11px;">🔧 Edit</button></p>
|
||||
<p class="insertr" >© 2024 Acme Consulting Services. All rights reserved.</p>
|
||||
<p class="insertr" >📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <button class="insertr-gate" style="background: none; border: 1px solid #ccc; padding: 4px 8px; margin-left: 10px; border-radius: 3px; font-size: 11px;">🔧 Edit</button></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Insertr JavaScript Library -->
|
||||
<script type="text/javascript" src="insertr.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
</body></html>
|
||||
@@ -1,17 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!DOCTYPE html><html lang="en"><head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Acme Consulting Services - Live Reload Test</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<link rel="stylesheet" href="assets/style.css"/>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<h1 class="logo insertr">Acme Consulting</h1>
|
||||
<h1 class="logo insertr" >Acme Consulting!!!</h1>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html">Home</a></li>
|
||||
<li><a href="about.html">About</a></li>
|
||||
@@ -24,30 +22,30 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1 class="insertr">Transform Your Business with Expert Consulting</h1>
|
||||
<p class="lead insertr">We help small businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success.</p>
|
||||
<a href="contact.html" class="btn-primary insertr">Get Started Today</a>
|
||||
<h1 class="insertr" >!Transform Your Business with Expert Consulting!</h1>
|
||||
<p class="lead insertr" >We help small businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success. Superb</p>
|
||||
<a href="contact.html" class="btn-primary insertr" >Get Started Today?</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Services Section -->
|
||||
<section class="services">
|
||||
<div class="container">
|
||||
<h2 class="insertr">Our Services</h2>
|
||||
<p class="section-subtitle insertr">Comprehensive solutions tailored to your business needs</p>
|
||||
<h2 class="insertr" >Our Services</h2>
|
||||
<p class="section-subtitle insertr" >Comprehensive solutions tailored to your business needs</p>
|
||||
|
||||
<div class="services-grid">
|
||||
<div class="service-card">
|
||||
<h3 class="insertr">Strategic Planning</h3>
|
||||
<p class="insertr">Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
|
||||
<h3 class="insertr" >Strategic Planning</h3>
|
||||
<p class="insertr" >Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
|
||||
</div>
|
||||
<div class="service-card">
|
||||
<h3 class="insertr">Operations Optimization</h3>
|
||||
<p class="insertr">Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
|
||||
<h3 class="insertr" >Operations Optimization</h3>
|
||||
<p class="insertr" >Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
|
||||
</div>
|
||||
<div class="service-card">
|
||||
<h3 class="insertr">Digital Transformation</h3>
|
||||
<p class="insertr">Modernize your technology stack and digital presence to compete effectively in today's marketplace.</p>
|
||||
<h3 class="insertr" >Digital Transformation</h3>
|
||||
<p class="insertr" >Modernize your technology stack and digital presence to compete effectively in today's marketplace.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,8 +55,8 @@
|
||||
<section class="testimonial">
|
||||
<div class="container">
|
||||
<blockquote>
|
||||
<p class="insertr">"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p>
|
||||
<cite class="insertr">Sarah Johnson, CEO of TechStart Inc.</cite>
|
||||
<p class="insertr" >"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p>
|
||||
<cite class="insertr" >Sarah Johnson, CEO of TechStart Inc.</cite>
|
||||
</blockquote>
|
||||
</div>
|
||||
</section>
|
||||
@@ -66,22 +64,22 @@
|
||||
<!-- Call to Action -->
|
||||
<section class="cta">
|
||||
<div class="container">
|
||||
<h2 class="insertr">Ready to Transform Your Business?</h2>
|
||||
<p class="insertr">Contact us today for a free consultation and discover how we can help you achieve your goals.</p>
|
||||
<a href="contact.html" class="btn-primary insertr">Schedule Consultation</a>
|
||||
<h2 class="insertr" >Ready to Transform Your Business?</h2>
|
||||
<p class="insertr" >Contact us today for a free consultation and discover how we can help you achieve your goals.</p>
|
||||
<a href="contact.html" class="btn-primary insertr" >Schedule Consultation</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="insertr">© 2024 Acme Consulting Services. All rights reserved.</p>
|
||||
<p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <a href="#" class="insertr-gate">Editor</a></p>
|
||||
<p class="insertr" >© 2024 Acme Consulting Services. All rights reserved.</p>
|
||||
<p class="insertr" >📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <a href="#" class="insertr-gate">Editor</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Insertr JavaScript Library -->
|
||||
<script type="text/javascript" src="insertr.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</body></html>
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"github.com/insertr/insertr/internal/db"
|
||||
"github.com/insertr/insertr/internal/db/postgresql"
|
||||
"github.com/insertr/insertr/internal/db/sqlite"
|
||||
"github.com/insertr/insertr/internal/parser"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// ContentHandler handles all content-related HTTP requests
|
||||
@@ -232,6 +234,16 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
||||
siteID = "default" // final fallback
|
||||
}
|
||||
|
||||
// Determine content ID - use provided ID or generate from element context
|
||||
contentID := req.ID
|
||||
if contentID == "" {
|
||||
if req.ElementContext == nil {
|
||||
http.Error(w, "Either ID or element_context required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
contentID = h.generateContentID(req.ElementContext)
|
||||
}
|
||||
|
||||
// Extract user from request using authentication service
|
||||
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
|
||||
if authErr != nil {
|
||||
@@ -246,7 +258,7 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
content, err = h.database.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{
|
||||
ID: req.ID,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
Type: req.Type,
|
||||
@@ -254,7 +266,7 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
case "postgresql":
|
||||
content, err = h.database.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{
|
||||
ID: req.ID,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
Type: req.Type,
|
||||
@@ -728,3 +740,41 @@ func (h *ContentHandler) versionMatches(version interface{}, contentID, siteID s
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// generateContentID creates a content ID from element context using the parser
|
||||
func (h *ContentHandler) generateContentID(ctx *ElementContext) string {
|
||||
// Create virtual node for existing parser ID generation
|
||||
virtualNode := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: ctx.Tag,
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: strings.Join(ctx.Classes, " ")},
|
||||
},
|
||||
}
|
||||
|
||||
// Add parent context as a virtual parent node if provided
|
||||
if ctx.ParentContext != "" && ctx.ParentContext != "content" {
|
||||
parentNode := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: "section",
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: ctx.ParentContext},
|
||||
},
|
||||
}
|
||||
parentNode.AppendChild(virtualNode)
|
||||
virtualNode.Parent = parentNode
|
||||
}
|
||||
|
||||
// Add text content for hash generation
|
||||
if ctx.OriginalContent != "" {
|
||||
textNode := &html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: ctx.OriginalContent,
|
||||
}
|
||||
virtualNode.AppendChild(textNode)
|
||||
}
|
||||
|
||||
// Use existing parser ID generator
|
||||
idGenerator := parser.NewIDGenerator()
|
||||
return idGenerator.Generate(virtualNode)
|
||||
}
|
||||
|
||||
@@ -31,9 +31,19 @@ type ContentVersionsResponse struct {
|
||||
Versions []ContentVersion `json:"versions"`
|
||||
}
|
||||
|
||||
// Element context for backend ID generation
|
||||
type ElementContext struct {
|
||||
Tag string `json:"tag"`
|
||||
Classes []string `json:"classes"`
|
||||
OriginalContent string `json:"original_content"`
|
||||
ParentContext string `json:"parent_context"`
|
||||
Purpose string `json:"purpose"`
|
||||
}
|
||||
|
||||
// Request models
|
||||
type CreateContentRequest struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id,omitempty"` // For enhanced sites
|
||||
ElementContext *ElementContext `json:"element_context,omitempty"` // For non-enhanced sites
|
||||
SiteID string `json:"site_id,omitempty"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
|
||||
@@ -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))
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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(node, contentItem.Value)
|
||||
e.injector.injectTextContent(targetNode, contentItem.Value)
|
||||
case parser.ContentMarkdown:
|
||||
e.injector.injectMarkdownContent(node, contentItem.Value)
|
||||
e.injector.injectMarkdownContent(targetNode, contentItem.Value)
|
||||
case parser.ContentLink:
|
||||
e.injector.injectLinkContent(node, contentItem.Value)
|
||||
e.injector.injectLinkContent(targetNode, contentItem.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process children
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
e.findAndInjectNodes(child, elem, contentItem)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -28,7 +28,9 @@ func (g *IDGenerator) Generate(node *html.Node) string {
|
||||
contentHash := g.getContentHash(node)
|
||||
|
||||
baseID := g.createBaseID(context, purpose, contentHash)
|
||||
return g.ensureUnique(baseID)
|
||||
finalID := g.ensureUnique(baseID)
|
||||
|
||||
return finalID
|
||||
}
|
||||
|
||||
// getSemanticContext determines the semantic context from parent elements
|
||||
@@ -36,11 +38,11 @@ func (g *IDGenerator) getSemanticContext(node *html.Node) string {
|
||||
// Walk up the tree to find semantic containers
|
||||
parent := node.Parent
|
||||
for parent != nil && parent.Type == html.ElementNode {
|
||||
classes := getClasses(parent)
|
||||
classes := GetClasses(parent)
|
||||
|
||||
// Check for common semantic section classes
|
||||
for _, class := range []string{"hero", "services", "nav", "navbar", "footer", "about", "contact", "testimonial"} {
|
||||
if containsClass(classes, class) {
|
||||
if ContainsClass(classes, class) {
|
||||
return class
|
||||
}
|
||||
}
|
||||
@@ -68,7 +70,7 @@ func (g *IDGenerator) getSemanticContext(node *html.Node) string {
|
||||
// getPurpose determines the purpose/role of the element
|
||||
func (g *IDGenerator) getPurpose(node *html.Node) string {
|
||||
tag := strings.ToLower(node.Data)
|
||||
classes := getClasses(node)
|
||||
classes := GetClasses(node)
|
||||
|
||||
// Check for specific CSS classes that indicate purpose
|
||||
for _, class := range classes {
|
||||
|
||||
@@ -90,15 +90,15 @@ func (p *Parser) parseFile(filePath string) ([]Element, []string, error) {
|
||||
// findInsertrElements recursively finds all elements with "insertr" class
|
||||
func (p *Parser) findInsertrElements(node *html.Node, filePath string, elements *[]Element, warnings *[]string) {
|
||||
if node.Type == html.ElementNode {
|
||||
classes := getClasses(node)
|
||||
classes := GetClasses(node)
|
||||
|
||||
// Check if element has "insertr" class
|
||||
if containsClass(classes, "insertr") {
|
||||
if ContainsClass(classes, "insertr") {
|
||||
if isContainer(node) {
|
||||
// Container element - expand to viable children
|
||||
viableChildren := findViableChildren(node)
|
||||
for _, child := range viableChildren {
|
||||
childClasses := getClasses(child)
|
||||
childClasses := GetClasses(child)
|
||||
element, warning := p.createElement(child, filePath, childClasses)
|
||||
*elements = append(*elements, element)
|
||||
if warning != "" {
|
||||
@@ -181,13 +181,13 @@ func (p *Parser) resolveContentID(node *html.Node) (string, bool) {
|
||||
// detectContentType determines the content type based on element and classes
|
||||
func (p *Parser) detectContentType(node *html.Node, classes []string) ContentType {
|
||||
// Check for explicit type classes first
|
||||
if containsClass(classes, "insertr-markdown") {
|
||||
if ContainsClass(classes, "insertr-markdown") {
|
||||
return ContentMarkdown
|
||||
}
|
||||
if containsClass(classes, "insertr-link") {
|
||||
if ContainsClass(classes, "insertr-link") {
|
||||
return ContentLink
|
||||
}
|
||||
if containsClass(classes, "insertr-text") {
|
||||
if ContainsClass(classes, "insertr-text") {
|
||||
return ContentText
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// getClasses extracts CSS classes from an HTML node
|
||||
func getClasses(node *html.Node) []string {
|
||||
// GetClasses extracts CSS classes from an HTML node
|
||||
func GetClasses(node *html.Node) []string {
|
||||
classAttr := getAttribute(node, "class")
|
||||
if classAttr == "" {
|
||||
return []string{}
|
||||
@@ -17,8 +17,8 @@ func getClasses(node *html.Node) []string {
|
||||
return classes
|
||||
}
|
||||
|
||||
// containsClass checks if a class list contains a specific class
|
||||
func containsClass(classes []string, target string) bool {
|
||||
// ContainsClass checks if a class list contains a specific class
|
||||
func ContainsClass(classes []string, target string) bool {
|
||||
for _, class := range classes {
|
||||
if class == target {
|
||||
return true
|
||||
@@ -157,3 +157,39 @@ func isSelfClosing(node *html.Node) bool {
|
||||
|
||||
return selfClosingTags[node.Data]
|
||||
}
|
||||
|
||||
// FindElementInDocument finds a parser element in HTML document tree using semantic matching
|
||||
func FindElementInDocument(doc *html.Node, element Element) *html.Node {
|
||||
return findElementWithContext(doc, element)
|
||||
}
|
||||
|
||||
// findElementWithContext uses the parser's semantic understanding to find the correct element
|
||||
func findElementWithContext(node *html.Node, target Element) *html.Node {
|
||||
if node.Type == html.ElementNode && node.Data == target.Tag {
|
||||
classes := GetClasses(node)
|
||||
if ContainsClass(classes, "insertr") {
|
||||
// Content-based validation for precise matching
|
||||
textContent := extractTextContent(node)
|
||||
nodeContent := strings.TrimSpace(textContent)
|
||||
targetContent := strings.TrimSpace(target.Content)
|
||||
|
||||
if nodeContent == targetContent {
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively search children
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if result := findElementWithContext(child, target); result != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAttribute gets an attribute value from an HTML node (exported version)
|
||||
func GetAttribute(node *html.Node, key string) string {
|
||||
return getAttribute(node, key)
|
||||
}
|
||||
|
||||
@@ -58,27 +58,39 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async createContent(contentId, content, type) {
|
||||
async createContent(contentId, content, type, elementContext = null) {
|
||||
try {
|
||||
const payload = {
|
||||
value: content,
|
||||
type: type
|
||||
};
|
||||
|
||||
if (contentId) {
|
||||
// Enhanced site - provide existing ID
|
||||
payload.id = contentId;
|
||||
} else if (elementContext) {
|
||||
// Non-enhanced site - provide context for backend ID generation
|
||||
payload.element_context = elementContext;
|
||||
} else {
|
||||
throw new Error('Either contentId or elementContext must be provided');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.getAuthToken()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: contentId,
|
||||
value: content,
|
||||
type: type
|
||||
})
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log(`✅ Content created: ${contentId} (${type})`);
|
||||
return true;
|
||||
const result = await response.json();
|
||||
console.log(`✅ Content created: ${result.id} (${result.type})`);
|
||||
return result;
|
||||
} else {
|
||||
console.warn(`⚠️ Create failed (${response.status}): ${contentId}`);
|
||||
return false;
|
||||
console.warn(`⚠️ Create failed (${response.status}): ${contentId || 'backend-generated'}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
|
||||
@@ -102,31 +102,34 @@ export class InsertrEditor {
|
||||
contentValue = formData.text || formData;
|
||||
}
|
||||
|
||||
// Try to update existing content first
|
||||
if (meta.hasExistingId) {
|
||||
// Enhanced site - update existing content
|
||||
const updateSuccess = await this.apiClient.updateContent(meta.contentId, contentValue);
|
||||
|
||||
if (!updateSuccess) {
|
||||
// If update fails, try to create new content
|
||||
console.error('❌ Failed to update content:', meta.contentId);
|
||||
} else {
|
||||
console.log(`✅ Content updated:`, meta.contentId, contentValue);
|
||||
}
|
||||
} else {
|
||||
// Non-enhanced site - create with backend ID generation
|
||||
const contentType = this.determineContentType(meta.element);
|
||||
const createSuccess = await this.apiClient.createContent(meta.contentId, contentValue, contentType);
|
||||
const result = await this.apiClient.createContent(null, contentValue, contentType, meta.elementContext);
|
||||
|
||||
if (!createSuccess) {
|
||||
console.error('❌ Failed to save content to server:', meta.contentId);
|
||||
// Still update the UI optimistically
|
||||
if (result) {
|
||||
// Store the backend-generated ID in the element
|
||||
meta.element.setAttribute('data-content-id', result.id);
|
||||
meta.element.setAttribute('data-content-type', result.type);
|
||||
console.log(`✅ Content created with backend ID: ${result.id}`, contentValue);
|
||||
} else {
|
||||
console.error('❌ Failed to save content to server');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Close form
|
||||
this.formRenderer.closeForm();
|
||||
|
||||
console.log(`✅ Content saved:`, meta.contentId, contentValue);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error saving content:', error);
|
||||
|
||||
|
||||
this.formRenderer.closeForm();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,10 +104,36 @@ export class InsertrCore {
|
||||
|
||||
// Get element metadata
|
||||
getElementMetadata(element) {
|
||||
const existingId = element.getAttribute('data-content-id');
|
||||
|
||||
if (existingId) {
|
||||
// Enhanced site - use existing ID
|
||||
return {
|
||||
contentId: element.getAttribute('data-content-id') || this.generateDeterministicId(element),
|
||||
contentId: existingId,
|
||||
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
|
||||
element: element
|
||||
element: element,
|
||||
hasExistingId: true
|
||||
};
|
||||
} else {
|
||||
// Non-enhanced site - prepare context for backend ID generation
|
||||
return {
|
||||
contentId: null, // Backend will generate
|
||||
contentType: this.detectContentType(element),
|
||||
element: element,
|
||||
elementContext: this.extractElementContext(element),
|
||||
hasExistingId: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Extract element context for backend ID generation
|
||||
extractElementContext(element) {
|
||||
return {
|
||||
tag: element.tagName.toLowerCase(),
|
||||
classes: Array.from(element.classList),
|
||||
original_content: element.textContent.trim(),
|
||||
parent_context: this.getSemanticContext(element),
|
||||
purpose: this.getPurpose(element)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user