File size: 4,890 Bytes
6a7089a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Package selector provides a unified element targeting system.
//
// Instead of separate ref, css, xpath, text, and semantic fields,
// callers use a single selector string. The type is auto-detected
// from the value or an explicit prefix:
//
//	"e5"              β†’ Ref   (element ref from snapshot)
//	"css:#login"      β†’ CSS   (explicit prefix)
//	"#login"          β†’ CSS   (auto-detected)
//	"xpath://div"     β†’ XPath
//	"text:Submit"     β†’ Text  (match by visible text)
//	"find:login btn"  β†’ Semantic (natural-language query)
//
// Bare strings that look like CSS selectors (start with ., #, [,
// or contain tag-like patterns) are treated as CSS. Everything else
// without a prefix is treated as a ref if it matches the eN pattern,
// or as CSS otherwise.
package selector

import (
	"fmt"
	"strings"
)

// Kind represents the type of a selector.
type Kind string

const (
	KindNone     Kind = ""
	KindRef      Kind = "ref"
	KindCSS      Kind = "css"
	KindXPath    Kind = "xpath"
	KindText     Kind = "text"
	KindSemantic Kind = "semantic"
)

// Selector is a parsed, unified element selector.
type Selector struct {
	Kind  Kind   `json:"kind"`
	Value string `json:"value"`
}

// String returns the canonical string representation with prefix.
func (s Selector) String() string {
	switch s.Kind {
	case KindRef:
		return s.Value
	case KindCSS:
		return "css:" + s.Value
	case KindXPath:
		return "xpath:" + s.Value
	case KindText:
		return "text:" + s.Value
	case KindSemantic:
		return "find:" + s.Value
	default:
		return s.Value
	}
}

// IsEmpty returns true if the selector has no value.
func (s Selector) IsEmpty() bool {
	return s.Value == ""
}

// Parse interprets a selector string and returns a typed Selector.
//
// Explicit prefixes take priority:
//
//	"css:..."    β†’ CSS
//	"xpath:..."  β†’ XPath
//	"text:..."   β†’ Text
//	"find:..."   β†’ Semantic
//	"ref:..."    β†’ Ref (optional explicit prefix)
//
// Without a prefix, auto-detection applies:
//
//	"e123"       β†’ Ref (matches /^e\d+$/)
//	"#id"        β†’ CSS
//	".class"     β†’ CSS
//	"[attr]"     β†’ CSS
//	"tag.class"  β†’ CSS
//	"//xpath"    β†’ XPath
//	everything else β†’ CSS (safest default for backward compat)
func Parse(s string) Selector {
	s = strings.TrimSpace(s)
	if s == "" {
		return Selector{}
	}

	// Explicit prefixes
	if after, ok := cutPrefix(s, "css:"); ok {
		return Selector{Kind: KindCSS, Value: after}
	}
	if after, ok := cutPrefix(s, "xpath:"); ok {
		return Selector{Kind: KindXPath, Value: after}
	}
	if after, ok := cutPrefix(s, "text:"); ok {
		return Selector{Kind: KindText, Value: after}
	}
	if after, ok := cutPrefix(s, "find:"); ok {
		return Selector{Kind: KindSemantic, Value: after}
	}
	if after, ok := cutPrefix(s, "ref:"); ok {
		return Selector{Kind: KindRef, Value: after}
	}

	// Auto-detect: XPath
	if strings.HasPrefix(s, "//") || strings.HasPrefix(s, "(//") {
		return Selector{Kind: KindXPath, Value: s}
	}

	// Auto-detect: Ref (e.g. e5, e123)
	if IsRef(s) {
		return Selector{Kind: KindRef, Value: s}
	}

	// Everything else is CSS (backward compatible default)
	return Selector{Kind: KindCSS, Value: s}
}

// IsRef returns true if the string matches the element ref pattern (e.g. "e5", "e123").
func IsRef(s string) bool {
	if len(s) < 2 || s[0] != 'e' {
		return false
	}
	for i := 1; i < len(s); i++ {
		if s[i] < '0' || s[i] > '9' {
			return false
		}
	}
	return true
}

// FromRef creates a Selector from a ref string.
func FromRef(ref string) Selector {
	if ref == "" {
		return Selector{}
	}
	return Selector{Kind: KindRef, Value: ref}
}

// FromCSS creates a Selector from a CSS selector string.
func FromCSS(css string) Selector {
	if css == "" {
		return Selector{}
	}
	return Selector{Kind: KindCSS, Value: css}
}

// FromXPath creates a Selector from an XPath expression.
func FromXPath(xpath string) Selector {
	if xpath == "" {
		return Selector{}
	}
	return Selector{Kind: KindXPath, Value: xpath}
}

// FromText creates a Selector from a text content query.
func FromText(text string) Selector {
	if text == "" {
		return Selector{}
	}
	return Selector{Kind: KindText, Value: text}
}

// FromSemantic creates a Selector from a semantic/natural-language query.
func FromSemantic(query string) Selector {
	if query == "" {
		return Selector{}
	}
	return Selector{Kind: KindSemantic, Value: query}
}

// Validate returns an error if the selector is invalid.
func (s Selector) Validate() error {
	if s.IsEmpty() {
		return fmt.Errorf("empty selector")
	}
	switch s.Kind {
	case KindRef, KindCSS, KindXPath, KindText, KindSemantic:
		return nil
	default:
		return fmt.Errorf("unknown selector kind: %q", s.Kind)
	}
}

// cutPrefix is a helper for strings.CutPrefix (available in Go 1.20+).
func cutPrefix(s, prefix string) (string, bool) {
	if strings.HasPrefix(s, prefix) {
		return s[len(prefix):], true
	}
	return s, false
}