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
}
|