@reactiive/ennio 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EnnioCore.podspec +61 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/android/CMakeLists.txt +40 -0
- package/android/build.gradle +64 -0
- package/cpp/ElementMatcher.cpp +661 -0
- package/cpp/ElementMatcher.hpp +244 -0
- package/cpp/EnnioLog.hpp +182 -0
- package/cpp/HybridEnnio.cpp +1161 -0
- package/cpp/HybridEnnio.hpp +174 -0
- package/cpp/IdleMonitor.hpp +277 -0
- package/cpp/Protocol.cpp +135 -0
- package/cpp/Protocol.hpp +47 -0
- package/cpp/SelectorCriteria.hpp +281 -0
- package/cpp/SelectorParser.cpp +649 -0
- package/cpp/SelectorParser.hpp +94 -0
- package/cpp/ShadowTreeTraverser.cpp +305 -0
- package/cpp/ShadowTreeTraverser.hpp +142 -0
- package/cpp/TestIDRegistry.cpp +109 -0
- package/cpp/TestIDRegistry.hpp +84 -0
- package/dist/cli.js +16221 -0
- package/ios/EnnioAutoInit.mm +338 -0
- package/ios/EnnioDebugBanner.h +19 -0
- package/ios/EnnioDebugBanner.mm +178 -0
- package/ios/EnnioRuntimeHelper.h +264 -0
- package/ios/EnnioRuntimeHelper.mm +2443 -0
- package/lib/Ennio.nitro.d.ts +263 -0
- package/lib/Ennio.nitro.d.ts.map +1 -0
- package/lib/Ennio.nitro.js +2 -0
- package/lib/Ennio.nitro.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +45 -0
- package/lib/index.js.map +1 -0
- package/nitro.json +24 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/EnnioCore+autolinking.cmake +81 -0
- package/nitrogen/generated/android/EnnioCore+autolinking.gradle +27 -0
- package/nitrogen/generated/android/EnnioCoreOnLoad.cpp +49 -0
- package/nitrogen/generated/android/EnnioCoreOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ennio/EnnioCoreOnLoad.kt +35 -0
- package/nitrogen/generated/ios/EnnioCore+autolinking.rb +62 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/EnnioCoreAutolinking.mm +35 -0
- package/nitrogen/generated/ios/EnnioCoreAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/ExtendedElementInfo.hpp +118 -0
- package/nitrogen/generated/shared/c++/HybridEnnioSpec.cpp +44 -0
- package/nitrogen/generated/shared/c++/HybridEnnioSpec.hpp +93 -0
- package/nitrogen/generated/shared/c++/LayoutMetrics.hpp +103 -0
- package/nitrogen/generated/shared/c++/ScrollDirection.hpp +84 -0
- package/package.json +78 -0
- package/react-native.config.js +14 -0
- package/src/Ennio.nitro.ts +363 -0
- package/src/cli/hid-daemon.py +129 -0
- package/src/index.ts +72 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#include "SelectorParser.hpp"
|
|
2
|
+
#include "EnnioLog.hpp"
|
|
3
|
+
#include <stdexcept>
|
|
4
|
+
#include <sstream>
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <cctype>
|
|
7
|
+
|
|
8
|
+
namespace ennio {
|
|
9
|
+
|
|
10
|
+
static const char* LOG_TAG = "SelectorParser";
|
|
11
|
+
|
|
12
|
+
namespace {
|
|
13
|
+
// Helper to trim whitespace
|
|
14
|
+
std::string trim(const std::string& str) {
|
|
15
|
+
size_t start = str.find_first_not_of(" \t\n\r");
|
|
16
|
+
if (start == std::string::npos) return "";
|
|
17
|
+
size_t end = str.find_last_not_of(" \t\n\r");
|
|
18
|
+
return str.substr(start, end - start + 1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Find matching bracket/brace
|
|
22
|
+
size_t findMatchingBracket(const std::string& json, size_t start, char open, char close) {
|
|
23
|
+
int depth = 1;
|
|
24
|
+
bool inString = false;
|
|
25
|
+
bool escaped = false;
|
|
26
|
+
|
|
27
|
+
for (size_t i = start + 1; i < json.size(); i++) {
|
|
28
|
+
char c = json[i];
|
|
29
|
+
|
|
30
|
+
if (escaped) {
|
|
31
|
+
escaped = false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (c == '\\') {
|
|
36
|
+
escaped = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (c == '"') {
|
|
41
|
+
inString = !inString;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!inString) {
|
|
46
|
+
if (c == open) depth++;
|
|
47
|
+
else if (c == close) {
|
|
48
|
+
depth--;
|
|
49
|
+
if (depth == 0) return i;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return std::string::npos;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Locate `"key"` at the top level of the given JSON object. Skips
|
|
57
|
+
// occurrences nested inside child objects or arrays — without this,
|
|
58
|
+
// outer hasKey/extractString hits keys belonging to a nested
|
|
59
|
+
// selector (e.g. `rightOf: {id: ...}` would leak its `id` into the
|
|
60
|
+
// outer criteria.id and corrupt the match). Returns npos if absent.
|
|
61
|
+
size_t findTopLevelKey(const std::string& json, const std::string& key) {
|
|
62
|
+
std::string searchKey = "\"" + key + "\"";
|
|
63
|
+
int depth = 0;
|
|
64
|
+
bool inString = false;
|
|
65
|
+
bool escaped = false;
|
|
66
|
+
for (size_t i = 0; i < json.size(); i++) {
|
|
67
|
+
char c = json[i];
|
|
68
|
+
if (escaped) { escaped = false; continue; }
|
|
69
|
+
if (inString) {
|
|
70
|
+
if (c == '\\') { escaped = true; continue; }
|
|
71
|
+
if (c == '"') { inString = false; }
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c == '"') {
|
|
75
|
+
if (depth == 1 &&
|
|
76
|
+
json.compare(i, searchKey.size(), searchKey) == 0) {
|
|
77
|
+
// Confirm the next non-space char is ':' (key, not value).
|
|
78
|
+
size_t after = i + searchKey.size();
|
|
79
|
+
while (after < json.size() && std::isspace(json[after])) after++;
|
|
80
|
+
if (after < json.size() && json[after] == ':') {
|
|
81
|
+
return i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
inString = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (c == '{' || c == '[') depth++;
|
|
88
|
+
else if (c == '}' || c == ']') depth--;
|
|
89
|
+
}
|
|
90
|
+
return std::string::npos;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Escape string for JSON output
|
|
94
|
+
std::string escapeString(const std::string& str) {
|
|
95
|
+
std::ostringstream oss;
|
|
96
|
+
for (char c : str) {
|
|
97
|
+
switch (c) {
|
|
98
|
+
case '"': oss << "\\\""; break;
|
|
99
|
+
case '\\': oss << "\\\\"; break;
|
|
100
|
+
case '\n': oss << "\\n"; break;
|
|
101
|
+
case '\r': oss << "\\r"; break;
|
|
102
|
+
case '\t': oss << "\\t"; break;
|
|
103
|
+
default: oss << c;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return oss.str();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
SelectorCriteria SelectorParser::parse(const std::string& json) {
|
|
111
|
+
std::string trimmed = trim(json);
|
|
112
|
+
|
|
113
|
+
if (trimmed.empty()) {
|
|
114
|
+
throw std::runtime_error("Empty selector");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Simple string selector (just testID)
|
|
118
|
+
if (trimmed[0] == '"') {
|
|
119
|
+
// Extract string value
|
|
120
|
+
size_t end = trimmed.find('"', 1);
|
|
121
|
+
if (end == std::string::npos) {
|
|
122
|
+
throw std::runtime_error("Invalid selector: unterminated string");
|
|
123
|
+
}
|
|
124
|
+
std::string id = trimmed.substr(1, end - 1);
|
|
125
|
+
return SelectorCriteria::fromId(id);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Object selector
|
|
129
|
+
if (trimmed[0] == '{') {
|
|
130
|
+
return parseObject(trimmed);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Plain string without quotes (legacy testID format)
|
|
134
|
+
return SelectorCriteria::fromId(trimmed);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
SelectorCriteria SelectorParser::parseObject(const std::string& json) {
|
|
138
|
+
SelectorCriteria criteria;
|
|
139
|
+
ENNIO_LOG_TRACE(LOG_TAG, ENNIO_LOG_FMT("parseObject: json=" << json));
|
|
140
|
+
|
|
141
|
+
// Parse id
|
|
142
|
+
if (hasKey(json, "id")) {
|
|
143
|
+
criteria.id = extractString(json, "id");
|
|
144
|
+
ENNIO_LOG_DEBUG(LOG_TAG, ENNIO_LOG_FMT("parseObject: id=" << *criteria.id));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Parse text (can be string or object with pattern/mode)
|
|
148
|
+
if (hasKey(json, "text")) {
|
|
149
|
+
std::string textValue = extractString(json, "text");
|
|
150
|
+
ENNIO_LOG_DEBUG(LOG_TAG, ENNIO_LOG_FMT("parseObject: text=" << textValue));
|
|
151
|
+
TextMatchMode mode = TextMatchMode::Exact;
|
|
152
|
+
|
|
153
|
+
// Check for textMatchMode
|
|
154
|
+
if (hasKey(json, "textMatchMode")) {
|
|
155
|
+
mode = parseTextMatchMode(extractString(json, "textMatchMode"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
criteria.text = TextMatcher{textValue, mode};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Parse index
|
|
162
|
+
auto indexOpt = extractNumber(json, "index");
|
|
163
|
+
if (indexOpt) {
|
|
164
|
+
criteria.index = static_cast<int>(*indexOpt);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Parse point
|
|
168
|
+
if (hasKey(json, "point")) {
|
|
169
|
+
std::string pointStr = extractString(json, "point");
|
|
170
|
+
if (!pointStr.empty()) {
|
|
171
|
+
criteria.point = parsePoint(pointStr);
|
|
172
|
+
} else {
|
|
173
|
+
// Try object format
|
|
174
|
+
std::string pointObj = extractObject(json, "point");
|
|
175
|
+
if (!pointObj.empty()) {
|
|
176
|
+
Point p;
|
|
177
|
+
auto x = extractNumber(pointObj, "x");
|
|
178
|
+
auto y = extractNumber(pointObj, "y");
|
|
179
|
+
if (x) p.x = static_cast<float>(*x);
|
|
180
|
+
if (y) p.y = static_cast<float>(*y);
|
|
181
|
+
p.isPercentage = false;
|
|
182
|
+
criteria.point = p;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Parse state selectors
|
|
188
|
+
criteria.enabled = extractBool(json, "enabled");
|
|
189
|
+
criteria.checked = extractBool(json, "checked");
|
|
190
|
+
criteria.focused = extractBool(json, "focused");
|
|
191
|
+
criteria.selected = extractBool(json, "selected");
|
|
192
|
+
|
|
193
|
+
// Parse spatial selectors
|
|
194
|
+
criteria.below = parseNested(json, "below");
|
|
195
|
+
criteria.above = parseNested(json, "above");
|
|
196
|
+
criteria.leftOf = parseNested(json, "leftOf");
|
|
197
|
+
criteria.rightOf = parseNested(json, "rightOf");
|
|
198
|
+
|
|
199
|
+
// Parse hierarchical selectors
|
|
200
|
+
criteria.containsChild = parseNested(json, "containsChild");
|
|
201
|
+
criteria.childOf = parseNested(json, "childOf");
|
|
202
|
+
|
|
203
|
+
// Parse containsDescendants array
|
|
204
|
+
auto descendants = extractArray(json, "containsDescendants");
|
|
205
|
+
for (const auto& desc : descendants) {
|
|
206
|
+
auto parsed = std::make_shared<SelectorCriteria>(parse(desc));
|
|
207
|
+
criteria.containsDescendants.push_back(parsed);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Parse dimension selectors
|
|
211
|
+
auto widthOpt = extractNumber(json, "width");
|
|
212
|
+
if (widthOpt) {
|
|
213
|
+
criteria.width = static_cast<float>(*widthOpt);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
auto heightOpt = extractNumber(json, "height");
|
|
217
|
+
if (heightOpt) {
|
|
218
|
+
criteria.height = static_cast<float>(*heightOpt);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
auto toleranceOpt = extractNumber(json, "tolerance");
|
|
222
|
+
if (toleranceOpt) {
|
|
223
|
+
criteria.tolerance = static_cast<float>(*toleranceOpt);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Parse traits array
|
|
227
|
+
auto traitsArray = extractArray(json, "traits");
|
|
228
|
+
for (const auto& trait : traitsArray) {
|
|
229
|
+
std::string t = trim(trait);
|
|
230
|
+
// Remove quotes if present
|
|
231
|
+
if (t.size() >= 2 && t[0] == '"') {
|
|
232
|
+
t = t.substr(1, t.size() - 2);
|
|
233
|
+
}
|
|
234
|
+
criteria.traits.push_back(parseTrait(t));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return criteria;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
SelectorCriteriaPtr SelectorParser::parseNested(const std::string& json, const std::string& key) {
|
|
241
|
+
std::string nested = extractObject(json, key);
|
|
242
|
+
if (nested.empty()) {
|
|
243
|
+
return nullptr;
|
|
244
|
+
}
|
|
245
|
+
return std::make_shared<SelectorCriteria>(parse(nested));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
std::string SelectorParser::extractString(const std::string& json, const std::string& key) {
|
|
249
|
+
std::string searchKey = "\"" + key + "\"";
|
|
250
|
+
size_t keyPos = findTopLevelKey(json, key);
|
|
251
|
+
if (keyPos == std::string::npos) {
|
|
252
|
+
return "";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
size_t colonPos = json.find(':', keyPos + searchKey.size());
|
|
256
|
+
if (colonPos == std::string::npos) {
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Find the value start
|
|
261
|
+
size_t valueStart = colonPos + 1;
|
|
262
|
+
while (valueStart < json.size() && std::isspace(json[valueStart])) {
|
|
263
|
+
valueStart++;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (valueStart >= json.size()) {
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If it's a string value, parse with proper escape handling
|
|
271
|
+
if (json[valueStart] == '"') {
|
|
272
|
+
std::string result;
|
|
273
|
+
bool escaped = false;
|
|
274
|
+
for (size_t i = valueStart + 1; i < json.size(); i++) {
|
|
275
|
+
char c = json[i];
|
|
276
|
+
if (escaped) {
|
|
277
|
+
// Handle escape sequences
|
|
278
|
+
switch (c) {
|
|
279
|
+
case '"': result += '"'; break;
|
|
280
|
+
case '\\': result += '\\'; break;
|
|
281
|
+
case 'n': result += '\n'; break;
|
|
282
|
+
case 'r': result += '\r'; break;
|
|
283
|
+
case 't': result += '\t'; break;
|
|
284
|
+
default: result += c; break;
|
|
285
|
+
}
|
|
286
|
+
escaped = false;
|
|
287
|
+
} else if (c == '\\') {
|
|
288
|
+
escaped = true;
|
|
289
|
+
} else if (c == '"') {
|
|
290
|
+
// End of string
|
|
291
|
+
break;
|
|
292
|
+
} else {
|
|
293
|
+
result += c;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return "";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
std::optional<bool> SelectorParser::extractBool(const std::string& json, const std::string& key) {
|
|
303
|
+
std::string searchKey = "\"" + key + "\"";
|
|
304
|
+
size_t keyPos = findTopLevelKey(json, key);
|
|
305
|
+
if (keyPos == std::string::npos) {
|
|
306
|
+
return std::nullopt;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
size_t colonPos = json.find(':', keyPos + searchKey.size());
|
|
310
|
+
if (colonPos == std::string::npos) {
|
|
311
|
+
return std::nullopt;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
size_t valueStart = colonPos + 1;
|
|
315
|
+
while (valueStart < json.size() && std::isspace(json[valueStart])) {
|
|
316
|
+
valueStart++;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (json.compare(valueStart, 4, "true") == 0) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
if (json.compare(valueStart, 5, "false") == 0) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return std::nullopt;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
std::optional<double> SelectorParser::extractNumber(const std::string& json, const std::string& key) {
|
|
330
|
+
std::string searchKey = "\"" + key + "\"";
|
|
331
|
+
size_t keyPos = findTopLevelKey(json, key);
|
|
332
|
+
if (keyPos == std::string::npos) {
|
|
333
|
+
return std::nullopt;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
size_t colonPos = json.find(':', keyPos + searchKey.size());
|
|
337
|
+
if (colonPos == std::string::npos) {
|
|
338
|
+
return std::nullopt;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
size_t valueStart = colonPos + 1;
|
|
342
|
+
while (valueStart < json.size() && std::isspace(json[valueStart])) {
|
|
343
|
+
valueStart++;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check if it's a number
|
|
347
|
+
if (valueStart < json.size() && (std::isdigit(json[valueStart]) || json[valueStart] == '-' || json[valueStart] == '.')) {
|
|
348
|
+
size_t valueEnd = valueStart;
|
|
349
|
+
while (valueEnd < json.size() && (std::isdigit(json[valueEnd]) || json[valueEnd] == '.' || json[valueEnd] == '-' || json[valueEnd] == 'e' || json[valueEnd] == 'E')) {
|
|
350
|
+
valueEnd++;
|
|
351
|
+
}
|
|
352
|
+
std::string numStr = json.substr(valueStart, valueEnd - valueStart);
|
|
353
|
+
try {
|
|
354
|
+
return std::stod(numStr);
|
|
355
|
+
} catch (...) {
|
|
356
|
+
return std::nullopt;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return std::nullopt;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
std::string SelectorParser::extractObject(const std::string& json, const std::string& key) {
|
|
364
|
+
std::string searchKey = "\"" + key + "\"";
|
|
365
|
+
size_t keyPos = findTopLevelKey(json, key);
|
|
366
|
+
if (keyPos == std::string::npos) {
|
|
367
|
+
return "";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
size_t colonPos = json.find(':', keyPos + searchKey.size());
|
|
371
|
+
if (colonPos == std::string::npos) {
|
|
372
|
+
return "";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
size_t valueStart = colonPos + 1;
|
|
376
|
+
while (valueStart < json.size() && std::isspace(json[valueStart])) {
|
|
377
|
+
valueStart++;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (valueStart >= json.size() || json[valueStart] != '{') {
|
|
381
|
+
return "";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
size_t valueEnd = findMatchingBracket(json, valueStart, '{', '}');
|
|
385
|
+
if (valueEnd == std::string::npos) {
|
|
386
|
+
return "";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return json.substr(valueStart, valueEnd - valueStart + 1);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
std::vector<std::string> SelectorParser::extractArray(const std::string& json, const std::string& key) {
|
|
393
|
+
std::vector<std::string> result;
|
|
394
|
+
|
|
395
|
+
std::string searchKey = "\"" + key + "\"";
|
|
396
|
+
size_t keyPos = findTopLevelKey(json, key);
|
|
397
|
+
if (keyPos == std::string::npos) {
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
size_t colonPos = json.find(':', keyPos + searchKey.size());
|
|
402
|
+
if (colonPos == std::string::npos) {
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
size_t valueStart = colonPos + 1;
|
|
407
|
+
while (valueStart < json.size() && std::isspace(json[valueStart])) {
|
|
408
|
+
valueStart++;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (valueStart >= json.size() || json[valueStart] != '[') {
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
size_t arrayEnd = findMatchingBracket(json, valueStart, '[', ']');
|
|
416
|
+
if (arrayEnd == std::string::npos) {
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Parse array elements
|
|
421
|
+
size_t pos = valueStart + 1;
|
|
422
|
+
while (pos < arrayEnd) {
|
|
423
|
+
// Skip whitespace
|
|
424
|
+
while (pos < arrayEnd && std::isspace(json[pos])) pos++;
|
|
425
|
+
if (pos >= arrayEnd) break;
|
|
426
|
+
|
|
427
|
+
// Skip comma
|
|
428
|
+
if (json[pos] == ',') {
|
|
429
|
+
pos++;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Parse element
|
|
434
|
+
if (json[pos] == '{') {
|
|
435
|
+
size_t end = findMatchingBracket(json, pos, '{', '}');
|
|
436
|
+
if (end != std::string::npos) {
|
|
437
|
+
result.push_back(json.substr(pos, end - pos + 1));
|
|
438
|
+
pos = end + 1;
|
|
439
|
+
} else {
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
} else if (json[pos] == '"') {
|
|
443
|
+
size_t end = pos + 1;
|
|
444
|
+
while (end < arrayEnd && !(json[end] == '"' && json[end - 1] != '\\')) {
|
|
445
|
+
end++;
|
|
446
|
+
}
|
|
447
|
+
result.push_back(json.substr(pos, end - pos + 1));
|
|
448
|
+
pos = end + 1;
|
|
449
|
+
} else {
|
|
450
|
+
// Skip to next comma or end
|
|
451
|
+
size_t end = pos;
|
|
452
|
+
while (end < arrayEnd && json[end] != ',' && json[end] != ']') {
|
|
453
|
+
end++;
|
|
454
|
+
}
|
|
455
|
+
std::string element = trim(json.substr(pos, end - pos));
|
|
456
|
+
if (!element.empty()) {
|
|
457
|
+
result.push_back(element);
|
|
458
|
+
}
|
|
459
|
+
pos = end;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
bool SelectorParser::hasKey(const std::string& json, const std::string& key) {
|
|
467
|
+
return findTopLevelKey(json, key) != std::string::npos;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
TextMatchMode SelectorParser::parseTextMatchMode(const std::string& mode) {
|
|
471
|
+
if (mode == "contains") return TextMatchMode::Contains;
|
|
472
|
+
if (mode == "regex") return TextMatchMode::Regex;
|
|
473
|
+
if (mode == "startsWith") return TextMatchMode::StartsWith;
|
|
474
|
+
if (mode == "endsWith") return TextMatchMode::EndsWith;
|
|
475
|
+
return TextMatchMode::Exact;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
Trait SelectorParser::parseTrait(const std::string& trait) {
|
|
479
|
+
if (trait == "long-text" || trait == "longText") return Trait::LongText;
|
|
480
|
+
if (trait == "square") return Trait::Square;
|
|
481
|
+
return Trait::Text; // default
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
Point SelectorParser::parsePoint(const std::string& value) {
|
|
485
|
+
Point point{0, 0, false};
|
|
486
|
+
|
|
487
|
+
// Check if percentage format: "50%,50%"
|
|
488
|
+
if (value.find('%') != std::string::npos) {
|
|
489
|
+
point.isPercentage = true;
|
|
490
|
+
size_t comma = value.find(',');
|
|
491
|
+
if (comma != std::string::npos) {
|
|
492
|
+
std::string xStr = value.substr(0, comma);
|
|
493
|
+
std::string yStr = value.substr(comma + 1);
|
|
494
|
+
|
|
495
|
+
// Remove % signs
|
|
496
|
+
xStr.erase(std::remove(xStr.begin(), xStr.end(), '%'), xStr.end());
|
|
497
|
+
yStr.erase(std::remove(yStr.begin(), yStr.end(), '%'), yStr.end());
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
point.x = std::stof(trim(xStr));
|
|
501
|
+
point.y = std::stof(trim(yStr));
|
|
502
|
+
} catch (...) {}
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
// Absolute format: "100,200"
|
|
506
|
+
size_t comma = value.find(',');
|
|
507
|
+
if (comma != std::string::npos) {
|
|
508
|
+
try {
|
|
509
|
+
point.x = std::stof(trim(value.substr(0, comma)));
|
|
510
|
+
point.y = std::stof(trim(value.substr(comma + 1)));
|
|
511
|
+
} catch (...) {}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return point;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
std::string SelectorParser::toJSON(const SelectorCriteria& criteria) {
|
|
519
|
+
std::ostringstream oss;
|
|
520
|
+
oss << "{";
|
|
521
|
+
|
|
522
|
+
bool first = true;
|
|
523
|
+
auto addComma = [&]() {
|
|
524
|
+
if (!first) oss << ",";
|
|
525
|
+
first = false;
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
if (criteria.id) {
|
|
529
|
+
addComma();
|
|
530
|
+
oss << "\"id\":\"" << escapeString(*criteria.id) << "\"";
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (criteria.text) {
|
|
534
|
+
addComma();
|
|
535
|
+
oss << "\"text\":\"" << escapeString(criteria.text->pattern) << "\"";
|
|
536
|
+
if (criteria.text->mode != TextMatchMode::Exact) {
|
|
537
|
+
oss << ",\"textMatchMode\":\"";
|
|
538
|
+
switch (criteria.text->mode) {
|
|
539
|
+
case TextMatchMode::Contains: oss << "contains"; break;
|
|
540
|
+
case TextMatchMode::Regex: oss << "regex"; break;
|
|
541
|
+
case TextMatchMode::StartsWith: oss << "startsWith"; break;
|
|
542
|
+
case TextMatchMode::EndsWith: oss << "endsWith"; break;
|
|
543
|
+
default: oss << "exact";
|
|
544
|
+
}
|
|
545
|
+
oss << "\"";
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (criteria.index) {
|
|
550
|
+
addComma();
|
|
551
|
+
oss << "\"index\":" << *criteria.index;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (criteria.enabled) {
|
|
555
|
+
addComma();
|
|
556
|
+
oss << "\"enabled\":" << (*criteria.enabled ? "true" : "false");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (criteria.checked) {
|
|
560
|
+
addComma();
|
|
561
|
+
oss << "\"checked\":" << (*criteria.checked ? "true" : "false");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (criteria.focused) {
|
|
565
|
+
addComma();
|
|
566
|
+
oss << "\"focused\":" << (*criteria.focused ? "true" : "false");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (criteria.selected) {
|
|
570
|
+
addComma();
|
|
571
|
+
oss << "\"selected\":" << (*criteria.selected ? "true" : "false");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (criteria.below) {
|
|
575
|
+
addComma();
|
|
576
|
+
oss << "\"below\":" << toJSON(*criteria.below);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (criteria.above) {
|
|
580
|
+
addComma();
|
|
581
|
+
oss << "\"above\":" << toJSON(*criteria.above);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (criteria.leftOf) {
|
|
585
|
+
addComma();
|
|
586
|
+
oss << "\"leftOf\":" << toJSON(*criteria.leftOf);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (criteria.rightOf) {
|
|
590
|
+
addComma();
|
|
591
|
+
oss << "\"rightOf\":" << toJSON(*criteria.rightOf);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (criteria.containsChild) {
|
|
595
|
+
addComma();
|
|
596
|
+
oss << "\"containsChild\":" << toJSON(*criteria.containsChild);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (criteria.childOf) {
|
|
600
|
+
addComma();
|
|
601
|
+
oss << "\"childOf\":" << toJSON(*criteria.childOf);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!criteria.containsDescendants.empty()) {
|
|
605
|
+
addComma();
|
|
606
|
+
oss << "\"containsDescendants\":[";
|
|
607
|
+
for (size_t i = 0; i < criteria.containsDescendants.size(); i++) {
|
|
608
|
+
if (i > 0) oss << ",";
|
|
609
|
+
oss << toJSON(*criteria.containsDescendants[i]);
|
|
610
|
+
}
|
|
611
|
+
oss << "]";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (criteria.width) {
|
|
615
|
+
addComma();
|
|
616
|
+
oss << "\"width\":" << *criteria.width;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (criteria.height) {
|
|
620
|
+
addComma();
|
|
621
|
+
oss << "\"height\":" << *criteria.height;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (criteria.tolerance) {
|
|
625
|
+
addComma();
|
|
626
|
+
oss << "\"tolerance\":" << *criteria.tolerance;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!criteria.traits.empty()) {
|
|
630
|
+
addComma();
|
|
631
|
+
oss << "\"traits\":[";
|
|
632
|
+
for (size_t i = 0; i < criteria.traits.size(); i++) {
|
|
633
|
+
if (i > 0) oss << ",";
|
|
634
|
+
oss << "\"";
|
|
635
|
+
switch (criteria.traits[i]) {
|
|
636
|
+
case Trait::Text: oss << "text"; break;
|
|
637
|
+
case Trait::LongText: oss << "long-text"; break;
|
|
638
|
+
case Trait::Square: oss << "square"; break;
|
|
639
|
+
}
|
|
640
|
+
oss << "\"";
|
|
641
|
+
}
|
|
642
|
+
oss << "]";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
oss << "}";
|
|
646
|
+
return oss.str();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
} // namespace ennio
|