diff --git a/cmd/restore.go b/cmd/restore.go
new file mode 100644
index 0000000..9a641df
--- /dev/null
+++ b/cmd/restore.go
@@ -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)
+}
diff --git a/demo-site/about.html b/demo-site/about.html
index 615cfa0..f440a15 100644
--- a/demo-site/about.html
+++ b/demo-site/about.html
@@ -1,138 +1,137 @@
-
-
-
-
-
- About - Acme Consulting Services
-
+
+
+
+ About - Acme Consulting Services
+
-
-
-
-
Acme Consulting
-
+
+
+
-
+
+
-
-
-
-
About Acme Consulting
-
We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.
-
-
+
+
+
+
About Acme Consulting
+
We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.
+
+
-
-
-
-
Our Story
-
-
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.
-
-
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.
-
-
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.
-
-
-
+
+
+
+
Our Story
+
+
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.
+
+
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.
+
+
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.
+
+
+
-
-
-
-
Our Team
-
We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.
-
-
-
-
-
Sarah Chen
-
Founder & CEO
-
Former McKinsey consultant with 15 years of experience in strategy and operations. MBA from Stanford.
-
-
-
-
-
Michael Rodriguez
-
Head of Operations
-
20 years in manufacturing and supply chain optimization. Expert in lean methodologies and process improvement.
-
-
-
-
-
Emma Thompson
-
Digital Strategy Lead
-
Former tech startup founder turned consultant. Specializes in digital transformation and technology adoption.
-
-
-
-
-
+
+
+
+
Our Team
+
We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.
+
+
+
+
+
Sarah Chen
+
Founder & CEO
+
Former McKinsey consultant with 15 years of experience in strategy and operations. MBA from Stanford.
+
+
+
+
+
Michael Rodriguez
+
Head of Operations
+
20 years in manufacturing and supply chain optimization. Expert in lean methodologies and process improvement.
+
+
+
+
+
Emma Thompson
+
Digital Strategy Lead
+
Former tech startup founder turned consultant. Specializes in digital transformation and technology adoption.
+
+
+
+
+
-
-
-
-
Our Values
-
-
-
Client-First
-
Every recommendation we make is designed with your specific business context and goals in mind.
-
-
-
Practical Solutions
-
We believe in strategies that you can actually implement with your current resources and capabilities.
-
-
-
Long-term Partnership
-
We're not just consultants; we're partners in your business success for the long haul.
-
-
-
-
+
+
+
+
Our Values
+
+
+
Client-First
+
Every recommendation we make is designed with your specific business context and goals in mind.
+
+
+
Practical Solutions
+
We believe in strategies that you can actually implement with your current resources and capabilities.
+
+
+
Long-term Partnership
+
We're not just consultants; we're partners in your business success for the long haul.
+
+
+
+
-
-
-
-
Feature Tests
-
-
-
-
Test 1: Container Expansion (.insertr)
-
-
This paragraph should be individually editable with a textarea.
-
This second paragraph should also be individually editable.
-
Each paragraph should get its own modal when clicked.
-
-
-
-
-
-
Test 2: Group Editing (.insertr-group)
-
-
This paragraph is part of a group .
-
Clicking anywhere should open one markdown editor with rich formatting .
-
All content should be editable together as markdown with proper HTML conversion .
-
-
-
-
+
+
+
+
Feature Tests
+
+
+
+
Test 1: Container Expansion (.insertr)
+
+
This paragraph should be individually editable with a textarea.
+
This second paragraph should also be individually editable.
+
Each paragraph should get its own modal when clicked.
+
+
+
+
+
+
Test 2: Group Editing (.insertr-group)
+
+
This paragraph is part of a group .
+
Clicking anywhere should open one markdown editor with rich formatting .
+
All content should be editable together as markdown with proper HTML conversion .
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
\ No newline at end of file
diff --git a/demo-site/index.html b/demo-site/index.html
index 316eb12..d9b21a2 100644
--- a/demo-site/index.html
+++ b/demo-site/index.html
@@ -1,87 +1,85 @@
-
-
-
-
-
- Acme Consulting Services - Live Reload Test
-
+
+
+
+ Acme Consulting Services - Live Reload Test
+
-
-
-
-
Acme Consulting
-
+
+
+
-
+
+
-
-
-
-
Transform Your Business with Expert Consulting
-
We help small businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success.
-
Get Started Today
-
-
+
+
+
+
!Transform Your Business with Expert Consulting!
+
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
+
Get Started Today?
+
+
-
-
-
-
Our Services
-
Comprehensive solutions tailored to your business needs
-
-
-
-
Strategic Planning
-
Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.
-
-
-
Operations Optimization
-
Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.
-
-
-
Digital Transformation
-
Modernize your technology stack and digital presence to compete effectively in today's marketplace.
-
-
-
-
+
+
+
+
Our Services
+
Comprehensive solutions tailored to your business needs
+
+
+
+
Strategic Planning
+
Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.
+
+
+
Operations Optimization
+
Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.
+
+
+
Digital Transformation
+
Modernize your technology stack and digital presence to compete effectively in today's marketplace.
+
+
+
+
-
-
-
-
- "Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."
- Sarah Johnson, CEO of TechStart Inc.
-
-
-
+
+
+
+
+ "Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."
+ Sarah Johnson, CEO of TechStart Inc.
+
+
+
-
-
-
-
Ready to Transform Your Business?
-
Contact us today for a free consultation and discover how we can help you achieve your goals.
-
Schedule Consultation
-
-
+
+
+
+
Ready to Transform Your Business?
+
Contact us today for a free consultation and discover how we can help you achieve your goals.
+
Schedule Consultation
+
+
-
-
+
+
-
-
+
+
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/internal/api/handlers.go b/internal/api/handlers.go
index e29f7fa..0e87b63 100644
--- a/internal/api/handlers.go
+++ b/internal/api/handlers.go
@@ -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)
+}
diff --git a/internal/api/models.go b/internal/api/models.go
index 7aaa220..3cb42d0 100644
--- a/internal/api/models.go
+++ b/internal/api/models.go
@@ -31,13 +31,23 @@ 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"`
- SiteID string `json:"site_id,omitempty"`
- Value string `json:"value"`
- Type string `json:"type"`
- CreatedBy string `json:"created_by,omitempty"`
+ 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"`
+ CreatedBy string `json:"created_by,omitempty"`
}
type UpdateContentRequest struct {
diff --git a/internal/content/enhancer.go b/internal/content/enhancer.go
index bce4ca8..c428406 100644
--- a/internal/content/enhancer.go
+++ b/internal/content/enhancer.go
@@ -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
diff --git a/internal/parser/id_generator.go b/internal/parser/id_generator.go
index 932ed22..b16b136 100644
--- a/internal/parser/id_generator.go
+++ b/internal/parser/id_generator.go
@@ -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 {
diff --git a/internal/parser/parser.go b/internal/parser/parser.go
index add1a87..036c50d 100644
--- a/internal/parser/parser.go
+++ b/internal/parser/parser.go
@@ -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
}
diff --git a/internal/parser/utils.go b/internal/parser/utils.go
index d4de57c..2926a6c 100644
--- a/internal/parser/utils.go
+++ b/internal/parser/utils.go
@@ -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)
+}
diff --git a/lib/src/core/api-client.js b/lib/src/core/api-client.js
index 43f16e6..4f71c22 100644
--- a/lib/src/core/api-client.js
+++ b/lib/src/core/api-client.js
@@ -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')) {
diff --git a/lib/src/core/editor.js b/lib/src/core/editor.js
index a24daf9..f997bef 100644
--- a/lib/src/core/editor.js
+++ b/lib/src/core/editor.js
@@ -102,31 +102,34 @@ export class InsertrEditor {
contentValue = formData.text || formData;
}
- // Try to update existing content first
- const updateSuccess = await this.apiClient.updateContent(meta.contentId, contentValue);
-
- if (!updateSuccess) {
- // If update fails, try to create new content
+ if (meta.hasExistingId) {
+ // Enhanced site - update existing content
+ const updateSuccess = await this.apiClient.updateContent(meta.contentId, contentValue);
+ if (!updateSuccess) {
+ 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();
}
}
diff --git a/lib/src/core/insertr.js b/lib/src/core/insertr.js
index ed6e80b..77ed222 100644
--- a/lib/src/core/insertr.js
+++ b/lib/src/core/insertr.js
@@ -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: existingId,
+ contentType: element.getAttribute('data-content-type') || this.detectContentType(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 {
- contentId: element.getAttribute('data-content-id') || this.generateDeterministicId(element),
- contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
- element: element
+ tag: element.tagName.toLowerCase(),
+ classes: Array.from(element.classList),
+ original_content: element.textContent.trim(),
+ parent_context: this.getSemanticContext(element),
+ purpose: this.getPurpose(element)
};
}