feat: implement enhanced deterministic ID generation system
- Replace random UUID with 6-component deterministic signature - Use filePath|domPath|tag|classes|contentPreview|siblingIndex for uniqueness - Enhance sibling positioning with insertr-aware index calculation - Improve DOM path generation with meaningful class inclusion - Restore ID consistency across enhancement runs for reliable content injection Results: ✅ ID Consistency: Same elements always get same IDs (index-p-639460) ✅ Collision Resistance: Different elements get different IDs (4c7206, 23df20, 5a975d) ✅ File Scoping: Same structure in different files gets different IDs ✅ Enhanced Workflow: API edit → enhance button → content injected successfully Fixes enhance button by ensuring API content IDs match enhancement-generated IDs.
This commit is contained in:
@@ -120,34 +120,30 @@ func (g *IDGenerator) buildPrefix(fileName, tag, primaryClass string, index int)
|
|||||||
|
|
||||||
// createDeterministicSignature creates a deterministic signature for element identification
|
// createDeterministicSignature creates a deterministic signature for element identification
|
||||||
func (g *IDGenerator) createDeterministicSignature(node *html.Node, filePath string) string {
|
func (g *IDGenerator) createDeterministicSignature(node *html.Node, filePath string) string {
|
||||||
// Build signature from stable characteristics
|
// Build enhanced signature with 6 components for maximum differentiation
|
||||||
var sigParts []string
|
tag := node.Data
|
||||||
|
|
||||||
// 1. DOM path (simplified, max 3 levels)
|
|
||||||
domPath := g.getSimpleDOMPath(node)
|
domPath := g.getSimpleDOMPath(node)
|
||||||
if domPath != "" {
|
classes := strings.Join(GetClasses(node), " ")
|
||||||
sigParts = append(sigParts, domPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Sibling position
|
|
||||||
siblingIndex := g.getSiblingIndex(node)
|
|
||||||
sigParts = append(sigParts, fmt.Sprintf("pos%d", siblingIndex))
|
|
||||||
|
|
||||||
// 3. Content preview (first few chars for uniqueness)
|
|
||||||
contentPreview := g.getContentPreview(node)
|
contentPreview := g.getContentPreview(node)
|
||||||
if contentPreview != "" {
|
siblingIndex := g.getSiblingIndex(node)
|
||||||
// Use first 20 chars for signature
|
|
||||||
if len(contentPreview) > 20 {
|
// Normalize content preview to first 20 chars
|
||||||
contentPreview = contentPreview[:20]
|
if len(contentPreview) > 20 {
|
||||||
}
|
contentPreview = contentPreview[:20]
|
||||||
sigParts = append(sigParts, contentPreview)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Create hash of combined signature
|
// Create comprehensive deterministic signature
|
||||||
combined := strings.Join(sigParts, "|")
|
signature := fmt.Sprintf("%s|%s|%s|%s|%s|%d",
|
||||||
hash := sha256.Sum256([]byte(combined))
|
filePath, // File context for uniqueness across files
|
||||||
|
domPath, // Structural position in DOM
|
||||||
|
tag, // Element type
|
||||||
|
classes, // CSS classes for style differentiation
|
||||||
|
contentPreview, // Content for similar-structure differentiation
|
||||||
|
siblingIndex, // Position among similar siblings
|
||||||
|
)
|
||||||
|
|
||||||
// Use first 6 characters of hash for short, deterministic suffix
|
// Create deterministic hash suffix (6 chars)
|
||||||
|
hash := sha256.Sum256([]byte(signature))
|
||||||
return fmt.Sprintf("%x", hash)[:6]
|
return fmt.Sprintf("%x", hash)[:6]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,17 +153,24 @@ func (g *IDGenerator) createSignature(node *html.Node, filePath string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSimpleDOMPath creates a simple DOM path for uniqueness
|
// getSimpleDOMPath creates a simple but precise DOM path for uniqueness (max 3 levels)
|
||||||
func (g *IDGenerator) getSimpleDOMPath(node *html.Node) string {
|
func (g *IDGenerator) getSimpleDOMPath(node *html.Node) string {
|
||||||
var pathParts []string
|
var pathParts []string
|
||||||
current := node
|
current := node
|
||||||
depth := 0
|
depth := 0
|
||||||
|
|
||||||
for current != nil && current.Type == html.ElementNode && depth < 5 {
|
for current != nil && current.Type == html.ElementNode && depth < 3 {
|
||||||
part := current.Data
|
part := current.Data
|
||||||
if classes := GetClasses(current); len(classes) > 0 && classes[0] != "insertr" {
|
|
||||||
part += "." + classes[0]
|
// Add first meaningful class (not insertr) for better differentiation
|
||||||
|
classes := GetClasses(current)
|
||||||
|
for _, class := range classes {
|
||||||
|
if class != "insertr" && class != "" {
|
||||||
|
part += "." + class
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pathParts = append([]string{part}, pathParts...)
|
pathParts = append([]string{part}, pathParts...)
|
||||||
current = current.Parent
|
current = current.Parent
|
||||||
depth++
|
depth++
|
||||||
@@ -203,7 +206,7 @@ func (g *IDGenerator) extractTextContent(node *html.Node, text *strings.Builder)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSiblingIndex returns the position of this element among its siblings of the same type
|
// getSiblingIndex returns the position of this element among its siblings of the same type and class
|
||||||
func (g *IDGenerator) getSiblingIndex(node *html.Node) int {
|
func (g *IDGenerator) getSiblingIndex(node *html.Node) int {
|
||||||
if node.Parent == nil {
|
if node.Parent == nil {
|
||||||
return 0
|
return 0
|
||||||
@@ -213,15 +216,42 @@ func (g *IDGenerator) getSiblingIndex(node *html.Node) int {
|
|||||||
tag := node.Data
|
tag := node.Data
|
||||||
classes := GetClasses(node)
|
classes := GetClasses(node)
|
||||||
|
|
||||||
|
// First try: match by tag + insertr class (most common case)
|
||||||
|
hasInsertr := false
|
||||||
|
for _, class := range classes {
|
||||||
|
if class == "insertr" {
|
||||||
|
hasInsertr = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for sibling := node.Parent.FirstChild; sibling != nil; sibling = sibling.NextSibling {
|
for sibling := node.Parent.FirstChild; sibling != nil; sibling = sibling.NextSibling {
|
||||||
if sibling.Type == html.ElementNode && sibling.Data == tag {
|
if sibling.Type == html.ElementNode && sibling.Data == tag {
|
||||||
siblingClasses := GetClasses(sibling)
|
siblingClasses := GetClasses(sibling)
|
||||||
// Check if classes match (for more precise positioning)
|
|
||||||
if g.classesMatch(classes, siblingClasses) {
|
// For insertr elements, match by tag + insertr class
|
||||||
if sibling == node {
|
if hasInsertr {
|
||||||
return index
|
siblingHasInsertr := false
|
||||||
|
for _, class := range siblingClasses {
|
||||||
|
if class == "insertr" {
|
||||||
|
siblingHasInsertr = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if siblingHasInsertr {
|
||||||
|
if sibling == node {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-insertr elements, match by exact class list
|
||||||
|
if g.classesMatch(classes, siblingClasses) {
|
||||||
|
if sibling == node {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
index++
|
||||||
}
|
}
|
||||||
index++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user