@lokascript/semantic 1.2.0 → 1.3.0
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/dist/core.d.ts +1246 -0
- package/dist/core.js +3073 -0
- package/dist/core.js.map +1 -0
- package/dist/languages/bn.d.ts +33 -0
- package/dist/languages/bn.js +1101 -0
- package/dist/languages/bn.js.map +1 -0
- package/dist/languages/es-MX.d.ts +23 -0
- package/dist/languages/es-MX.js +1676 -0
- package/dist/languages/es-MX.js.map +1 -0
- package/dist/languages/es.d.ts +3 -42
- package/dist/languages/he.d.ts +70 -0
- package/dist/languages/he.js +1331 -0
- package/dist/languages/he.js.map +1 -0
- package/dist/languages/hi.d.ts +36 -0
- package/dist/languages/hi.js +1162 -0
- package/dist/languages/hi.js.map +1 -0
- package/dist/languages/it.d.ts +53 -0
- package/dist/languages/it.js +1600 -0
- package/dist/languages/it.js.map +1 -0
- package/dist/languages/ms.d.ts +32 -0
- package/dist/languages/ms.js +1043 -0
- package/dist/languages/ms.js.map +1 -0
- package/dist/languages/pl.d.ts +37 -0
- package/dist/languages/pl.js +1331 -0
- package/dist/languages/pl.js.map +1 -0
- package/dist/languages/ru.d.ts +37 -0
- package/dist/languages/ru.js +1356 -0
- package/dist/languages/ru.js.map +1 -0
- package/dist/languages/th.d.ts +35 -0
- package/dist/languages/th.js +1076 -0
- package/dist/languages/th.js.map +1 -0
- package/dist/languages/tl.d.ts +32 -0
- package/dist/languages/tl.js +1034 -0
- package/dist/languages/tl.js.map +1 -0
- package/dist/languages/uk.d.ts +37 -0
- package/dist/languages/uk.js +1356 -0
- package/dist/languages/uk.js.map +1 -0
- package/dist/languages/vi.d.ts +59 -0
- package/dist/languages/vi.js +1220 -0
- package/dist/languages/vi.js.map +1 -0
- package/dist/spanish-BedpM-NU.d.ts +43 -0
- package/package.json +53 -1
- package/src/core.ts +155 -0
package/dist/core.js
ADDED
|
@@ -0,0 +1,3073 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
function createSelector(value) {
|
|
3
|
+
let selectorKind = "complex";
|
|
4
|
+
if (value.startsWith("#") && !value.includes(" ")) {
|
|
5
|
+
selectorKind = "id";
|
|
6
|
+
} else if (value.startsWith(".") && !value.includes(" ")) {
|
|
7
|
+
selectorKind = "class";
|
|
8
|
+
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
9
|
+
selectorKind = "attribute";
|
|
10
|
+
} else if (/^[a-z][a-z0-9]*$/i.test(value)) {
|
|
11
|
+
selectorKind = "element";
|
|
12
|
+
}
|
|
13
|
+
return { type: "selector", value, selectorKind };
|
|
14
|
+
}
|
|
15
|
+
function createLiteral(value, dataType) {
|
|
16
|
+
const result = { type: "literal", value };
|
|
17
|
+
if (dataType !== void 0) {
|
|
18
|
+
return { type: "literal", value, dataType };
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
function createReference(value) {
|
|
23
|
+
return { type: "reference", value };
|
|
24
|
+
}
|
|
25
|
+
function createPropertyPath(object, property) {
|
|
26
|
+
return { type: "property-path", object, property };
|
|
27
|
+
}
|
|
28
|
+
function createCommandNode(action, roles, metadata) {
|
|
29
|
+
const node = {
|
|
30
|
+
kind: "command",
|
|
31
|
+
action,
|
|
32
|
+
roles: new Map(Object.entries(roles))
|
|
33
|
+
};
|
|
34
|
+
if (metadata !== void 0) {
|
|
35
|
+
return { ...node, metadata };
|
|
36
|
+
}
|
|
37
|
+
return node;
|
|
38
|
+
}
|
|
39
|
+
function createEventHandler(event, body, modifiers, metadata, parameterNames) {
|
|
40
|
+
const roles = /* @__PURE__ */ new Map();
|
|
41
|
+
roles.set("event", event);
|
|
42
|
+
const node = {
|
|
43
|
+
kind: "event-handler",
|
|
44
|
+
action: "on",
|
|
45
|
+
roles,
|
|
46
|
+
body
|
|
47
|
+
};
|
|
48
|
+
if (modifiers !== void 0) {
|
|
49
|
+
node.eventModifiers = modifiers;
|
|
50
|
+
}
|
|
51
|
+
if (metadata !== void 0) {
|
|
52
|
+
node.metadata = metadata;
|
|
53
|
+
}
|
|
54
|
+
if (parameterNames !== void 0 && parameterNames.length > 0) {
|
|
55
|
+
node.parameterNames = parameterNames;
|
|
56
|
+
}
|
|
57
|
+
return node;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/parser/utils/type-validation.ts
|
|
61
|
+
function isTypeCompatible(actualType, expectedTypes) {
|
|
62
|
+
if (!expectedTypes || expectedTypes.length === 0) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (expectedTypes.includes(actualType)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (expectedTypes.includes("expression")) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (actualType === "property-path") {
|
|
72
|
+
return expectedTypes.some((t) => ["selector", "reference", "expression"].includes(t));
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/parser/utils/possessive-keywords.ts
|
|
78
|
+
function getPossessiveReference(profile, keyword) {
|
|
79
|
+
return profile.possessive?.keywords?.[keyword];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/registry.ts
|
|
83
|
+
var tokenizers = /* @__PURE__ */ new Map();
|
|
84
|
+
var profiles = /* @__PURE__ */ new Map();
|
|
85
|
+
var patternCache = /* @__PURE__ */ new Map();
|
|
86
|
+
var patternGenerator = null;
|
|
87
|
+
function deepMerge(base, variant) {
|
|
88
|
+
const result = { ...base };
|
|
89
|
+
for (const key of Object.keys(variant)) {
|
|
90
|
+
const variantValue = variant[key];
|
|
91
|
+
const baseValue = base[key];
|
|
92
|
+
if (variantValue === void 0) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (typeof variantValue === "object" && variantValue !== null && !Array.isArray(variantValue) && typeof baseValue === "object" && baseValue !== null && !Array.isArray(baseValue)) {
|
|
96
|
+
result[key] = deepMerge(
|
|
97
|
+
baseValue,
|
|
98
|
+
variantValue
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
result[key] = variantValue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function mergeProfiles(base, variant) {
|
|
107
|
+
return deepMerge(base, variant);
|
|
108
|
+
}
|
|
109
|
+
function resolveProfile(profile) {
|
|
110
|
+
if (!profile.extends) {
|
|
111
|
+
return profile;
|
|
112
|
+
}
|
|
113
|
+
const baseProfile = profiles.get(profile.extends);
|
|
114
|
+
if (!baseProfile) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[Registry] Profile '${profile.code}' extends '${profile.extends}' but base is not registered. Make sure to import the base language before the variant.`
|
|
117
|
+
);
|
|
118
|
+
return profile;
|
|
119
|
+
}
|
|
120
|
+
const resolvedBase = resolveProfile(baseProfile);
|
|
121
|
+
return mergeProfiles(resolvedBase, profile);
|
|
122
|
+
}
|
|
123
|
+
function registerLanguage(code, tokenizer, profile) {
|
|
124
|
+
tokenizers.set(code, tokenizer);
|
|
125
|
+
profiles.set(code, profile);
|
|
126
|
+
patternCache.delete(code);
|
|
127
|
+
}
|
|
128
|
+
function setPatternGenerator(generator) {
|
|
129
|
+
patternGenerator = generator;
|
|
130
|
+
}
|
|
131
|
+
var registeredPatterns = /* @__PURE__ */ new Map();
|
|
132
|
+
function registerPatterns(code, patterns) {
|
|
133
|
+
registeredPatterns.set(code, patterns);
|
|
134
|
+
patternCache.delete(code);
|
|
135
|
+
}
|
|
136
|
+
function getBaseLanguageCode(code) {
|
|
137
|
+
return code.split("-")[0];
|
|
138
|
+
}
|
|
139
|
+
function isLanguageVariant(code) {
|
|
140
|
+
return code.includes("-");
|
|
141
|
+
}
|
|
142
|
+
function getTokenizer(code) {
|
|
143
|
+
let tokenizer = tokenizers.get(code);
|
|
144
|
+
if (!tokenizer && isLanguageVariant(code)) {
|
|
145
|
+
const baseCode = getBaseLanguageCode(code);
|
|
146
|
+
tokenizer = tokenizers.get(baseCode);
|
|
147
|
+
}
|
|
148
|
+
if (!tokenizer) {
|
|
149
|
+
const registered = Array.from(tokenizers.keys()).join(", ");
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Language '${code}' is not registered. Registered languages: ${registered || "none"}. Import the language module first: import '@lokascript/semantic/languages/${code}';`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return tokenizer;
|
|
155
|
+
}
|
|
156
|
+
function getProfile(code) {
|
|
157
|
+
let profile = profiles.get(code);
|
|
158
|
+
if (!profile && isLanguageVariant(code)) {
|
|
159
|
+
const baseCode = getBaseLanguageCode(code);
|
|
160
|
+
profile = profiles.get(baseCode);
|
|
161
|
+
}
|
|
162
|
+
if (!profile) {
|
|
163
|
+
const registered = Array.from(profiles.keys()).join(", ");
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Language profile '${code}' is not registered. Registered languages: ${registered || "none"}. Import the language module first: import '@lokascript/semantic/languages/${code}';`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return resolveProfile(profile);
|
|
169
|
+
}
|
|
170
|
+
function tryGetTokenizer(code) {
|
|
171
|
+
let tokenizer = tokenizers.get(code);
|
|
172
|
+
if (!tokenizer && isLanguageVariant(code)) {
|
|
173
|
+
tokenizer = tokenizers.get(getBaseLanguageCode(code));
|
|
174
|
+
}
|
|
175
|
+
return tokenizer;
|
|
176
|
+
}
|
|
177
|
+
function tryGetProfile(code) {
|
|
178
|
+
let profile = profiles.get(code);
|
|
179
|
+
if (!profile && isLanguageVariant(code)) {
|
|
180
|
+
profile = profiles.get(getBaseLanguageCode(code));
|
|
181
|
+
}
|
|
182
|
+
return profile ? resolveProfile(profile) : void 0;
|
|
183
|
+
}
|
|
184
|
+
function getRegisteredLanguages() {
|
|
185
|
+
return Array.from(tokenizers.keys());
|
|
186
|
+
}
|
|
187
|
+
function isLanguageRegistered(code) {
|
|
188
|
+
if (tokenizers.has(code) && profiles.has(code)) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
if (isLanguageVariant(code)) {
|
|
192
|
+
const baseCode = getBaseLanguageCode(code);
|
|
193
|
+
return tokenizers.has(baseCode) && profiles.has(baseCode);
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
function isLanguageSupported(code) {
|
|
198
|
+
if (tokenizers.has(code)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (isLanguageVariant(code)) {
|
|
202
|
+
return tokenizers.has(getBaseLanguageCode(code));
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
function getPatternsForLanguage(code) {
|
|
207
|
+
let cached = patternCache.get(code);
|
|
208
|
+
if (!cached && isLanguageVariant(code)) {
|
|
209
|
+
cached = patternCache.get(getBaseLanguageCode(code));
|
|
210
|
+
}
|
|
211
|
+
if (cached) {
|
|
212
|
+
return cached;
|
|
213
|
+
}
|
|
214
|
+
let registered = registeredPatterns.get(code);
|
|
215
|
+
if (!registered && isLanguageVariant(code)) {
|
|
216
|
+
registered = registeredPatterns.get(getBaseLanguageCode(code));
|
|
217
|
+
}
|
|
218
|
+
if (registered) {
|
|
219
|
+
patternCache.set(code, registered);
|
|
220
|
+
return registered;
|
|
221
|
+
}
|
|
222
|
+
if (!patternGenerator) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`No patterns registered for language '${code}'. Either import the language module or set a pattern generator.`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const profile = getProfile(code);
|
|
228
|
+
const patterns = patternGenerator(profile);
|
|
229
|
+
patternCache.set(code, patterns);
|
|
230
|
+
return patterns;
|
|
231
|
+
}
|
|
232
|
+
function getPatternsForLanguageAndCommand(language, command) {
|
|
233
|
+
return getPatternsForLanguage(language).filter((p) => p.command === command).sort((a, b) => b.priority - a.priority);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/parser/pattern-matcher.ts
|
|
237
|
+
var _PatternMatcher = class _PatternMatcher {
|
|
238
|
+
constructor() {
|
|
239
|
+
/**
|
|
240
|
+
* Track stem matches for confidence calculation.
|
|
241
|
+
* This is set during matching and read during confidence calculation.
|
|
242
|
+
*/
|
|
243
|
+
this.stemMatchCount = 0;
|
|
244
|
+
this.totalKeywordMatches = 0;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Try to match a single pattern against the token stream.
|
|
248
|
+
* Returns the match result or null if no match.
|
|
249
|
+
*/
|
|
250
|
+
matchPattern(tokens, pattern) {
|
|
251
|
+
const mark = tokens.mark();
|
|
252
|
+
const captured = /* @__PURE__ */ new Map();
|
|
253
|
+
this.currentProfile = tryGetProfile(pattern.language);
|
|
254
|
+
this.stemMatchCount = 0;
|
|
255
|
+
this.totalKeywordMatches = 0;
|
|
256
|
+
const success = this.matchTokenSequence(tokens, pattern.template.tokens, captured);
|
|
257
|
+
if (!success) {
|
|
258
|
+
tokens.reset(mark);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const confidence = this.calculateConfidence(pattern, captured);
|
|
262
|
+
this.applyExtractionRules(pattern, captured);
|
|
263
|
+
return {
|
|
264
|
+
pattern,
|
|
265
|
+
captured,
|
|
266
|
+
consumedTokens: tokens.position() - mark.position,
|
|
267
|
+
confidence
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Try to match multiple patterns, return the best match.
|
|
272
|
+
*/
|
|
273
|
+
matchBest(tokens, patterns) {
|
|
274
|
+
const matches = [];
|
|
275
|
+
for (const pattern of patterns) {
|
|
276
|
+
const mark = tokens.mark();
|
|
277
|
+
const result = this.matchPattern(tokens, pattern);
|
|
278
|
+
if (result) {
|
|
279
|
+
matches.push(result);
|
|
280
|
+
}
|
|
281
|
+
tokens.reset(mark);
|
|
282
|
+
}
|
|
283
|
+
if (matches.length === 0) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
matches.sort((a, b) => {
|
|
287
|
+
const priorityDiff = b.pattern.priority - a.pattern.priority;
|
|
288
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
289
|
+
return b.confidence - a.confidence;
|
|
290
|
+
});
|
|
291
|
+
const best = matches[0];
|
|
292
|
+
this.matchPattern(tokens, best.pattern);
|
|
293
|
+
return best;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Match a sequence of pattern tokens against the token stream.
|
|
297
|
+
*/
|
|
298
|
+
matchTokenSequence(tokens, patternTokens, captured) {
|
|
299
|
+
const firstPatternToken = patternTokens[0];
|
|
300
|
+
const patternExpectsConjunction = firstPatternToken?.type === "literal" && (firstPatternToken.value === "and" || firstPatternToken.value === "then" || firstPatternToken.alternatives?.includes("and") || firstPatternToken.alternatives?.includes("then"));
|
|
301
|
+
if (this.currentProfile?.code === "ar" && !patternExpectsConjunction) {
|
|
302
|
+
while (tokens.peek()?.kind === "conjunction") {
|
|
303
|
+
tokens.advance();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (const patternToken of patternTokens) {
|
|
307
|
+
const matched = this.matchPatternToken(tokens, patternToken, captured);
|
|
308
|
+
if (!matched) {
|
|
309
|
+
if (this.isOptional(patternToken)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Match a single pattern token against the current position in the stream.
|
|
319
|
+
*/
|
|
320
|
+
matchPatternToken(tokens, patternToken, captured) {
|
|
321
|
+
switch (patternToken.type) {
|
|
322
|
+
case "literal":
|
|
323
|
+
return this.matchLiteralToken(tokens, patternToken);
|
|
324
|
+
case "role":
|
|
325
|
+
return this.matchRoleToken(tokens, patternToken, captured);
|
|
326
|
+
case "group":
|
|
327
|
+
return this.matchGroupToken(tokens, patternToken, captured);
|
|
328
|
+
default:
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Match a literal pattern token (keyword or particle).
|
|
334
|
+
*/
|
|
335
|
+
matchLiteralToken(tokens, patternToken) {
|
|
336
|
+
const token = tokens.peek();
|
|
337
|
+
if (!token) return false;
|
|
338
|
+
const matchType = this.getMatchType(token, patternToken.value);
|
|
339
|
+
if (matchType !== "none") {
|
|
340
|
+
this.totalKeywordMatches++;
|
|
341
|
+
if (matchType === "stem") {
|
|
342
|
+
this.stemMatchCount++;
|
|
343
|
+
}
|
|
344
|
+
tokens.advance();
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
if (patternToken.alternatives) {
|
|
348
|
+
for (const alt of patternToken.alternatives) {
|
|
349
|
+
const altMatchType = this.getMatchType(token, alt);
|
|
350
|
+
if (altMatchType !== "none") {
|
|
351
|
+
this.totalKeywordMatches++;
|
|
352
|
+
if (altMatchType === "stem") {
|
|
353
|
+
this.stemMatchCount++;
|
|
354
|
+
}
|
|
355
|
+
tokens.advance();
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Match a role pattern token (captures a semantic value).
|
|
364
|
+
* Handles multi-token expressions like:
|
|
365
|
+
* - 'my value' (possessive keyword + property)
|
|
366
|
+
* - '#dialog.showModal()' (method call)
|
|
367
|
+
* - "#element's *opacity" (possessive selector + property)
|
|
368
|
+
*/
|
|
369
|
+
matchRoleToken(tokens, patternToken, captured) {
|
|
370
|
+
this.skipNoiseWords(tokens);
|
|
371
|
+
const token = tokens.peek();
|
|
372
|
+
if (!token) {
|
|
373
|
+
return patternToken.optional || false;
|
|
374
|
+
}
|
|
375
|
+
const possessiveValue = this.tryMatchPossessiveExpression(tokens);
|
|
376
|
+
if (possessiveValue) {
|
|
377
|
+
if (patternToken.expectedTypes && patternToken.expectedTypes.length > 0) {
|
|
378
|
+
if (!patternToken.expectedTypes.includes(possessiveValue.type) && !patternToken.expectedTypes.includes("expression")) {
|
|
379
|
+
return patternToken.optional || false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
captured.set(patternToken.role, possessiveValue);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
const methodCallValue = this.tryMatchMethodCallExpression(tokens);
|
|
386
|
+
if (methodCallValue) {
|
|
387
|
+
if (patternToken.expectedTypes && patternToken.expectedTypes.length > 0) {
|
|
388
|
+
if (!patternToken.expectedTypes.includes(methodCallValue.type) && !patternToken.expectedTypes.includes("expression")) {
|
|
389
|
+
return patternToken.optional || false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
captured.set(patternToken.role, methodCallValue);
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
const possessiveSelectorValue = this.tryMatchPossessiveSelectorExpression(tokens);
|
|
396
|
+
if (possessiveSelectorValue) {
|
|
397
|
+
if (patternToken.expectedTypes && patternToken.expectedTypes.length > 0) {
|
|
398
|
+
if (!isTypeCompatible(possessiveSelectorValue.type, patternToken.expectedTypes)) {
|
|
399
|
+
return patternToken.optional || false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
captured.set(patternToken.role, possessiveSelectorValue);
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
const propertyAccessValue = this.tryMatchPropertyAccessExpression(tokens);
|
|
406
|
+
if (propertyAccessValue) {
|
|
407
|
+
if (patternToken.expectedTypes && patternToken.expectedTypes.length > 0) {
|
|
408
|
+
if (!patternToken.expectedTypes.includes(propertyAccessValue.type) && !patternToken.expectedTypes.includes("expression")) {
|
|
409
|
+
return patternToken.optional || false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
captured.set(patternToken.role, propertyAccessValue);
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
const selectorPropertyValue = this.tryMatchSelectorPropertyExpression(tokens);
|
|
416
|
+
if (selectorPropertyValue) {
|
|
417
|
+
if (patternToken.expectedTypes && patternToken.expectedTypes.length > 0) {
|
|
418
|
+
if (!isTypeCompatible(selectorPropertyValue.type, patternToken.expectedTypes)) {
|
|
419
|
+
return patternToken.optional || false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
captured.set(patternToken.role, selectorPropertyValue);
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
const value = this.tokenToSemanticValue(token);
|
|
426
|
+
if (!value) {
|
|
427
|
+
return patternToken.optional || false;
|
|
428
|
+
}
|
|
429
|
+
if (patternToken.expectedTypes && patternToken.expectedTypes.length > 0) {
|
|
430
|
+
if (!patternToken.expectedTypes.includes(value.type)) {
|
|
431
|
+
return patternToken.optional || false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
captured.set(patternToken.role, value);
|
|
435
|
+
tokens.advance();
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Try to match a possessive expression like 'my value' or 'its innerHTML'.
|
|
440
|
+
* Returns the PropertyPathValue if matched, or null if not.
|
|
441
|
+
*/
|
|
442
|
+
tryMatchPossessiveExpression(tokens) {
|
|
443
|
+
const token = tokens.peek();
|
|
444
|
+
if (!token) return null;
|
|
445
|
+
if (!this.currentProfile) return null;
|
|
446
|
+
const tokenLower = (token.normalized || token.value).toLowerCase();
|
|
447
|
+
const baseRef = getPossessiveReference(this.currentProfile, tokenLower);
|
|
448
|
+
if (!baseRef) return null;
|
|
449
|
+
const mark = tokens.mark();
|
|
450
|
+
tokens.advance();
|
|
451
|
+
const propertyToken = tokens.peek();
|
|
452
|
+
if (!propertyToken) {
|
|
453
|
+
tokens.reset(mark);
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
if (propertyToken.kind === "identifier" || propertyToken.kind === "keyword" && !this.isStructuralKeyword(propertyToken.value) || propertyToken.kind === "selector" && propertyToken.value.startsWith("*")) {
|
|
457
|
+
tokens.advance();
|
|
458
|
+
return createPropertyPath(createReference(baseRef), propertyToken.value);
|
|
459
|
+
}
|
|
460
|
+
tokens.reset(mark);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Check if a keyword is a structural keyword (preposition, control flow, etc.)
|
|
465
|
+
* that shouldn't be consumed as a property name.
|
|
466
|
+
*/
|
|
467
|
+
isStructuralKeyword(value) {
|
|
468
|
+
const structural = /* @__PURE__ */ new Set([
|
|
469
|
+
// Prepositions
|
|
470
|
+
"into",
|
|
471
|
+
"in",
|
|
472
|
+
"to",
|
|
473
|
+
"from",
|
|
474
|
+
"at",
|
|
475
|
+
"by",
|
|
476
|
+
"with",
|
|
477
|
+
"without",
|
|
478
|
+
"before",
|
|
479
|
+
"after",
|
|
480
|
+
"of",
|
|
481
|
+
"as",
|
|
482
|
+
"on",
|
|
483
|
+
// Control flow
|
|
484
|
+
"then",
|
|
485
|
+
"end",
|
|
486
|
+
"else",
|
|
487
|
+
"if",
|
|
488
|
+
"repeat",
|
|
489
|
+
"while",
|
|
490
|
+
"for",
|
|
491
|
+
// Commands (shouldn't be property names)
|
|
492
|
+
"toggle",
|
|
493
|
+
"add",
|
|
494
|
+
"remove",
|
|
495
|
+
"put",
|
|
496
|
+
"set",
|
|
497
|
+
"show",
|
|
498
|
+
"hide",
|
|
499
|
+
"increment",
|
|
500
|
+
"decrement",
|
|
501
|
+
"send",
|
|
502
|
+
"trigger",
|
|
503
|
+
"call"
|
|
504
|
+
]);
|
|
505
|
+
return structural.has(value.toLowerCase());
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Try to match a method call expression like '#dialog.showModal()'.
|
|
509
|
+
* Pattern: selector + '.' + identifier + '(' + [args] + ')'
|
|
510
|
+
* Returns an expression value if matched, or null if not.
|
|
511
|
+
*/
|
|
512
|
+
tryMatchMethodCallExpression(tokens) {
|
|
513
|
+
const token = tokens.peek();
|
|
514
|
+
if (!token || token.kind !== "selector") return null;
|
|
515
|
+
const mark = tokens.mark();
|
|
516
|
+
tokens.advance();
|
|
517
|
+
const dotToken = tokens.peek();
|
|
518
|
+
if (!dotToken || dotToken.kind !== "operator" || dotToken.value !== ".") {
|
|
519
|
+
tokens.reset(mark);
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
tokens.advance();
|
|
523
|
+
const methodToken = tokens.peek();
|
|
524
|
+
if (!methodToken || methodToken.kind !== "identifier") {
|
|
525
|
+
tokens.reset(mark);
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
tokens.advance();
|
|
529
|
+
const openParen = tokens.peek();
|
|
530
|
+
if (!openParen || openParen.kind !== "punctuation" || openParen.value !== "(") {
|
|
531
|
+
tokens.reset(mark);
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
tokens.advance();
|
|
535
|
+
const args = [];
|
|
536
|
+
while (!tokens.isAtEnd() && args.length < _PatternMatcher.MAX_METHOD_ARGS) {
|
|
537
|
+
const argToken = tokens.peek();
|
|
538
|
+
if (!argToken) break;
|
|
539
|
+
if (argToken.kind === "punctuation" && argToken.value === ")") {
|
|
540
|
+
tokens.advance();
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
if (argToken.kind === "punctuation" && argToken.value === ",") {
|
|
544
|
+
tokens.advance();
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
args.push(argToken.value);
|
|
548
|
+
tokens.advance();
|
|
549
|
+
}
|
|
550
|
+
const methodCall = `${token.value}.${methodToken.value}(${args.join(", ")})`;
|
|
551
|
+
return {
|
|
552
|
+
type: "expression",
|
|
553
|
+
raw: methodCall
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Try to match a property access expression like 'userData.name' or 'it.data'.
|
|
558
|
+
* Pattern: (identifier | keyword) + '.' + identifier [+ '.' + identifier ...]
|
|
559
|
+
* Returns an expression value if matched, or null if not.
|
|
560
|
+
*/
|
|
561
|
+
tryMatchPropertyAccessExpression(tokens) {
|
|
562
|
+
const token = tokens.peek();
|
|
563
|
+
if (!token) return null;
|
|
564
|
+
if (token.kind !== "identifier" && token.kind !== "keyword") return null;
|
|
565
|
+
const mark = tokens.mark();
|
|
566
|
+
tokens.advance();
|
|
567
|
+
const dotToken = tokens.peek();
|
|
568
|
+
if (!dotToken || dotToken.kind !== "operator" || dotToken.value !== ".") {
|
|
569
|
+
tokens.reset(mark);
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
tokens.advance();
|
|
573
|
+
const propertyToken = tokens.peek();
|
|
574
|
+
if (!propertyToken || propertyToken.kind !== "identifier") {
|
|
575
|
+
tokens.reset(mark);
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
tokens.advance();
|
|
579
|
+
let chain = `${token.value}.${propertyToken.value}`;
|
|
580
|
+
let depth = 1;
|
|
581
|
+
while (!tokens.isAtEnd() && depth < _PatternMatcher.MAX_PROPERTY_DEPTH) {
|
|
582
|
+
const nextDot = tokens.peek();
|
|
583
|
+
if (!nextDot || nextDot.kind !== "operator" || nextDot.value !== ".") {
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
tokens.advance();
|
|
587
|
+
const nextProp = tokens.peek();
|
|
588
|
+
if (!nextProp || nextProp.kind !== "identifier") {
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
tokens.advance();
|
|
592
|
+
chain += `.${nextProp.value}`;
|
|
593
|
+
depth++;
|
|
594
|
+
}
|
|
595
|
+
const openParen = tokens.peek();
|
|
596
|
+
if (openParen && openParen.kind === "punctuation" && openParen.value === "(") {
|
|
597
|
+
tokens.advance();
|
|
598
|
+
const args = [];
|
|
599
|
+
let argDepth = 0;
|
|
600
|
+
while (!tokens.isAtEnd() && args.length < _PatternMatcher.MAX_METHOD_ARGS) {
|
|
601
|
+
const argToken = tokens.peek();
|
|
602
|
+
if (!argToken) break;
|
|
603
|
+
if (argToken.kind === "punctuation" && argToken.value === ")") {
|
|
604
|
+
if (argDepth === 0) {
|
|
605
|
+
tokens.advance();
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
argDepth--;
|
|
609
|
+
}
|
|
610
|
+
if (argToken.kind === "punctuation" && argToken.value === "(") {
|
|
611
|
+
argDepth++;
|
|
612
|
+
}
|
|
613
|
+
if (argToken.kind === "punctuation" && argToken.value === ",") {
|
|
614
|
+
tokens.advance();
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
args.push(argToken.value);
|
|
618
|
+
tokens.advance();
|
|
619
|
+
}
|
|
620
|
+
const methodCall = `${chain}(${args.join(", ")})`;
|
|
621
|
+
return {
|
|
622
|
+
type: "expression",
|
|
623
|
+
raw: methodCall
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
type: "expression",
|
|
628
|
+
raw: chain
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Try to match a possessive selector expression like "#element's *opacity".
|
|
633
|
+
* Pattern: selector + "'s" + (selector | identifier)
|
|
634
|
+
* Returns a property-path value if matched, or null if not.
|
|
635
|
+
*/
|
|
636
|
+
tryMatchPossessiveSelectorExpression(tokens) {
|
|
637
|
+
const token = tokens.peek();
|
|
638
|
+
if (!token || token.kind !== "selector") return null;
|
|
639
|
+
const mark = tokens.mark();
|
|
640
|
+
tokens.advance();
|
|
641
|
+
const possessiveToken = tokens.peek();
|
|
642
|
+
if (!possessiveToken || possessiveToken.kind !== "punctuation" || possessiveToken.value !== "'s") {
|
|
643
|
+
tokens.reset(mark);
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
tokens.advance();
|
|
647
|
+
const propertyToken = tokens.peek();
|
|
648
|
+
if (!propertyToken) {
|
|
649
|
+
tokens.reset(mark);
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
if (propertyToken.kind !== "selector" && propertyToken.kind !== "identifier") {
|
|
653
|
+
tokens.reset(mark);
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
tokens.advance();
|
|
657
|
+
return createPropertyPath(createSelector(token.value), propertyToken.value);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Try to match a selector + property expression like "#output.innerText".
|
|
661
|
+
* This handles cases where the tokenizer produces two selector tokens:
|
|
662
|
+
* - #output (id selector)
|
|
663
|
+
* - .innerText (looks like class selector, but is actually property)
|
|
664
|
+
*
|
|
665
|
+
* Pattern: id-selector + class-selector-that-is-actually-property
|
|
666
|
+
* Returns a property-path value if matched, or null if not.
|
|
667
|
+
*/
|
|
668
|
+
tryMatchSelectorPropertyExpression(tokens) {
|
|
669
|
+
const token = tokens.peek();
|
|
670
|
+
if (!token || token.kind !== "selector") return null;
|
|
671
|
+
if (!token.value.startsWith("#")) return null;
|
|
672
|
+
const mark = tokens.mark();
|
|
673
|
+
tokens.advance();
|
|
674
|
+
const propertyToken = tokens.peek();
|
|
675
|
+
if (!propertyToken || propertyToken.kind !== "selector") {
|
|
676
|
+
tokens.reset(mark);
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
if (!propertyToken.value.startsWith(".")) {
|
|
680
|
+
tokens.reset(mark);
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
const peek2 = tokens.peek(1);
|
|
684
|
+
if (peek2 && peek2.kind === "selector") {
|
|
685
|
+
}
|
|
686
|
+
tokens.advance();
|
|
687
|
+
const propertyName = propertyToken.value.slice(1);
|
|
688
|
+
return createPropertyPath(createSelector(token.value), propertyName);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Match a group pattern token (optional sequence).
|
|
692
|
+
*/
|
|
693
|
+
matchGroupToken(tokens, patternToken, captured) {
|
|
694
|
+
const mark = tokens.mark();
|
|
695
|
+
const capturedBefore = new Set(captured.keys());
|
|
696
|
+
const success = this.matchTokenSequence(tokens, patternToken.tokens, captured);
|
|
697
|
+
if (!success) {
|
|
698
|
+
tokens.reset(mark);
|
|
699
|
+
for (const role of captured.keys()) {
|
|
700
|
+
if (!capturedBefore.has(role)) {
|
|
701
|
+
captured.delete(role);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return patternToken.optional || false;
|
|
705
|
+
}
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Get the type of match for a token against a value.
|
|
710
|
+
* Used for confidence calculation.
|
|
711
|
+
*/
|
|
712
|
+
getMatchType(token, value) {
|
|
713
|
+
if (token.value === value) return "exact";
|
|
714
|
+
if (token.normalized === value) return "normalized";
|
|
715
|
+
if (token.stem === value && token.stemConfidence !== void 0 && token.stemConfidence >= 0.7) {
|
|
716
|
+
return "stem";
|
|
717
|
+
}
|
|
718
|
+
if (token.kind === "keyword" && token.value.toLowerCase() === value.toLowerCase()) {
|
|
719
|
+
return "case-insensitive";
|
|
720
|
+
}
|
|
721
|
+
return "none";
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Convert a language token to a semantic value.
|
|
725
|
+
*/
|
|
726
|
+
tokenToSemanticValue(token) {
|
|
727
|
+
switch (token.kind) {
|
|
728
|
+
case "selector":
|
|
729
|
+
return createSelector(token.value);
|
|
730
|
+
case "literal":
|
|
731
|
+
return this.parseLiteralValue(token.value);
|
|
732
|
+
case "keyword":
|
|
733
|
+
const lower = (token.normalized || token.value).toLowerCase();
|
|
734
|
+
if (["me", "you", "it", "result", "event", "target", "body"].includes(lower)) {
|
|
735
|
+
return createReference(lower);
|
|
736
|
+
}
|
|
737
|
+
return createLiteral(token.normalized || token.value);
|
|
738
|
+
case "identifier":
|
|
739
|
+
if (token.value.startsWith(":")) {
|
|
740
|
+
return createReference(token.value);
|
|
741
|
+
}
|
|
742
|
+
const identLower = token.value.toLowerCase();
|
|
743
|
+
if (["me", "you", "it", "result", "event", "target", "body"].includes(identLower)) {
|
|
744
|
+
return createReference(identLower);
|
|
745
|
+
}
|
|
746
|
+
return { type: "expression", raw: token.value };
|
|
747
|
+
case "url":
|
|
748
|
+
return createLiteral(token.value, "string");
|
|
749
|
+
default:
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Parse a literal value (string, number, boolean).
|
|
755
|
+
*/
|
|
756
|
+
parseLiteralValue(value) {
|
|
757
|
+
if (value.startsWith('"') || value.startsWith("'") || value.startsWith("`") || value.startsWith("\u300C")) {
|
|
758
|
+
const inner = value.slice(1, -1);
|
|
759
|
+
return createLiteral(inner, "string");
|
|
760
|
+
}
|
|
761
|
+
if (value === "true") return createLiteral(true, "boolean");
|
|
762
|
+
if (value === "false") return createLiteral(false, "boolean");
|
|
763
|
+
const durationMatch = value.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
764
|
+
if (durationMatch) {
|
|
765
|
+
const num2 = parseFloat(durationMatch[1]);
|
|
766
|
+
const unit = durationMatch[2];
|
|
767
|
+
if (unit) {
|
|
768
|
+
return createLiteral(value, "duration");
|
|
769
|
+
}
|
|
770
|
+
return createLiteral(num2, "number");
|
|
771
|
+
}
|
|
772
|
+
const num = parseFloat(value);
|
|
773
|
+
if (!isNaN(num)) {
|
|
774
|
+
return createLiteral(num, "number");
|
|
775
|
+
}
|
|
776
|
+
return createLiteral(value, "string");
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Apply extraction rules to fill in default values for missing roles.
|
|
780
|
+
*/
|
|
781
|
+
applyExtractionRules(pattern, captured) {
|
|
782
|
+
for (const [role, rule] of Object.entries(pattern.extraction)) {
|
|
783
|
+
if (!captured.has(role) && rule.default) {
|
|
784
|
+
captured.set(role, rule.default);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Check if a pattern token is optional.
|
|
790
|
+
*/
|
|
791
|
+
isOptional(patternToken) {
|
|
792
|
+
return patternToken.optional === true;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Calculate confidence score for a match (0-1).
|
|
796
|
+
*
|
|
797
|
+
* Confidence is reduced for:
|
|
798
|
+
* - Stem matches (morphological normalization has inherent uncertainty)
|
|
799
|
+
* - Missing optional roles (but less penalty if role has a default value)
|
|
800
|
+
*
|
|
801
|
+
* Confidence is increased for:
|
|
802
|
+
* - VSO languages (Arabic) when pattern starts with a verb
|
|
803
|
+
*/
|
|
804
|
+
calculateConfidence(pattern, captured) {
|
|
805
|
+
let score = 0;
|
|
806
|
+
let maxScore = 0;
|
|
807
|
+
const hasDefault = (role) => {
|
|
808
|
+
return pattern.extraction?.[role]?.default !== void 0;
|
|
809
|
+
};
|
|
810
|
+
for (const token of pattern.template.tokens) {
|
|
811
|
+
if (token.type === "role") {
|
|
812
|
+
maxScore += 1;
|
|
813
|
+
if (captured.has(token.role)) {
|
|
814
|
+
score += 1;
|
|
815
|
+
}
|
|
816
|
+
} else if (token.type === "group") {
|
|
817
|
+
for (const subToken of token.tokens) {
|
|
818
|
+
if (subToken.type === "role") {
|
|
819
|
+
const roleHasDefault = hasDefault(subToken.role);
|
|
820
|
+
const weight = 0.8;
|
|
821
|
+
maxScore += weight;
|
|
822
|
+
if (captured.has(subToken.role)) {
|
|
823
|
+
score += weight;
|
|
824
|
+
} else if (roleHasDefault) {
|
|
825
|
+
score += weight * 0.6;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
let baseConfidence = maxScore > 0 ? score / maxScore : 1;
|
|
832
|
+
if (this.stemMatchCount > 0 && this.totalKeywordMatches > 0) {
|
|
833
|
+
const stemPenalty = this.stemMatchCount / this.totalKeywordMatches * 0.15;
|
|
834
|
+
baseConfidence = Math.max(0.5, baseConfidence - stemPenalty);
|
|
835
|
+
}
|
|
836
|
+
const vsoBoost = this.calculateVSOConfidenceBoost(pattern);
|
|
837
|
+
baseConfidence = Math.min(1, baseConfidence + vsoBoost);
|
|
838
|
+
const prepositionAdjustment = this.arabicPrepositionDisambiguation(pattern, captured);
|
|
839
|
+
baseConfidence = Math.max(0, Math.min(1, baseConfidence + prepositionAdjustment));
|
|
840
|
+
return baseConfidence;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Calculate confidence boost for VSO (Verb-Subject-Object) language patterns.
|
|
844
|
+
* Arabic naturally uses VSO word order, so patterns that start with a verb
|
|
845
|
+
* should receive a confidence boost.
|
|
846
|
+
*
|
|
847
|
+
* Returns +0.15 confidence boost if:
|
|
848
|
+
* - Language is Arabic ('ar')
|
|
849
|
+
* - Pattern's first token is a verb keyword
|
|
850
|
+
*
|
|
851
|
+
* @param pattern The language pattern being matched
|
|
852
|
+
* @returns Confidence boost (0 or 0.15)
|
|
853
|
+
*/
|
|
854
|
+
calculateVSOConfidenceBoost(pattern) {
|
|
855
|
+
if (pattern.language !== "ar") {
|
|
856
|
+
return 0;
|
|
857
|
+
}
|
|
858
|
+
const firstToken = pattern.template.tokens[0];
|
|
859
|
+
if (!firstToken || firstToken.type !== "literal") {
|
|
860
|
+
return 0;
|
|
861
|
+
}
|
|
862
|
+
const ARABIC_VERBS = /* @__PURE__ */ new Set([
|
|
863
|
+
"\u0628\u062F\u0644",
|
|
864
|
+
"\u063A\u064A\u0631",
|
|
865
|
+
"\u0623\u0636\u0641",
|
|
866
|
+
"\u0623\u0632\u0644",
|
|
867
|
+
"\u0636\u0639",
|
|
868
|
+
"\u0627\u062C\u0639\u0644",
|
|
869
|
+
"\u0639\u064A\u0646",
|
|
870
|
+
"\u0632\u062F",
|
|
871
|
+
"\u0627\u0646\u0642\u0635",
|
|
872
|
+
"\u0633\u062C\u0644",
|
|
873
|
+
"\u0623\u0638\u0647\u0631",
|
|
874
|
+
"\u0623\u062E\u0641",
|
|
875
|
+
"\u0634\u063A\u0644",
|
|
876
|
+
"\u0623\u0631\u0633\u0644",
|
|
877
|
+
"\u0631\u0643\u0632",
|
|
878
|
+
"\u0634\u0648\u0634",
|
|
879
|
+
"\u062A\u0648\u0642\u0641",
|
|
880
|
+
"\u0627\u0646\u0633\u062E",
|
|
881
|
+
"\u0627\u062D\u0630\u0641",
|
|
882
|
+
"\u0627\u0635\u0646\u0639",
|
|
883
|
+
"\u0627\u0646\u062A\u0638\u0631",
|
|
884
|
+
"\u0627\u0646\u062A\u0642\u0627\u0644",
|
|
885
|
+
"\u0623\u0648"
|
|
886
|
+
]);
|
|
887
|
+
if (ARABIC_VERBS.has(firstToken.value)) {
|
|
888
|
+
return 0.15;
|
|
889
|
+
}
|
|
890
|
+
if (firstToken.alternatives) {
|
|
891
|
+
for (const alt of firstToken.alternatives) {
|
|
892
|
+
if (ARABIC_VERBS.has(alt)) {
|
|
893
|
+
return 0.15;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return 0;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Arabic preposition disambiguation for confidence adjustment.
|
|
901
|
+
*
|
|
902
|
+
* Different Arabic prepositions are more or less natural for different semantic roles:
|
|
903
|
+
* - على (on/upon) is preferred for patient/target roles (element selectors)
|
|
904
|
+
* - إلى (to) is preferred for destination roles
|
|
905
|
+
* - من (from) is preferred for source roles
|
|
906
|
+
* - في (in) is preferred for location roles
|
|
907
|
+
*
|
|
908
|
+
* This method analyzes the prepositions used with captured semantic roles and
|
|
909
|
+
* adjusts confidence based on idiomaticity:
|
|
910
|
+
* - +0.10 for highly idiomatic preposition choices
|
|
911
|
+
* - -0.10 for less natural preposition choices
|
|
912
|
+
*
|
|
913
|
+
* @param pattern The language pattern being matched
|
|
914
|
+
* @param captured The captured semantic values
|
|
915
|
+
* @returns Confidence adjustment (-0.10 to +0.10)
|
|
916
|
+
*/
|
|
917
|
+
arabicPrepositionDisambiguation(pattern, captured) {
|
|
918
|
+
if (pattern.language !== "ar") {
|
|
919
|
+
return 0;
|
|
920
|
+
}
|
|
921
|
+
let adjustment = 0;
|
|
922
|
+
const PREFERRED_PREPOSITIONS = {
|
|
923
|
+
patient: ["\u0639\u0644\u0649"],
|
|
924
|
+
// element selectors prefer على (on/upon)
|
|
925
|
+
destination: ["\u0625\u0644\u0649", "\u0627\u0644\u0649"],
|
|
926
|
+
// destination prefers إلى (to)
|
|
927
|
+
source: ["\u0645\u0646"],
|
|
928
|
+
// source prefers من (from)
|
|
929
|
+
agent: ["\u0645\u0646"],
|
|
930
|
+
// agent/by prefers من (from/by)
|
|
931
|
+
manner: ["\u0628"],
|
|
932
|
+
// manner prefers ب (with/by)
|
|
933
|
+
style: ["\u0628"],
|
|
934
|
+
// style prefers ب (with)
|
|
935
|
+
goal: ["\u0625\u0644\u0649", "\u0627\u0644\u0649"],
|
|
936
|
+
// target state prefers إلى (to)
|
|
937
|
+
method: ["\u0628"]
|
|
938
|
+
// method prefers ب (with/by)
|
|
939
|
+
};
|
|
940
|
+
for (const [role, value] of captured.entries()) {
|
|
941
|
+
const preferred = PREFERRED_PREPOSITIONS[role];
|
|
942
|
+
if (!preferred || preferred.length === 0) {
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
const metadata = value.metadata;
|
|
946
|
+
if (metadata && typeof metadata.prepositionValue === "string") {
|
|
947
|
+
const usedPreposition = metadata.prepositionValue;
|
|
948
|
+
if (preferred.includes(usedPreposition)) {
|
|
949
|
+
adjustment += 0.1;
|
|
950
|
+
} else {
|
|
951
|
+
adjustment -= 0.1;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return Math.max(-0.1, Math.min(0.1, adjustment));
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Skip noise words like "the" before selectors.
|
|
959
|
+
* This enables more natural English syntax like "toggle the .active".
|
|
960
|
+
*/
|
|
961
|
+
skipNoiseWords(tokens) {
|
|
962
|
+
const token = tokens.peek();
|
|
963
|
+
if (!token) return;
|
|
964
|
+
const tokenLower = token.value.toLowerCase();
|
|
965
|
+
if (_PatternMatcher.ENGLISH_NOISE_WORDS.has(tokenLower)) {
|
|
966
|
+
const mark = tokens.mark();
|
|
967
|
+
tokens.advance();
|
|
968
|
+
const nextToken = tokens.peek();
|
|
969
|
+
if (nextToken && nextToken.kind === "selector") {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
tokens.reset(mark);
|
|
973
|
+
}
|
|
974
|
+
if (tokenLower === "class") {
|
|
975
|
+
tokens.advance();
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Extract event modifiers from the token stream.
|
|
980
|
+
* Event modifiers are .once, .debounce(N), .throttle(N), .queue(strategy)
|
|
981
|
+
* that can appear after event names.
|
|
982
|
+
*
|
|
983
|
+
* Returns EventModifiers object or undefined if no modifiers found.
|
|
984
|
+
*/
|
|
985
|
+
extractEventModifiers(tokens) {
|
|
986
|
+
const modifiers = {};
|
|
987
|
+
let foundModifier = false;
|
|
988
|
+
while (!tokens.isAtEnd()) {
|
|
989
|
+
const token = tokens.peek();
|
|
990
|
+
if (!token || token.kind !== "event-modifier") {
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
const metadata = token.metadata;
|
|
994
|
+
if (!metadata) {
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
foundModifier = true;
|
|
998
|
+
switch (metadata.modifierName) {
|
|
999
|
+
case "once":
|
|
1000
|
+
modifiers.once = true;
|
|
1001
|
+
break;
|
|
1002
|
+
case "debounce":
|
|
1003
|
+
if (typeof metadata.value === "number") {
|
|
1004
|
+
modifiers.debounce = metadata.value;
|
|
1005
|
+
}
|
|
1006
|
+
break;
|
|
1007
|
+
case "throttle":
|
|
1008
|
+
if (typeof metadata.value === "number") {
|
|
1009
|
+
modifiers.throttle = metadata.value;
|
|
1010
|
+
}
|
|
1011
|
+
break;
|
|
1012
|
+
case "queue":
|
|
1013
|
+
if (metadata.value === "first" || metadata.value === "last" || metadata.value === "all" || metadata.value === "none") {
|
|
1014
|
+
modifiers.queue = metadata.value;
|
|
1015
|
+
}
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
tokens.advance();
|
|
1019
|
+
}
|
|
1020
|
+
return foundModifier ? modifiers : void 0;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
// ==========================================================================
|
|
1024
|
+
// Depth Limits for Expression Parsing (security hardening)
|
|
1025
|
+
// ==========================================================================
|
|
1026
|
+
/** Maximum depth for nested property access (e.g., a.b.c.d...) */
|
|
1027
|
+
_PatternMatcher.MAX_PROPERTY_DEPTH = 10;
|
|
1028
|
+
/** Maximum number of arguments in method calls */
|
|
1029
|
+
_PatternMatcher.MAX_METHOD_ARGS = 20;
|
|
1030
|
+
// ===========================================================================
|
|
1031
|
+
// English Idiom Support - Noise Word Handling
|
|
1032
|
+
// ===========================================================================
|
|
1033
|
+
/**
|
|
1034
|
+
* Noise words that can be skipped in English for more natural syntax.
|
|
1035
|
+
* - "the" before selectors: "toggle the .active" → "toggle .active"
|
|
1036
|
+
* - "class" after class selectors: "add the .visible class" → "add .visible"
|
|
1037
|
+
*/
|
|
1038
|
+
_PatternMatcher.ENGLISH_NOISE_WORDS = /* @__PURE__ */ new Set(["the", "a", "an"]);
|
|
1039
|
+
var PatternMatcher = _PatternMatcher;
|
|
1040
|
+
var patternMatcher = new PatternMatcher();
|
|
1041
|
+
|
|
1042
|
+
// src/tokenizers/index.ts
|
|
1043
|
+
function getTokenizer2(language) {
|
|
1044
|
+
return tryGetTokenizer(language);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/cache/semantic-cache.ts
|
|
1048
|
+
var SemanticCache = class {
|
|
1049
|
+
constructor(config = {}) {
|
|
1050
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
1051
|
+
this.config = {
|
|
1052
|
+
maxSize: config.maxSize ?? 1e3,
|
|
1053
|
+
ttlMs: config.ttlMs ?? 0,
|
|
1054
|
+
enabled: config.enabled ?? true
|
|
1055
|
+
};
|
|
1056
|
+
this.stats = {
|
|
1057
|
+
hits: 0,
|
|
1058
|
+
misses: 0,
|
|
1059
|
+
evictions: 0,
|
|
1060
|
+
expirations: 0
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Generate cache key from input and language.
|
|
1065
|
+
*/
|
|
1066
|
+
makeKey(input, language) {
|
|
1067
|
+
return `${language}:${input}`;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Check if an entry has expired.
|
|
1071
|
+
*/
|
|
1072
|
+
isExpired(entry) {
|
|
1073
|
+
if (this.config.ttlMs === 0) return false;
|
|
1074
|
+
return Date.now() - entry.createdAt > this.config.ttlMs;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Evict the least recently used entry.
|
|
1078
|
+
*/
|
|
1079
|
+
evictLRU() {
|
|
1080
|
+
const firstKey = this.cache.keys().next().value;
|
|
1081
|
+
if (firstKey !== void 0) {
|
|
1082
|
+
this.cache.delete(firstKey);
|
|
1083
|
+
this.stats.evictions++;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Get a cached result.
|
|
1088
|
+
*
|
|
1089
|
+
* @param input - The input string
|
|
1090
|
+
* @param language - The language code
|
|
1091
|
+
* @returns The cached result, or undefined if not found/expired
|
|
1092
|
+
*/
|
|
1093
|
+
get(input, language) {
|
|
1094
|
+
if (!this.config.enabled) {
|
|
1095
|
+
this.stats.misses++;
|
|
1096
|
+
return void 0;
|
|
1097
|
+
}
|
|
1098
|
+
const key = this.makeKey(input, language);
|
|
1099
|
+
const entry = this.cache.get(key);
|
|
1100
|
+
if (!entry) {
|
|
1101
|
+
this.stats.misses++;
|
|
1102
|
+
return void 0;
|
|
1103
|
+
}
|
|
1104
|
+
if (this.isExpired(entry)) {
|
|
1105
|
+
this.cache.delete(key);
|
|
1106
|
+
this.stats.expirations++;
|
|
1107
|
+
this.stats.misses++;
|
|
1108
|
+
return void 0;
|
|
1109
|
+
}
|
|
1110
|
+
this.cache.delete(key);
|
|
1111
|
+
entry.lastAccessed = Date.now();
|
|
1112
|
+
this.cache.set(key, entry);
|
|
1113
|
+
this.stats.hits++;
|
|
1114
|
+
return entry.result;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Store a result in the cache.
|
|
1118
|
+
*
|
|
1119
|
+
* @param input - The input string
|
|
1120
|
+
* @param language - The language code
|
|
1121
|
+
* @param result - The analysis result to cache
|
|
1122
|
+
*/
|
|
1123
|
+
set(input, language, result) {
|
|
1124
|
+
if (!this.config.enabled) return;
|
|
1125
|
+
if (result.confidence === 0) return;
|
|
1126
|
+
const key = this.makeKey(input, language);
|
|
1127
|
+
const now = Date.now();
|
|
1128
|
+
while (this.cache.size >= this.config.maxSize) {
|
|
1129
|
+
this.evictLRU();
|
|
1130
|
+
}
|
|
1131
|
+
this.cache.set(key, {
|
|
1132
|
+
result,
|
|
1133
|
+
createdAt: now,
|
|
1134
|
+
lastAccessed: now
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Check if a result is cached (without updating LRU).
|
|
1139
|
+
*/
|
|
1140
|
+
has(input, language) {
|
|
1141
|
+
if (!this.config.enabled) return false;
|
|
1142
|
+
const key = this.makeKey(input, language);
|
|
1143
|
+
const entry = this.cache.get(key);
|
|
1144
|
+
if (!entry) return false;
|
|
1145
|
+
if (this.isExpired(entry)) {
|
|
1146
|
+
this.cache.delete(key);
|
|
1147
|
+
this.stats.expirations++;
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Remove a specific entry from the cache.
|
|
1154
|
+
*/
|
|
1155
|
+
delete(input, language) {
|
|
1156
|
+
const key = this.makeKey(input, language);
|
|
1157
|
+
return this.cache.delete(key);
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Clear all cached entries.
|
|
1161
|
+
*/
|
|
1162
|
+
clear() {
|
|
1163
|
+
this.cache.clear();
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Reset statistics.
|
|
1167
|
+
*/
|
|
1168
|
+
resetStats() {
|
|
1169
|
+
this.stats = {
|
|
1170
|
+
hits: 0,
|
|
1171
|
+
misses: 0,
|
|
1172
|
+
evictions: 0,
|
|
1173
|
+
expirations: 0
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Get cache statistics.
|
|
1178
|
+
*/
|
|
1179
|
+
getStats() {
|
|
1180
|
+
const total = this.stats.hits + this.stats.misses;
|
|
1181
|
+
return {
|
|
1182
|
+
hits: this.stats.hits,
|
|
1183
|
+
misses: this.stats.misses,
|
|
1184
|
+
size: this.cache.size,
|
|
1185
|
+
maxSize: this.config.maxSize,
|
|
1186
|
+
hitRate: total > 0 ? this.stats.hits / total : 0,
|
|
1187
|
+
evictions: this.stats.evictions,
|
|
1188
|
+
expirations: this.stats.expirations,
|
|
1189
|
+
enabled: this.config.enabled
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Update cache configuration.
|
|
1194
|
+
*/
|
|
1195
|
+
configure(config) {
|
|
1196
|
+
if (config.maxSize !== void 0) {
|
|
1197
|
+
this.config.maxSize = config.maxSize;
|
|
1198
|
+
while (this.cache.size > this.config.maxSize) {
|
|
1199
|
+
this.evictLRU();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (config.ttlMs !== void 0) {
|
|
1203
|
+
this.config.ttlMs = config.ttlMs;
|
|
1204
|
+
}
|
|
1205
|
+
if (config.enabled !== void 0) {
|
|
1206
|
+
this.config.enabled = config.enabled;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Enable caching.
|
|
1211
|
+
*/
|
|
1212
|
+
enable() {
|
|
1213
|
+
this.config.enabled = true;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Disable caching.
|
|
1217
|
+
*/
|
|
1218
|
+
disable() {
|
|
1219
|
+
this.config.enabled = false;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Get current configuration.
|
|
1223
|
+
*/
|
|
1224
|
+
getConfig() {
|
|
1225
|
+
return { ...this.config };
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
var semanticCache = new SemanticCache();
|
|
1229
|
+
function createSemanticCache(config) {
|
|
1230
|
+
return new SemanticCache(config);
|
|
1231
|
+
}
|
|
1232
|
+
function withCache(analyzeFn, cache = semanticCache) {
|
|
1233
|
+
return ((input, language) => {
|
|
1234
|
+
const cached = cache.get(input, language);
|
|
1235
|
+
if (cached) {
|
|
1236
|
+
return cached;
|
|
1237
|
+
}
|
|
1238
|
+
const result = analyzeFn(input, language);
|
|
1239
|
+
cache.set(input, language, result);
|
|
1240
|
+
return result;
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/core-bridge.ts
|
|
1245
|
+
var SemanticAnalyzerImpl = class {
|
|
1246
|
+
constructor(options = {}) {
|
|
1247
|
+
this.patternMatcher = new PatternMatcher();
|
|
1248
|
+
this.languages = new Set(getRegisteredLanguages());
|
|
1249
|
+
if (options.cache === false) {
|
|
1250
|
+
this.cache = new SemanticCache({ enabled: false });
|
|
1251
|
+
} else {
|
|
1252
|
+
this.cache = options.cache ? new SemanticCache(options.cache) : semanticCache;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
analyze(input, language) {
|
|
1256
|
+
if (!this.supportsLanguage(language)) {
|
|
1257
|
+
return {
|
|
1258
|
+
confidence: 0,
|
|
1259
|
+
errors: [`Language '${language}' is not supported for semantic parsing`]
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
const cached = this.cache.get(input, language);
|
|
1263
|
+
if (cached) {
|
|
1264
|
+
return cached;
|
|
1265
|
+
}
|
|
1266
|
+
const result = this.analyzeUncached(input, language);
|
|
1267
|
+
this.cache.set(input, language, result);
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Perform analysis without cache lookup.
|
|
1272
|
+
*/
|
|
1273
|
+
analyzeUncached(input, language) {
|
|
1274
|
+
try {
|
|
1275
|
+
const tokenizer = getTokenizer2(language);
|
|
1276
|
+
if (!tokenizer) {
|
|
1277
|
+
return {
|
|
1278
|
+
confidence: 0,
|
|
1279
|
+
errors: [`No tokenizer available for language '${language}'`]
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
const tokenStream = tokenizer.tokenize(input);
|
|
1283
|
+
const patterns = getPatternsForLanguage(language);
|
|
1284
|
+
if (patterns.length === 0) {
|
|
1285
|
+
return {
|
|
1286
|
+
confidence: 0,
|
|
1287
|
+
errors: [`No patterns available for language '${language}'`]
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
const match = this.patternMatcher.matchBest(tokenStream, patterns);
|
|
1291
|
+
if (!match) {
|
|
1292
|
+
return {
|
|
1293
|
+
confidence: 0,
|
|
1294
|
+
errors: ["No pattern matched the input"]
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
const node = this.buildSemanticNode(match);
|
|
1298
|
+
return {
|
|
1299
|
+
confidence: match.confidence,
|
|
1300
|
+
command: {
|
|
1301
|
+
name: match.pattern.command,
|
|
1302
|
+
roles: match.captured
|
|
1303
|
+
},
|
|
1304
|
+
node,
|
|
1305
|
+
tokensConsumed: match.consumedTokens
|
|
1306
|
+
};
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
return {
|
|
1309
|
+
confidence: 0,
|
|
1310
|
+
errors: [error instanceof Error ? error.message : String(error)]
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
supportsLanguage(language) {
|
|
1315
|
+
return this.languages.has(language);
|
|
1316
|
+
}
|
|
1317
|
+
supportedLanguages() {
|
|
1318
|
+
return Array.from(this.languages);
|
|
1319
|
+
}
|
|
1320
|
+
getCacheStats() {
|
|
1321
|
+
return this.cache.getStats();
|
|
1322
|
+
}
|
|
1323
|
+
clearCache() {
|
|
1324
|
+
this.cache.clear();
|
|
1325
|
+
}
|
|
1326
|
+
configureCache(config) {
|
|
1327
|
+
this.cache.configure(config);
|
|
1328
|
+
}
|
|
1329
|
+
buildSemanticNode(match) {
|
|
1330
|
+
return {
|
|
1331
|
+
kind: "command",
|
|
1332
|
+
action: match.pattern.command,
|
|
1333
|
+
roles: match.captured,
|
|
1334
|
+
metadata: {
|
|
1335
|
+
patternId: match.pattern.id
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
function createSemanticAnalyzer(options) {
|
|
1341
|
+
return new SemanticAnalyzerImpl(options);
|
|
1342
|
+
}
|
|
1343
|
+
var DEFAULT_CONFIDENCE_THRESHOLD = 0.5;
|
|
1344
|
+
var HIGH_CONFIDENCE_THRESHOLD = 0.8;
|
|
1345
|
+
function shouldUseSemanticResult(result, threshold = DEFAULT_CONFIDENCE_THRESHOLD) {
|
|
1346
|
+
return result.confidence >= threshold && result.command !== void 0;
|
|
1347
|
+
}
|
|
1348
|
+
function rolesToCommandArgs(roles, command) {
|
|
1349
|
+
const args = [];
|
|
1350
|
+
const modifiers = {};
|
|
1351
|
+
for (const [role, value] of roles) {
|
|
1352
|
+
switch (role) {
|
|
1353
|
+
// Primary arguments (positional)
|
|
1354
|
+
case "patient":
|
|
1355
|
+
case "event":
|
|
1356
|
+
args.push(value);
|
|
1357
|
+
break;
|
|
1358
|
+
// Destination: context-dependent preposition
|
|
1359
|
+
case "destination":
|
|
1360
|
+
if (command === "put") {
|
|
1361
|
+
modifiers["into"] = value;
|
|
1362
|
+
} else {
|
|
1363
|
+
modifiers["on"] = value;
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1366
|
+
// Source: always 'from'
|
|
1367
|
+
case "source":
|
|
1368
|
+
modifiers["from"] = value;
|
|
1369
|
+
break;
|
|
1370
|
+
// Quantitative roles
|
|
1371
|
+
case "quantity":
|
|
1372
|
+
modifiers["by"] = value;
|
|
1373
|
+
break;
|
|
1374
|
+
case "duration":
|
|
1375
|
+
modifiers["over"] = value;
|
|
1376
|
+
break;
|
|
1377
|
+
// Adverbial roles
|
|
1378
|
+
case "method":
|
|
1379
|
+
modifiers["as"] = value;
|
|
1380
|
+
break;
|
|
1381
|
+
case "style":
|
|
1382
|
+
modifiers["with"] = value;
|
|
1383
|
+
break;
|
|
1384
|
+
// Conditional
|
|
1385
|
+
case "condition":
|
|
1386
|
+
modifiers["if"] = value;
|
|
1387
|
+
break;
|
|
1388
|
+
// Agent (for future multi-actor systems)
|
|
1389
|
+
case "agent":
|
|
1390
|
+
modifiers["agent"] = value;
|
|
1391
|
+
break;
|
|
1392
|
+
default:
|
|
1393
|
+
modifiers[role] = value;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return { args, modifiers };
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/ast-builder/expression-parser/tokenizer.ts
|
|
1400
|
+
var CONTEXT_VARS = /* @__PURE__ */ new Set([
|
|
1401
|
+
"me",
|
|
1402
|
+
"my",
|
|
1403
|
+
"myself",
|
|
1404
|
+
"you",
|
|
1405
|
+
"your",
|
|
1406
|
+
"yourself",
|
|
1407
|
+
"it",
|
|
1408
|
+
"its",
|
|
1409
|
+
"result",
|
|
1410
|
+
"event",
|
|
1411
|
+
"target",
|
|
1412
|
+
"body",
|
|
1413
|
+
"detail",
|
|
1414
|
+
"window",
|
|
1415
|
+
"document"
|
|
1416
|
+
]);
|
|
1417
|
+
var LOGICAL_OPERATORS = /* @__PURE__ */ new Set(["and", "or", "not", "no"]);
|
|
1418
|
+
var BOOLEAN_LITERALS = /* @__PURE__ */ new Set(["true", "false", "null", "undefined"]);
|
|
1419
|
+
var TIME_UNITS = /* @__PURE__ */ new Set([
|
|
1420
|
+
"ms",
|
|
1421
|
+
"s",
|
|
1422
|
+
"seconds",
|
|
1423
|
+
"second",
|
|
1424
|
+
"milliseconds",
|
|
1425
|
+
"millisecond",
|
|
1426
|
+
"minutes",
|
|
1427
|
+
"minute",
|
|
1428
|
+
"hours",
|
|
1429
|
+
"hour"
|
|
1430
|
+
]);
|
|
1431
|
+
function tokenize2(input) {
|
|
1432
|
+
const tokens = [];
|
|
1433
|
+
let pos = 0;
|
|
1434
|
+
let line = 1;
|
|
1435
|
+
let column = 1;
|
|
1436
|
+
function previousTokenAllowsSelector() {
|
|
1437
|
+
if (tokens.length === 0) return true;
|
|
1438
|
+
const prev = tokens[tokens.length - 1];
|
|
1439
|
+
return [
|
|
1440
|
+
"OPERATOR" /* OPERATOR */,
|
|
1441
|
+
"COMPARISON" /* COMPARISON */,
|
|
1442
|
+
"LOGICAL" /* LOGICAL */,
|
|
1443
|
+
"LPAREN" /* LPAREN */,
|
|
1444
|
+
"LBRACKET" /* LBRACKET */,
|
|
1445
|
+
"LBRACE" /* LBRACE */,
|
|
1446
|
+
"COMMA" /* COMMA */,
|
|
1447
|
+
"COLON" /* COLON */
|
|
1448
|
+
].includes(prev.type);
|
|
1449
|
+
}
|
|
1450
|
+
function peek(offset = 0) {
|
|
1451
|
+
return input[pos + offset] ?? "";
|
|
1452
|
+
}
|
|
1453
|
+
function advance() {
|
|
1454
|
+
const char = input[pos];
|
|
1455
|
+
pos++;
|
|
1456
|
+
if (char === "\n") {
|
|
1457
|
+
line++;
|
|
1458
|
+
column = 1;
|
|
1459
|
+
} else {
|
|
1460
|
+
column++;
|
|
1461
|
+
}
|
|
1462
|
+
return char;
|
|
1463
|
+
}
|
|
1464
|
+
function skipWhitespace() {
|
|
1465
|
+
while (pos < input.length && /\s/.test(input[pos])) {
|
|
1466
|
+
advance();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
function readWhile(predicate) {
|
|
1470
|
+
let result = "";
|
|
1471
|
+
while (pos < input.length && predicate(input[pos])) {
|
|
1472
|
+
result += advance();
|
|
1473
|
+
}
|
|
1474
|
+
return result;
|
|
1475
|
+
}
|
|
1476
|
+
function readString(quote) {
|
|
1477
|
+
let result = quote;
|
|
1478
|
+
advance();
|
|
1479
|
+
while (pos < input.length && input[pos] !== quote) {
|
|
1480
|
+
if (input[pos] === "\\" && pos + 1 < input.length) {
|
|
1481
|
+
result += advance();
|
|
1482
|
+
result += advance();
|
|
1483
|
+
} else {
|
|
1484
|
+
result += advance();
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (pos < input.length) {
|
|
1488
|
+
result += advance();
|
|
1489
|
+
}
|
|
1490
|
+
return result;
|
|
1491
|
+
}
|
|
1492
|
+
function readTemplateLiteral() {
|
|
1493
|
+
let result = "`";
|
|
1494
|
+
advance();
|
|
1495
|
+
while (pos < input.length && input[pos] !== "`") {
|
|
1496
|
+
if (input[pos] === "\\" && pos + 1 < input.length) {
|
|
1497
|
+
result += advance();
|
|
1498
|
+
result += advance();
|
|
1499
|
+
} else {
|
|
1500
|
+
result += advance();
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (pos < input.length) {
|
|
1504
|
+
result += advance();
|
|
1505
|
+
}
|
|
1506
|
+
return result;
|
|
1507
|
+
}
|
|
1508
|
+
function readQuerySelector() {
|
|
1509
|
+
let result = "<";
|
|
1510
|
+
advance();
|
|
1511
|
+
while (pos < input.length) {
|
|
1512
|
+
result += advance();
|
|
1513
|
+
if (result.endsWith("/>")) {
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return result;
|
|
1518
|
+
}
|
|
1519
|
+
function makeToken(type, value, start) {
|
|
1520
|
+
return {
|
|
1521
|
+
type,
|
|
1522
|
+
value,
|
|
1523
|
+
start,
|
|
1524
|
+
end: pos,
|
|
1525
|
+
line,
|
|
1526
|
+
column: column - value.length
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
while (pos < input.length) {
|
|
1530
|
+
skipWhitespace();
|
|
1531
|
+
if (pos >= input.length) break;
|
|
1532
|
+
const start = pos;
|
|
1533
|
+
const char = peek();
|
|
1534
|
+
if (char === "'" && peek(1) === "s" && !/\w/.test(peek(2))) {
|
|
1535
|
+
advance();
|
|
1536
|
+
advance();
|
|
1537
|
+
tokens.push(makeToken("POSSESSIVE" /* POSSESSIVE */, "'s", start));
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (char === '"' || char === "'") {
|
|
1541
|
+
const value = readString(char);
|
|
1542
|
+
tokens.push(makeToken("STRING" /* STRING */, value, start));
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (char === "`") {
|
|
1546
|
+
const value = readTemplateLiteral();
|
|
1547
|
+
tokens.push(makeToken("TEMPLATE_LITERAL" /* TEMPLATE_LITERAL */, value, start));
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
if (char === "<" && /[a-zA-Z.#\[]/.test(peek(1))) {
|
|
1551
|
+
const value = readQuerySelector();
|
|
1552
|
+
tokens.push(makeToken("QUERY_SELECTOR" /* QUERY_SELECTOR */, value, start));
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
if (char === "#" && previousTokenAllowsSelector()) {
|
|
1556
|
+
advance();
|
|
1557
|
+
const name = readWhile((c) => /[\w-]/.test(c));
|
|
1558
|
+
tokens.push(makeToken("ID_SELECTOR" /* ID_SELECTOR */, "#" + name, start));
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
if (char === "." && /[a-zA-Z_-]/.test(peek(1)) && previousTokenAllowsSelector()) {
|
|
1562
|
+
advance();
|
|
1563
|
+
const name = readWhile((c) => /[\w-]/.test(c));
|
|
1564
|
+
tokens.push(makeToken("CLASS_SELECTOR" /* CLASS_SELECTOR */, "." + name, start));
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
if (char === "[" && previousTokenAllowsSelector()) {
|
|
1568
|
+
const nextChar = peek(1);
|
|
1569
|
+
if (nextChar === "@" || /[a-zA-Z]/.test(nextChar)) {
|
|
1570
|
+
let value = "";
|
|
1571
|
+
value += advance();
|
|
1572
|
+
while (pos < input.length && input[pos] !== "]") {
|
|
1573
|
+
if (input[pos] === '"' || input[pos] === "'") {
|
|
1574
|
+
value += readString(input[pos]);
|
|
1575
|
+
} else {
|
|
1576
|
+
value += advance();
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (pos < input.length) {
|
|
1580
|
+
value += advance();
|
|
1581
|
+
}
|
|
1582
|
+
tokens.push(makeToken("ATTRIBUTE_SELECTOR" /* ATTRIBUTE_SELECTOR */, value, start));
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (char === "[") {
|
|
1587
|
+
advance();
|
|
1588
|
+
tokens.push(makeToken("LBRACKET" /* LBRACKET */, "[", start));
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
if (char === "]") {
|
|
1592
|
+
advance();
|
|
1593
|
+
tokens.push(makeToken("RBRACKET" /* RBRACKET */, "]", start));
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
if (/\d/.test(char)) {
|
|
1597
|
+
const num = readWhile((c) => /[\d.]/.test(c));
|
|
1598
|
+
const unitStart = pos;
|
|
1599
|
+
const unit = readWhile((c) => /[a-zA-Z]/.test(c));
|
|
1600
|
+
if (TIME_UNITS.has(unit)) {
|
|
1601
|
+
tokens.push(makeToken("TIME_EXPRESSION" /* TIME_EXPRESSION */, num + unit, start));
|
|
1602
|
+
} else {
|
|
1603
|
+
pos = unitStart;
|
|
1604
|
+
tokens.push(makeToken("NUMBER" /* NUMBER */, num, start));
|
|
1605
|
+
}
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
if (char === "(") {
|
|
1609
|
+
advance();
|
|
1610
|
+
tokens.push(makeToken("LPAREN" /* LPAREN */, "(", start));
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (char === ")") {
|
|
1614
|
+
advance();
|
|
1615
|
+
tokens.push(makeToken("RPAREN" /* RPAREN */, ")", start));
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
if (char === "{") {
|
|
1619
|
+
advance();
|
|
1620
|
+
tokens.push(makeToken("LBRACE" /* LBRACE */, "{", start));
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (char === "}") {
|
|
1624
|
+
advance();
|
|
1625
|
+
tokens.push(makeToken("RBRACE" /* RBRACE */, "}", start));
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (char === ",") {
|
|
1629
|
+
advance();
|
|
1630
|
+
tokens.push(makeToken("COMMA" /* COMMA */, ",", start));
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
if (char === ":") {
|
|
1634
|
+
advance();
|
|
1635
|
+
tokens.push(makeToken("COLON" /* COLON */, ":", start));
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
if (char === ".") {
|
|
1639
|
+
advance();
|
|
1640
|
+
tokens.push(makeToken("DOT" /* DOT */, ".", start));
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
if (char === "+" || char === "-" || char === "*" || char === "/" || char === "%") {
|
|
1644
|
+
advance();
|
|
1645
|
+
tokens.push(makeToken("OPERATOR" /* OPERATOR */, char, start));
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
if (char === "=" || char === "!" || char === "<" || char === ">") {
|
|
1649
|
+
let op = advance();
|
|
1650
|
+
if (peek() === "=") {
|
|
1651
|
+
op += advance();
|
|
1652
|
+
}
|
|
1653
|
+
tokens.push(makeToken("COMPARISON" /* COMPARISON */, op, start));
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
if (/[a-zA-Z_$]/.test(char)) {
|
|
1657
|
+
const word = readWhile((c) => /[\w$]/.test(c));
|
|
1658
|
+
const lower = word.toLowerCase();
|
|
1659
|
+
if (CONTEXT_VARS.has(lower)) {
|
|
1660
|
+
tokens.push(makeToken("CONTEXT_VAR" /* CONTEXT_VAR */, word, start));
|
|
1661
|
+
} else if (LOGICAL_OPERATORS.has(lower)) {
|
|
1662
|
+
tokens.push(makeToken("LOGICAL" /* LOGICAL */, word, start));
|
|
1663
|
+
} else if (BOOLEAN_LITERALS.has(lower)) {
|
|
1664
|
+
tokens.push(makeToken("BOOLEAN" /* BOOLEAN */, word, start));
|
|
1665
|
+
} else {
|
|
1666
|
+
tokens.push(makeToken("IDENTIFIER" /* IDENTIFIER */, word, start));
|
|
1667
|
+
}
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
advance();
|
|
1671
|
+
}
|
|
1672
|
+
tokens.push(makeToken("EOF" /* EOF */, "", pos));
|
|
1673
|
+
return tokens;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// src/ast-builder/expression-parser/parser.ts
|
|
1677
|
+
var ExpressionParser = class {
|
|
1678
|
+
constructor() {
|
|
1679
|
+
this.tokens = [];
|
|
1680
|
+
this.current = 0;
|
|
1681
|
+
}
|
|
1682
|
+
parse(input) {
|
|
1683
|
+
try {
|
|
1684
|
+
this.tokens = tokenize2(input);
|
|
1685
|
+
this.current = 0;
|
|
1686
|
+
if (this.isAtEnd()) {
|
|
1687
|
+
return { success: false, error: "Empty expression" };
|
|
1688
|
+
}
|
|
1689
|
+
const node = this.parseExpression();
|
|
1690
|
+
return { success: true, node, consumed: this.current };
|
|
1691
|
+
} catch (e) {
|
|
1692
|
+
return {
|
|
1693
|
+
success: false,
|
|
1694
|
+
error: e instanceof Error ? e.message : "Parse error"
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
// =============================================================================
|
|
1699
|
+
// Token Navigation
|
|
1700
|
+
// =============================================================================
|
|
1701
|
+
peek() {
|
|
1702
|
+
return this.tokens[this.current] ?? { type: "EOF" /* EOF */, value: "", start: 0, end: 0 };
|
|
1703
|
+
}
|
|
1704
|
+
previous() {
|
|
1705
|
+
return this.tokens[this.current - 1] ?? { type: "EOF" /* EOF */, value: "", start: 0, end: 0 };
|
|
1706
|
+
}
|
|
1707
|
+
isAtEnd() {
|
|
1708
|
+
return this.peek().type === "EOF" /* EOF */;
|
|
1709
|
+
}
|
|
1710
|
+
advance() {
|
|
1711
|
+
if (!this.isAtEnd()) {
|
|
1712
|
+
this.current++;
|
|
1713
|
+
}
|
|
1714
|
+
return this.previous();
|
|
1715
|
+
}
|
|
1716
|
+
check(type) {
|
|
1717
|
+
return this.peek().type === type;
|
|
1718
|
+
}
|
|
1719
|
+
checkValue(value) {
|
|
1720
|
+
return this.peek().value.toLowerCase() === value.toLowerCase();
|
|
1721
|
+
}
|
|
1722
|
+
match(...types) {
|
|
1723
|
+
for (const type of types) {
|
|
1724
|
+
if (this.check(type)) {
|
|
1725
|
+
this.advance();
|
|
1726
|
+
return true;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1731
|
+
// =============================================================================
|
|
1732
|
+
// Expression Parsing (Precedence Climbing)
|
|
1733
|
+
// =============================================================================
|
|
1734
|
+
parseExpression() {
|
|
1735
|
+
return this.parseOr();
|
|
1736
|
+
}
|
|
1737
|
+
parseOr() {
|
|
1738
|
+
let left = this.parseAnd();
|
|
1739
|
+
while (this.checkValue("or")) {
|
|
1740
|
+
const operator = this.advance().value;
|
|
1741
|
+
const right = this.parseAnd();
|
|
1742
|
+
left = this.createBinaryExpression(operator, left, right);
|
|
1743
|
+
}
|
|
1744
|
+
return left;
|
|
1745
|
+
}
|
|
1746
|
+
parseAnd() {
|
|
1747
|
+
let left = this.parseEquality();
|
|
1748
|
+
while (this.checkValue("and")) {
|
|
1749
|
+
const operator = this.advance().value;
|
|
1750
|
+
const right = this.parseEquality();
|
|
1751
|
+
left = this.createBinaryExpression(operator, left, right);
|
|
1752
|
+
}
|
|
1753
|
+
return left;
|
|
1754
|
+
}
|
|
1755
|
+
parseEquality() {
|
|
1756
|
+
let left = this.parseComparison();
|
|
1757
|
+
while (this.match("COMPARISON" /* COMPARISON */) || this.checkValue("is") || this.checkValue("matches") || this.checkValue("contains") || this.checkValue("in")) {
|
|
1758
|
+
const operator = this.previous().value;
|
|
1759
|
+
const right = this.parseComparison();
|
|
1760
|
+
left = this.createBinaryExpression(operator, left, right);
|
|
1761
|
+
}
|
|
1762
|
+
return left;
|
|
1763
|
+
}
|
|
1764
|
+
parseComparison() {
|
|
1765
|
+
let left = this.parseAddition();
|
|
1766
|
+
while (this.check("COMPARISON" /* COMPARISON */)) {
|
|
1767
|
+
const operator = this.advance().value;
|
|
1768
|
+
const right = this.parseAddition();
|
|
1769
|
+
left = this.createBinaryExpression(operator, left, right);
|
|
1770
|
+
}
|
|
1771
|
+
return left;
|
|
1772
|
+
}
|
|
1773
|
+
parseAddition() {
|
|
1774
|
+
let left = this.parseMultiplication();
|
|
1775
|
+
while (this.peek().value === "+" || this.peek().value === "-") {
|
|
1776
|
+
const operator = this.advance().value;
|
|
1777
|
+
const right = this.parseMultiplication();
|
|
1778
|
+
left = this.createBinaryExpression(operator, left, right);
|
|
1779
|
+
}
|
|
1780
|
+
return left;
|
|
1781
|
+
}
|
|
1782
|
+
parseMultiplication() {
|
|
1783
|
+
let left = this.parseUnary();
|
|
1784
|
+
while (this.peek().value === "*" || this.peek().value === "/" || this.peek().value === "%") {
|
|
1785
|
+
const operator = this.advance().value;
|
|
1786
|
+
const right = this.parseUnary();
|
|
1787
|
+
left = this.createBinaryExpression(operator, left, right);
|
|
1788
|
+
}
|
|
1789
|
+
return left;
|
|
1790
|
+
}
|
|
1791
|
+
parseUnary() {
|
|
1792
|
+
if (this.checkValue("not") || this.checkValue("no") || this.peek().value === "-") {
|
|
1793
|
+
const operator = this.advance().value;
|
|
1794
|
+
const operand = this.parseUnary();
|
|
1795
|
+
return this.createUnaryExpression(operator, operand);
|
|
1796
|
+
}
|
|
1797
|
+
return this.parsePostfix();
|
|
1798
|
+
}
|
|
1799
|
+
parsePostfix() {
|
|
1800
|
+
let expr = this.parsePrimary();
|
|
1801
|
+
while (true) {
|
|
1802
|
+
if (this.match("DOT" /* DOT */)) {
|
|
1803
|
+
if (this.check("IDENTIFIER" /* IDENTIFIER */) || this.check("CONTEXT_VAR" /* CONTEXT_VAR */)) {
|
|
1804
|
+
const property = this.advance().value;
|
|
1805
|
+
expr = this.createPropertyAccess(expr, property);
|
|
1806
|
+
} else {
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
} else if (this.match("POSSESSIVE" /* POSSESSIVE */)) {
|
|
1810
|
+
if (this.check("IDENTIFIER" /* IDENTIFIER */) || this.check("CONTEXT_VAR" /* CONTEXT_VAR */)) {
|
|
1811
|
+
const property = this.advance().value;
|
|
1812
|
+
expr = this.createPossessiveExpression(expr, property);
|
|
1813
|
+
} else {
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
} else if (this.match("LPAREN" /* LPAREN */)) {
|
|
1817
|
+
const args = this.parseArguments();
|
|
1818
|
+
expr = this.createCallExpression(expr, args);
|
|
1819
|
+
} else if (this.match("LBRACKET" /* LBRACKET */)) {
|
|
1820
|
+
const index = this.parseExpression();
|
|
1821
|
+
if (!this.match("RBRACKET" /* RBRACKET */)) {
|
|
1822
|
+
throw new Error("Expected ] after index");
|
|
1823
|
+
}
|
|
1824
|
+
expr = this.createPropertyAccess(expr, index);
|
|
1825
|
+
} else {
|
|
1826
|
+
break;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
return expr;
|
|
1830
|
+
}
|
|
1831
|
+
parsePrimary() {
|
|
1832
|
+
const token = this.peek();
|
|
1833
|
+
if (this.match("NUMBER" /* NUMBER */)) {
|
|
1834
|
+
return this.createLiteral(parseFloat(token.value), "number", token);
|
|
1835
|
+
}
|
|
1836
|
+
if (this.match("STRING" /* STRING */)) {
|
|
1837
|
+
const value = token.value.slice(1, -1);
|
|
1838
|
+
return this.createLiteral(value, "string", token);
|
|
1839
|
+
}
|
|
1840
|
+
if (this.match("BOOLEAN" /* BOOLEAN */)) {
|
|
1841
|
+
const value = token.value === "true" ? true : token.value === "false" ? false : token.value === "null" ? null : void 0;
|
|
1842
|
+
return this.createLiteral(value, token.value, token);
|
|
1843
|
+
}
|
|
1844
|
+
if (this.match("TEMPLATE_LITERAL" /* TEMPLATE_LITERAL */)) {
|
|
1845
|
+
const templateNode = {
|
|
1846
|
+
type: "templateLiteral",
|
|
1847
|
+
value: token.value,
|
|
1848
|
+
start: token.start,
|
|
1849
|
+
end: token.end,
|
|
1850
|
+
line: token.line,
|
|
1851
|
+
column: token.column
|
|
1852
|
+
};
|
|
1853
|
+
return templateNode;
|
|
1854
|
+
}
|
|
1855
|
+
if (this.match("TIME_EXPRESSION" /* TIME_EXPRESSION */)) {
|
|
1856
|
+
return this.parseTimeExpression(token);
|
|
1857
|
+
}
|
|
1858
|
+
if (this.match("ID_SELECTOR" /* ID_SELECTOR */)) {
|
|
1859
|
+
return this.createSelector(token.value, "id", token);
|
|
1860
|
+
}
|
|
1861
|
+
if (this.match("CLASS_SELECTOR" /* CLASS_SELECTOR */)) {
|
|
1862
|
+
return this.createSelector(token.value, "class", token);
|
|
1863
|
+
}
|
|
1864
|
+
if (this.match("ATTRIBUTE_SELECTOR" /* ATTRIBUTE_SELECTOR */)) {
|
|
1865
|
+
return this.createSelector(token.value, "attribute", token);
|
|
1866
|
+
}
|
|
1867
|
+
if (this.match("QUERY_SELECTOR" /* QUERY_SELECTOR */)) {
|
|
1868
|
+
const selector = token.value.slice(1, -2);
|
|
1869
|
+
return this.createSelector(selector, "query", token);
|
|
1870
|
+
}
|
|
1871
|
+
if (this.match("CONTEXT_VAR" /* CONTEXT_VAR */)) {
|
|
1872
|
+
return this.createContextReference(token.value, token);
|
|
1873
|
+
}
|
|
1874
|
+
if (this.match("IDENTIFIER" /* IDENTIFIER */)) {
|
|
1875
|
+
return this.createIdentifier(token.value, token);
|
|
1876
|
+
}
|
|
1877
|
+
if (this.match("LPAREN" /* LPAREN */)) {
|
|
1878
|
+
const expr = this.parseExpression();
|
|
1879
|
+
if (!this.match("RPAREN" /* RPAREN */)) {
|
|
1880
|
+
throw new Error("Expected ) after expression");
|
|
1881
|
+
}
|
|
1882
|
+
return expr;
|
|
1883
|
+
}
|
|
1884
|
+
if (this.match("LBRACKET" /* LBRACKET */)) {
|
|
1885
|
+
return this.parseArrayLiteral();
|
|
1886
|
+
}
|
|
1887
|
+
if (this.match("LBRACE" /* LBRACE */)) {
|
|
1888
|
+
return this.parseObjectLiteral();
|
|
1889
|
+
}
|
|
1890
|
+
throw new Error(`Unexpected token: ${token.value}`);
|
|
1891
|
+
}
|
|
1892
|
+
parseArguments() {
|
|
1893
|
+
const args = [];
|
|
1894
|
+
if (!this.check("RPAREN" /* RPAREN */)) {
|
|
1895
|
+
do {
|
|
1896
|
+
args.push(this.parseExpression());
|
|
1897
|
+
} while (this.match("COMMA" /* COMMA */));
|
|
1898
|
+
}
|
|
1899
|
+
if (!this.match("RPAREN" /* RPAREN */)) {
|
|
1900
|
+
throw new Error("Expected ) after arguments");
|
|
1901
|
+
}
|
|
1902
|
+
return args;
|
|
1903
|
+
}
|
|
1904
|
+
parseArrayLiteral() {
|
|
1905
|
+
const elements = [];
|
|
1906
|
+
const start = this.previous().start;
|
|
1907
|
+
if (!this.check("RBRACKET" /* RBRACKET */)) {
|
|
1908
|
+
do {
|
|
1909
|
+
elements.push(this.parseExpression());
|
|
1910
|
+
} while (this.match("COMMA" /* COMMA */));
|
|
1911
|
+
}
|
|
1912
|
+
if (!this.match("RBRACKET" /* RBRACKET */)) {
|
|
1913
|
+
throw new Error("Expected ] after array elements");
|
|
1914
|
+
}
|
|
1915
|
+
return {
|
|
1916
|
+
type: "arrayLiteral",
|
|
1917
|
+
elements,
|
|
1918
|
+
start,
|
|
1919
|
+
end: this.previous().end
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
parseObjectLiteral() {
|
|
1923
|
+
const properties = [];
|
|
1924
|
+
const start = this.previous().start;
|
|
1925
|
+
if (!this.check("RBRACE" /* RBRACE */)) {
|
|
1926
|
+
do {
|
|
1927
|
+
let key;
|
|
1928
|
+
if (this.check("STRING" /* STRING */)) {
|
|
1929
|
+
key = this.advance().value.slice(1, -1);
|
|
1930
|
+
} else if (this.check("IDENTIFIER" /* IDENTIFIER */)) {
|
|
1931
|
+
key = this.advance().value;
|
|
1932
|
+
} else {
|
|
1933
|
+
throw new Error("Expected property name");
|
|
1934
|
+
}
|
|
1935
|
+
if (!this.match("COLON" /* COLON */)) {
|
|
1936
|
+
throw new Error("Expected : after property name");
|
|
1937
|
+
}
|
|
1938
|
+
const value = this.parseExpression();
|
|
1939
|
+
properties.push({ key, value });
|
|
1940
|
+
} while (this.match("COMMA" /* COMMA */));
|
|
1941
|
+
}
|
|
1942
|
+
if (!this.match("RBRACE" /* RBRACE */)) {
|
|
1943
|
+
throw new Error("Expected } after object properties");
|
|
1944
|
+
}
|
|
1945
|
+
return {
|
|
1946
|
+
type: "objectLiteral",
|
|
1947
|
+
properties: properties.map((p) => ({
|
|
1948
|
+
type: "objectProperty",
|
|
1949
|
+
key: p.key,
|
|
1950
|
+
value: p.value
|
|
1951
|
+
})),
|
|
1952
|
+
start,
|
|
1953
|
+
end: this.previous().end
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
parseTimeExpression(token) {
|
|
1957
|
+
const match = token.value.match(
|
|
1958
|
+
/^(\d+(?:\.\d+)?)(ms|s|seconds?|milliseconds?|minutes?|hours?)$/i
|
|
1959
|
+
);
|
|
1960
|
+
if (!match) {
|
|
1961
|
+
throw new Error(`Invalid time expression: ${token.value}`);
|
|
1962
|
+
}
|
|
1963
|
+
const value = parseFloat(match[1]);
|
|
1964
|
+
const unit = match[2].toLowerCase();
|
|
1965
|
+
return {
|
|
1966
|
+
type: "timeExpression",
|
|
1967
|
+
value,
|
|
1968
|
+
unit,
|
|
1969
|
+
raw: token.value,
|
|
1970
|
+
start: token.start,
|
|
1971
|
+
end: token.end,
|
|
1972
|
+
line: token.line,
|
|
1973
|
+
column: token.column
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
// =============================================================================
|
|
1977
|
+
// Node Factories
|
|
1978
|
+
// =============================================================================
|
|
1979
|
+
createLiteral(value, dataType, token) {
|
|
1980
|
+
return {
|
|
1981
|
+
type: "literal",
|
|
1982
|
+
value,
|
|
1983
|
+
dataType,
|
|
1984
|
+
raw: token.value,
|
|
1985
|
+
start: token.start,
|
|
1986
|
+
end: token.end,
|
|
1987
|
+
line: token.line,
|
|
1988
|
+
column: token.column
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
createSelector(value, kind, token) {
|
|
1992
|
+
return {
|
|
1993
|
+
type: "selector",
|
|
1994
|
+
value,
|
|
1995
|
+
selector: value,
|
|
1996
|
+
selectorType: kind,
|
|
1997
|
+
start: token.start,
|
|
1998
|
+
end: token.end,
|
|
1999
|
+
line: token.line,
|
|
2000
|
+
column: token.column
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
createContextReference(contextType, token) {
|
|
2004
|
+
return {
|
|
2005
|
+
type: "contextReference",
|
|
2006
|
+
contextType,
|
|
2007
|
+
name: token.value,
|
|
2008
|
+
start: token.start,
|
|
2009
|
+
end: token.end,
|
|
2010
|
+
line: token.line,
|
|
2011
|
+
column: token.column
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
createIdentifier(name, token) {
|
|
2015
|
+
return {
|
|
2016
|
+
type: "identifier",
|
|
2017
|
+
name,
|
|
2018
|
+
start: token.start,
|
|
2019
|
+
end: token.end,
|
|
2020
|
+
line: token.line,
|
|
2021
|
+
column: token.column
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
createPropertyAccess(object, property) {
|
|
2025
|
+
return {
|
|
2026
|
+
type: "propertyAccess",
|
|
2027
|
+
object,
|
|
2028
|
+
property: typeof property === "string" ? property : property.name ?? "",
|
|
2029
|
+
start: object.start,
|
|
2030
|
+
end: this.previous().end
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
createPossessiveExpression(object, property) {
|
|
2034
|
+
return {
|
|
2035
|
+
type: "possessiveExpression",
|
|
2036
|
+
object,
|
|
2037
|
+
property,
|
|
2038
|
+
start: object.start,
|
|
2039
|
+
end: this.previous().end
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
createBinaryExpression(operator, left, right) {
|
|
2043
|
+
return {
|
|
2044
|
+
type: "binaryExpression",
|
|
2045
|
+
operator,
|
|
2046
|
+
left,
|
|
2047
|
+
right,
|
|
2048
|
+
start: left.start,
|
|
2049
|
+
end: right.end
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
createUnaryExpression(operator, operand) {
|
|
2053
|
+
return {
|
|
2054
|
+
type: "unaryExpression",
|
|
2055
|
+
operator,
|
|
2056
|
+
operand,
|
|
2057
|
+
prefix: true,
|
|
2058
|
+
start: this.previous().start,
|
|
2059
|
+
end: operand.end
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
createCallExpression(callee, args) {
|
|
2063
|
+
return {
|
|
2064
|
+
type: "callExpression",
|
|
2065
|
+
callee,
|
|
2066
|
+
arguments: args,
|
|
2067
|
+
start: callee.start,
|
|
2068
|
+
end: this.previous().end
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
function parseExpression(input) {
|
|
2073
|
+
const parser = new ExpressionParser();
|
|
2074
|
+
return parser.parse(input);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/ast-builder/value-converters.ts
|
|
2078
|
+
function convertValue(value, warnings) {
|
|
2079
|
+
switch (value.type) {
|
|
2080
|
+
case "literal":
|
|
2081
|
+
return convertLiteral(value);
|
|
2082
|
+
case "selector":
|
|
2083
|
+
return convertSelector(value, warnings);
|
|
2084
|
+
case "reference":
|
|
2085
|
+
return convertReference(value);
|
|
2086
|
+
case "property-path":
|
|
2087
|
+
return convertPropertyPath(value, warnings);
|
|
2088
|
+
case "expression":
|
|
2089
|
+
return convertExpression(value);
|
|
2090
|
+
default:
|
|
2091
|
+
const _exhaustive = value;
|
|
2092
|
+
throw new Error(`Unknown semantic value type: ${_exhaustive.type}`);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
function convertLiteral(value) {
|
|
2096
|
+
const result = {
|
|
2097
|
+
type: "literal",
|
|
2098
|
+
value: value.value
|
|
2099
|
+
};
|
|
2100
|
+
if (value.dataType) {
|
|
2101
|
+
return { ...result, dataType: value.dataType };
|
|
2102
|
+
}
|
|
2103
|
+
return result;
|
|
2104
|
+
}
|
|
2105
|
+
function convertSelector(value, warnings) {
|
|
2106
|
+
if (warnings && value.value.startsWith("*") && /^[a-zA-Z-]/.test(value.value.slice(1))) {
|
|
2107
|
+
warnings.push(
|
|
2108
|
+
`Converted '${value.value}' to a CSS selector, but it looks like a CSS property name. CSS properties in commands like 'transition' should be literal strings, not selectors. Consider using expectedTypes: ['literal'] instead of ['literal', 'selector'] in the command schema.`
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
return {
|
|
2112
|
+
type: "selector",
|
|
2113
|
+
value: value.value,
|
|
2114
|
+
selector: value.value,
|
|
2115
|
+
selectorType: value.selectorKind
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
function convertReference(value) {
|
|
2119
|
+
return {
|
|
2120
|
+
type: "contextReference",
|
|
2121
|
+
contextType: value.value,
|
|
2122
|
+
name: value.value
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
function convertPropertyPath(value, warnings) {
|
|
2126
|
+
return {
|
|
2127
|
+
type: "propertyAccess",
|
|
2128
|
+
object: convertValue(value.object, warnings),
|
|
2129
|
+
property: value.property
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
function convertExpression(value) {
|
|
2133
|
+
const result = parseExpression(value.raw);
|
|
2134
|
+
if (!result.success || !result.node) {
|
|
2135
|
+
const identifier = {
|
|
2136
|
+
type: "identifier",
|
|
2137
|
+
name: value.raw
|
|
2138
|
+
};
|
|
2139
|
+
return identifier;
|
|
2140
|
+
}
|
|
2141
|
+
return result.node;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/ast-builder/command-mappers.ts
|
|
2145
|
+
function getRole(node, role) {
|
|
2146
|
+
return node.roles.get(role);
|
|
2147
|
+
}
|
|
2148
|
+
function convertRoleValue(node, role, warnings) {
|
|
2149
|
+
const value = getRole(node, role);
|
|
2150
|
+
return value ? convertValue(value, warnings) : void 0;
|
|
2151
|
+
}
|
|
2152
|
+
function createCommandNode2(name, args = [], modifiers, options = {}) {
|
|
2153
|
+
const result = {
|
|
2154
|
+
type: "command",
|
|
2155
|
+
name,
|
|
2156
|
+
args
|
|
2157
|
+
};
|
|
2158
|
+
if (modifiers && Object.keys(modifiers).length > 0) {
|
|
2159
|
+
result.modifiers = modifiers;
|
|
2160
|
+
}
|
|
2161
|
+
if (options.isBlocking) {
|
|
2162
|
+
result.isBlocking = options.isBlocking;
|
|
2163
|
+
}
|
|
2164
|
+
if (options.implicitTarget) {
|
|
2165
|
+
result.implicitTarget = options.implicitTarget;
|
|
2166
|
+
}
|
|
2167
|
+
return result;
|
|
2168
|
+
}
|
|
2169
|
+
var toggleMapper = {
|
|
2170
|
+
action: "toggle",
|
|
2171
|
+
toAST(node, _builder) {
|
|
2172
|
+
const patient = convertRoleValue(node, "patient");
|
|
2173
|
+
const destination = convertRoleValue(node, "destination");
|
|
2174
|
+
const duration = convertRoleValue(node, "duration");
|
|
2175
|
+
const args = patient ? [patient] : [];
|
|
2176
|
+
const modifiers = {};
|
|
2177
|
+
if (destination) modifiers["on"] = destination;
|
|
2178
|
+
if (duration) modifiers["for"] = duration;
|
|
2179
|
+
return createCommandNode2("toggle", args, modifiers);
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
var addMapper = {
|
|
2183
|
+
action: "add",
|
|
2184
|
+
toAST(node, _builder) {
|
|
2185
|
+
const patient = convertRoleValue(node, "patient");
|
|
2186
|
+
const destination = convertRoleValue(node, "destination");
|
|
2187
|
+
const args = patient ? [patient] : [];
|
|
2188
|
+
const modifiers = {};
|
|
2189
|
+
if (destination) modifiers["to"] = destination;
|
|
2190
|
+
return createCommandNode2("add", args, modifiers);
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
var removeMapper = {
|
|
2194
|
+
action: "remove",
|
|
2195
|
+
toAST(node, _builder) {
|
|
2196
|
+
const patient = convertRoleValue(node, "patient");
|
|
2197
|
+
const source = convertRoleValue(node, "source");
|
|
2198
|
+
const args = patient ? [patient] : [];
|
|
2199
|
+
const modifiers = {};
|
|
2200
|
+
if (source) modifiers["from"] = source;
|
|
2201
|
+
return createCommandNode2("remove", args, modifiers);
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
var setMapper = {
|
|
2205
|
+
action: "set",
|
|
2206
|
+
toAST(node, _builder) {
|
|
2207
|
+
const destination = convertRoleValue(node, "destination");
|
|
2208
|
+
const patient = convertRoleValue(node, "patient");
|
|
2209
|
+
const args = [];
|
|
2210
|
+
const modifiers = {};
|
|
2211
|
+
if (destination) {
|
|
2212
|
+
args.push(destination);
|
|
2213
|
+
}
|
|
2214
|
+
if (patient) modifiers["to"] = patient;
|
|
2215
|
+
return createCommandNode2("set", args, modifiers);
|
|
2216
|
+
}
|
|
2217
|
+
};
|
|
2218
|
+
var showMapper = {
|
|
2219
|
+
action: "show",
|
|
2220
|
+
toAST(node, _builder) {
|
|
2221
|
+
const destination = convertRoleValue(node, "destination");
|
|
2222
|
+
const patient = convertRoleValue(node, "patient");
|
|
2223
|
+
const duration = convertRoleValue(node, "duration");
|
|
2224
|
+
const args = [];
|
|
2225
|
+
const modifiers = {};
|
|
2226
|
+
const target = destination ?? patient;
|
|
2227
|
+
if (target) args.push(target);
|
|
2228
|
+
if (duration) modifiers["with"] = duration;
|
|
2229
|
+
return createCommandNode2("show", args, modifiers);
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
var hideMapper = {
|
|
2233
|
+
action: "hide",
|
|
2234
|
+
toAST(node, _builder) {
|
|
2235
|
+
const destination = convertRoleValue(node, "destination");
|
|
2236
|
+
const patient = convertRoleValue(node, "patient");
|
|
2237
|
+
const duration = convertRoleValue(node, "duration");
|
|
2238
|
+
const args = [];
|
|
2239
|
+
const modifiers = {};
|
|
2240
|
+
const target = destination ?? patient;
|
|
2241
|
+
if (target) args.push(target);
|
|
2242
|
+
if (duration) modifiers["with"] = duration;
|
|
2243
|
+
return createCommandNode2("hide", args, modifiers);
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
var incrementMapper = {
|
|
2247
|
+
action: "increment",
|
|
2248
|
+
toAST(node, _builder) {
|
|
2249
|
+
const destination = convertRoleValue(node, "destination");
|
|
2250
|
+
const patient = convertRoleValue(node, "patient");
|
|
2251
|
+
const quantity = convertRoleValue(node, "quantity");
|
|
2252
|
+
const args = [];
|
|
2253
|
+
const modifiers = {};
|
|
2254
|
+
const target = destination ?? patient;
|
|
2255
|
+
if (target) args.push(target);
|
|
2256
|
+
if (quantity) modifiers["by"] = quantity;
|
|
2257
|
+
return createCommandNode2("increment", args, modifiers);
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2260
|
+
var decrementMapper = {
|
|
2261
|
+
action: "decrement",
|
|
2262
|
+
toAST(node, _builder) {
|
|
2263
|
+
const destination = convertRoleValue(node, "destination");
|
|
2264
|
+
const patient = convertRoleValue(node, "patient");
|
|
2265
|
+
const quantity = convertRoleValue(node, "quantity");
|
|
2266
|
+
const args = [];
|
|
2267
|
+
const modifiers = {};
|
|
2268
|
+
const target = destination ?? patient;
|
|
2269
|
+
if (target) args.push(target);
|
|
2270
|
+
if (quantity) modifiers["by"] = quantity;
|
|
2271
|
+
return createCommandNode2("decrement", args, modifiers);
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
var waitMapper = {
|
|
2275
|
+
action: "wait",
|
|
2276
|
+
toAST(node, _builder) {
|
|
2277
|
+
const duration = convertRoleValue(node, "duration");
|
|
2278
|
+
const args = duration ? [duration] : [];
|
|
2279
|
+
return createCommandNode2("wait", args, void 0, { isBlocking: true });
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
var logMapper = {
|
|
2283
|
+
action: "log",
|
|
2284
|
+
toAST(node, _builder) {
|
|
2285
|
+
const patient = convertRoleValue(node, "patient");
|
|
2286
|
+
const args = [];
|
|
2287
|
+
if (patient) args.push(patient);
|
|
2288
|
+
return createCommandNode2("log", args);
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
var putMapper = {
|
|
2292
|
+
action: "put",
|
|
2293
|
+
toAST(node, _builder) {
|
|
2294
|
+
const patient = convertRoleValue(node, "patient");
|
|
2295
|
+
const destination = convertRoleValue(node, "destination");
|
|
2296
|
+
const method = getRole(node, "method");
|
|
2297
|
+
const args = patient ? [patient] : [];
|
|
2298
|
+
const modifiers = {};
|
|
2299
|
+
if (destination) {
|
|
2300
|
+
const prep = method?.type === "literal" ? String(method.value) : "into";
|
|
2301
|
+
modifiers[prep] = destination;
|
|
2302
|
+
}
|
|
2303
|
+
return createCommandNode2("put", args, modifiers);
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
var fetchMapper = {
|
|
2307
|
+
action: "fetch",
|
|
2308
|
+
toAST(node, _builder) {
|
|
2309
|
+
const source = convertRoleValue(node, "source");
|
|
2310
|
+
const method = convertRoleValue(node, "method");
|
|
2311
|
+
const responseType = convertRoleValue(node, "responseType");
|
|
2312
|
+
const patient = convertRoleValue(node, "patient");
|
|
2313
|
+
const args = source ? [source] : [];
|
|
2314
|
+
const modifiers = {};
|
|
2315
|
+
if (method) modifiers["with"] = method;
|
|
2316
|
+
if (responseType) modifiers["as"] = responseType;
|
|
2317
|
+
if (patient) modifiers["body"] = patient;
|
|
2318
|
+
return createCommandNode2("fetch", args, modifiers, { isBlocking: true });
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
var appendMapper = {
|
|
2322
|
+
action: "append",
|
|
2323
|
+
toAST(node, _builder) {
|
|
2324
|
+
const patient = convertRoleValue(node, "patient");
|
|
2325
|
+
const destination = convertRoleValue(node, "destination");
|
|
2326
|
+
const args = patient ? [patient] : [];
|
|
2327
|
+
const modifiers = {};
|
|
2328
|
+
if (destination) modifiers["to"] = destination;
|
|
2329
|
+
return createCommandNode2("append", args, modifiers);
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
var prependMapper = {
|
|
2333
|
+
action: "prepend",
|
|
2334
|
+
toAST(node, _builder) {
|
|
2335
|
+
const patient = convertRoleValue(node, "patient");
|
|
2336
|
+
const destination = convertRoleValue(node, "destination");
|
|
2337
|
+
const args = patient ? [patient] : [];
|
|
2338
|
+
const modifiers = {};
|
|
2339
|
+
if (destination) modifiers["to"] = destination;
|
|
2340
|
+
return createCommandNode2("prepend", args, modifiers);
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
var triggerMapper = {
|
|
2344
|
+
action: "trigger",
|
|
2345
|
+
toAST(node, _builder) {
|
|
2346
|
+
const event = convertRoleValue(node, "event");
|
|
2347
|
+
const destination = convertRoleValue(node, "destination");
|
|
2348
|
+
const args = event ? [event] : [];
|
|
2349
|
+
const modifiers = {};
|
|
2350
|
+
if (destination) modifiers["on"] = destination;
|
|
2351
|
+
return createCommandNode2("trigger", args, modifiers);
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
var sendMapper = {
|
|
2355
|
+
action: "send",
|
|
2356
|
+
toAST(node, _builder) {
|
|
2357
|
+
const event = convertRoleValue(node, "event");
|
|
2358
|
+
const destination = convertRoleValue(node, "destination");
|
|
2359
|
+
const patient = convertRoleValue(node, "patient");
|
|
2360
|
+
const args = event ? [event] : [];
|
|
2361
|
+
const modifiers = {};
|
|
2362
|
+
if (destination) modifiers["to"] = destination;
|
|
2363
|
+
if (patient) modifiers["detail"] = patient;
|
|
2364
|
+
return createCommandNode2("send", args, modifiers);
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
var goMapper = {
|
|
2368
|
+
action: "go",
|
|
2369
|
+
toAST(node, _builder) {
|
|
2370
|
+
const source = convertRoleValue(node, "source");
|
|
2371
|
+
const destination = convertRoleValue(node, "destination");
|
|
2372
|
+
const args = [];
|
|
2373
|
+
const modifiers = {};
|
|
2374
|
+
if (source) args.push(source);
|
|
2375
|
+
if (destination) modifiers["to"] = destination;
|
|
2376
|
+
return createCommandNode2("go", args, modifiers);
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
var transitionMapper = {
|
|
2380
|
+
action: "transition",
|
|
2381
|
+
toAST(node, _builder) {
|
|
2382
|
+
const patient = convertRoleValue(node, "patient");
|
|
2383
|
+
const goal = convertRoleValue(node, "goal");
|
|
2384
|
+
const duration = convertRoleValue(node, "duration");
|
|
2385
|
+
const destination = convertRoleValue(node, "destination");
|
|
2386
|
+
const args = patient ? [patient] : [];
|
|
2387
|
+
const modifiers = {};
|
|
2388
|
+
if (goal) modifiers["to"] = goal;
|
|
2389
|
+
if (duration) modifiers["over"] = duration;
|
|
2390
|
+
if (destination) modifiers["on"] = destination;
|
|
2391
|
+
return createCommandNode2("transition", args, modifiers);
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
var focusMapper = {
|
|
2395
|
+
action: "focus",
|
|
2396
|
+
toAST(node, _builder) {
|
|
2397
|
+
const destination = convertRoleValue(node, "destination");
|
|
2398
|
+
const patient = convertRoleValue(node, "patient");
|
|
2399
|
+
const args = [];
|
|
2400
|
+
const modifiers = {};
|
|
2401
|
+
const target = destination ?? patient;
|
|
2402
|
+
if (target) args.push(target);
|
|
2403
|
+
return createCommandNode2("focus", args, modifiers);
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
var blurMapper = {
|
|
2407
|
+
action: "blur",
|
|
2408
|
+
toAST(node, _builder) {
|
|
2409
|
+
const destination = convertRoleValue(node, "destination");
|
|
2410
|
+
const patient = convertRoleValue(node, "patient");
|
|
2411
|
+
const args = [];
|
|
2412
|
+
const target = destination ?? patient;
|
|
2413
|
+
if (target) args.push(target);
|
|
2414
|
+
return createCommandNode2("blur", args);
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
var getMapper = {
|
|
2418
|
+
action: "get",
|
|
2419
|
+
toAST(node, _builder) {
|
|
2420
|
+
const source = convertRoleValue(node, "source");
|
|
2421
|
+
const patient = convertRoleValue(node, "patient");
|
|
2422
|
+
const args = [];
|
|
2423
|
+
const value = source ?? patient;
|
|
2424
|
+
if (value) args.push(value);
|
|
2425
|
+
return createCommandNode2("get", args);
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
var takeMapper = {
|
|
2429
|
+
action: "take",
|
|
2430
|
+
toAST(node, _builder) {
|
|
2431
|
+
const patient = convertRoleValue(node, "patient");
|
|
2432
|
+
const source = convertRoleValue(node, "source");
|
|
2433
|
+
const args = patient ? [patient] : [];
|
|
2434
|
+
const modifiers = {};
|
|
2435
|
+
if (source) modifiers["from"] = source;
|
|
2436
|
+
return createCommandNode2("take", args, modifiers);
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2439
|
+
var callMapper = {
|
|
2440
|
+
action: "call",
|
|
2441
|
+
toAST(node, _builder) {
|
|
2442
|
+
const patient = convertRoleValue(node, "patient");
|
|
2443
|
+
const args = patient ? [patient] : [];
|
|
2444
|
+
return createCommandNode2("call", args);
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
var returnMapper = {
|
|
2448
|
+
action: "return",
|
|
2449
|
+
toAST(node, _builder) {
|
|
2450
|
+
const patient = convertRoleValue(node, "patient");
|
|
2451
|
+
const args = patient ? [patient] : [];
|
|
2452
|
+
return createCommandNode2("return", args);
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
var haltMapper = {
|
|
2456
|
+
action: "halt",
|
|
2457
|
+
toAST(_node, _builder) {
|
|
2458
|
+
return createCommandNode2("halt", []);
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
var throwMapper = {
|
|
2462
|
+
action: "throw",
|
|
2463
|
+
toAST(node, _builder) {
|
|
2464
|
+
const patient = convertRoleValue(node, "patient");
|
|
2465
|
+
const args = patient ? [patient] : [];
|
|
2466
|
+
return createCommandNode2("throw", args);
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
var settleMapper = {
|
|
2470
|
+
action: "settle",
|
|
2471
|
+
toAST(node, _builder) {
|
|
2472
|
+
const destination = convertRoleValue(node, "destination");
|
|
2473
|
+
const patient = convertRoleValue(node, "patient");
|
|
2474
|
+
const args = [];
|
|
2475
|
+
const target = destination ?? patient;
|
|
2476
|
+
if (target) args.push(target);
|
|
2477
|
+
return createCommandNode2("settle", args, void 0, { isBlocking: true });
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
var swapMapper = {
|
|
2481
|
+
action: "swap",
|
|
2482
|
+
toAST(node, _builder) {
|
|
2483
|
+
const patient = convertRoleValue(node, "patient");
|
|
2484
|
+
const source = convertRoleValue(node, "source");
|
|
2485
|
+
const destination = convertRoleValue(node, "destination");
|
|
2486
|
+
const style = convertRoleValue(node, "style");
|
|
2487
|
+
const args = [];
|
|
2488
|
+
const modifiers = {};
|
|
2489
|
+
if (patient) args.push(patient);
|
|
2490
|
+
if (source) args.push(source);
|
|
2491
|
+
if (destination) modifiers["on"] = destination;
|
|
2492
|
+
if (style) modifiers["with"] = style;
|
|
2493
|
+
return createCommandNode2("swap", args, modifiers);
|
|
2494
|
+
}
|
|
2495
|
+
};
|
|
2496
|
+
var morphMapper = {
|
|
2497
|
+
action: "morph",
|
|
2498
|
+
toAST(node, _builder) {
|
|
2499
|
+
const source = convertRoleValue(node, "source");
|
|
2500
|
+
const destination = convertRoleValue(node, "destination");
|
|
2501
|
+
const patient = convertRoleValue(node, "patient");
|
|
2502
|
+
const args = [];
|
|
2503
|
+
const modifiers = {};
|
|
2504
|
+
const content = source ?? patient;
|
|
2505
|
+
if (content) args.push(content);
|
|
2506
|
+
if (destination) modifiers["on"] = destination;
|
|
2507
|
+
return createCommandNode2("morph", args, modifiers);
|
|
2508
|
+
}
|
|
2509
|
+
};
|
|
2510
|
+
var cloneMapper = {
|
|
2511
|
+
action: "clone",
|
|
2512
|
+
toAST(node, _builder) {
|
|
2513
|
+
const source = convertRoleValue(node, "source");
|
|
2514
|
+
const destination = convertRoleValue(node, "destination");
|
|
2515
|
+
const patient = convertRoleValue(node, "patient");
|
|
2516
|
+
const args = [];
|
|
2517
|
+
const modifiers = {};
|
|
2518
|
+
const target = source ?? patient;
|
|
2519
|
+
if (target) args.push(target);
|
|
2520
|
+
if (destination) modifiers["into"] = destination;
|
|
2521
|
+
return createCommandNode2("clone", args, modifiers);
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
var makeMapper = {
|
|
2525
|
+
action: "make",
|
|
2526
|
+
toAST(node, _builder) {
|
|
2527
|
+
const patient = convertRoleValue(node, "patient");
|
|
2528
|
+
const args = patient ? [patient] : [];
|
|
2529
|
+
return createCommandNode2("make", args);
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
var measureMapper = {
|
|
2533
|
+
action: "measure",
|
|
2534
|
+
toAST(node, _builder) {
|
|
2535
|
+
const patient = convertRoleValue(node, "patient");
|
|
2536
|
+
const destination = convertRoleValue(node, "destination");
|
|
2537
|
+
const source = convertRoleValue(node, "source");
|
|
2538
|
+
const args = [];
|
|
2539
|
+
const modifiers = {};
|
|
2540
|
+
if (patient) args.push(patient);
|
|
2541
|
+
const element = destination ?? source;
|
|
2542
|
+
if (element) modifiers["of"] = element;
|
|
2543
|
+
return createCommandNode2("measure", args, modifiers);
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
var tellMapper = {
|
|
2547
|
+
action: "tell",
|
|
2548
|
+
toAST(node, _builder) {
|
|
2549
|
+
const destination = convertRoleValue(node, "destination");
|
|
2550
|
+
const patient = convertRoleValue(node, "patient");
|
|
2551
|
+
const args = [];
|
|
2552
|
+
const target = destination ?? patient;
|
|
2553
|
+
if (target) args.push(target);
|
|
2554
|
+
return createCommandNode2("tell", args);
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2557
|
+
var jsMapper = {
|
|
2558
|
+
action: "js",
|
|
2559
|
+
toAST(node, _builder) {
|
|
2560
|
+
const patient = convertRoleValue(node, "patient");
|
|
2561
|
+
const args = patient ? [patient] : [];
|
|
2562
|
+
return createCommandNode2("js", args);
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
var asyncMapper = {
|
|
2566
|
+
action: "async",
|
|
2567
|
+
toAST(_node, _builder) {
|
|
2568
|
+
return createCommandNode2("async", []);
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2571
|
+
var ifMapper = {
|
|
2572
|
+
action: "if",
|
|
2573
|
+
toAST(node, _builder) {
|
|
2574
|
+
const condition = convertRoleValue(node, "condition");
|
|
2575
|
+
const args = condition ? [condition] : [];
|
|
2576
|
+
return createCommandNode2("if", args);
|
|
2577
|
+
}
|
|
2578
|
+
};
|
|
2579
|
+
var unlessMapper = {
|
|
2580
|
+
action: "unless",
|
|
2581
|
+
toAST(node, _builder) {
|
|
2582
|
+
const condition = convertRoleValue(node, "condition");
|
|
2583
|
+
const args = condition ? [condition] : [];
|
|
2584
|
+
return createCommandNode2("unless", args);
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
var repeatMapper = {
|
|
2588
|
+
action: "repeat",
|
|
2589
|
+
toAST(node, _builder) {
|
|
2590
|
+
const quantity = convertRoleValue(node, "quantity");
|
|
2591
|
+
const patient = convertRoleValue(node, "patient");
|
|
2592
|
+
const args = [];
|
|
2593
|
+
const count = quantity ?? patient;
|
|
2594
|
+
if (count) args.push(count);
|
|
2595
|
+
return createCommandNode2("repeat", args);
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
var forMapper = {
|
|
2599
|
+
action: "for",
|
|
2600
|
+
toAST(node, _builder) {
|
|
2601
|
+
const patient = convertRoleValue(node, "patient");
|
|
2602
|
+
const source = convertRoleValue(node, "source");
|
|
2603
|
+
const args = patient ? [patient] : [];
|
|
2604
|
+
const modifiers = {};
|
|
2605
|
+
if (source) modifiers["in"] = source;
|
|
2606
|
+
return createCommandNode2("for", args, modifiers);
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
var whileMapper = {
|
|
2610
|
+
action: "while",
|
|
2611
|
+
toAST(node, _builder) {
|
|
2612
|
+
const condition = convertRoleValue(node, "condition");
|
|
2613
|
+
const args = condition ? [condition] : [];
|
|
2614
|
+
return createCommandNode2("while", args);
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
var continueMapper = {
|
|
2618
|
+
action: "continue",
|
|
2619
|
+
toAST(_node, _builder) {
|
|
2620
|
+
return createCommandNode2("continue", []);
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
var defaultMapper = {
|
|
2624
|
+
action: "default",
|
|
2625
|
+
toAST(node, _builder) {
|
|
2626
|
+
const patient = convertRoleValue(node, "patient");
|
|
2627
|
+
const source = convertRoleValue(node, "source");
|
|
2628
|
+
const args = patient ? [patient] : [];
|
|
2629
|
+
const modifiers = {};
|
|
2630
|
+
if (source) modifiers["to"] = source;
|
|
2631
|
+
return createCommandNode2("default", args, modifiers);
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
var initMapper = {
|
|
2635
|
+
action: "init",
|
|
2636
|
+
toAST(_node, _builder) {
|
|
2637
|
+
return createCommandNode2("init", []);
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
var behaviorMapper = {
|
|
2641
|
+
action: "behavior",
|
|
2642
|
+
toAST(node, _builder) {
|
|
2643
|
+
const patient = convertRoleValue(node, "patient");
|
|
2644
|
+
const args = patient ? [patient] : [];
|
|
2645
|
+
return createCommandNode2("behavior", args);
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
var installMapper = {
|
|
2649
|
+
action: "install",
|
|
2650
|
+
toAST(node, _builder) {
|
|
2651
|
+
const patient = convertRoleValue(node, "patient");
|
|
2652
|
+
const destination = convertRoleValue(node, "destination");
|
|
2653
|
+
const args = patient ? [patient] : [];
|
|
2654
|
+
const modifiers = {};
|
|
2655
|
+
if (destination) modifiers["on"] = destination;
|
|
2656
|
+
return createCommandNode2("install", args, modifiers);
|
|
2657
|
+
}
|
|
2658
|
+
};
|
|
2659
|
+
var onMapper = {
|
|
2660
|
+
action: "on",
|
|
2661
|
+
toAST(node, _builder) {
|
|
2662
|
+
const event = convertRoleValue(node, "event");
|
|
2663
|
+
const source = convertRoleValue(node, "source");
|
|
2664
|
+
const args = event ? [event] : [];
|
|
2665
|
+
const modifiers = {};
|
|
2666
|
+
if (source) modifiers["from"] = source;
|
|
2667
|
+
return createCommandNode2("on", args, modifiers);
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
var mappers = /* @__PURE__ */ new Map([
|
|
2671
|
+
// Tier 1: Core commands
|
|
2672
|
+
["toggle", toggleMapper],
|
|
2673
|
+
["add", addMapper],
|
|
2674
|
+
["remove", removeMapper],
|
|
2675
|
+
["set", setMapper],
|
|
2676
|
+
["show", showMapper],
|
|
2677
|
+
["hide", hideMapper],
|
|
2678
|
+
["increment", incrementMapper],
|
|
2679
|
+
["decrement", decrementMapper],
|
|
2680
|
+
["wait", waitMapper],
|
|
2681
|
+
["log", logMapper],
|
|
2682
|
+
["put", putMapper],
|
|
2683
|
+
["fetch", fetchMapper],
|
|
2684
|
+
// Tier 2: Content manipulation
|
|
2685
|
+
["append", appendMapper],
|
|
2686
|
+
["prepend", prependMapper],
|
|
2687
|
+
["get", getMapper],
|
|
2688
|
+
["take", takeMapper],
|
|
2689
|
+
// Tier 2: Events
|
|
2690
|
+
["trigger", triggerMapper],
|
|
2691
|
+
["send", sendMapper],
|
|
2692
|
+
["on", onMapper],
|
|
2693
|
+
// Tier 2: Navigation & DOM
|
|
2694
|
+
["go", goMapper],
|
|
2695
|
+
["transition", transitionMapper],
|
|
2696
|
+
["focus", focusMapper],
|
|
2697
|
+
["blur", blurMapper],
|
|
2698
|
+
// Tier 2: Control flow
|
|
2699
|
+
["call", callMapper],
|
|
2700
|
+
["return", returnMapper],
|
|
2701
|
+
["halt", haltMapper],
|
|
2702
|
+
["throw", throwMapper],
|
|
2703
|
+
["settle", settleMapper],
|
|
2704
|
+
// Tier 3: Advanced DOM
|
|
2705
|
+
["swap", swapMapper],
|
|
2706
|
+
["morph", morphMapper],
|
|
2707
|
+
["clone", cloneMapper],
|
|
2708
|
+
["measure", measureMapper],
|
|
2709
|
+
// Tier 3: Object/Types
|
|
2710
|
+
["make", makeMapper],
|
|
2711
|
+
["tell", tellMapper],
|
|
2712
|
+
["default", defaultMapper],
|
|
2713
|
+
// Tier 3: JavaScript integration
|
|
2714
|
+
["js", jsMapper],
|
|
2715
|
+
["async", asyncMapper],
|
|
2716
|
+
// Tier 3: Conditionals
|
|
2717
|
+
["if", ifMapper],
|
|
2718
|
+
["unless", unlessMapper],
|
|
2719
|
+
// Tier 3: Loops
|
|
2720
|
+
["repeat", repeatMapper],
|
|
2721
|
+
["for", forMapper],
|
|
2722
|
+
["while", whileMapper],
|
|
2723
|
+
["continue", continueMapper],
|
|
2724
|
+
// Tier 3: Behaviors
|
|
2725
|
+
["init", initMapper],
|
|
2726
|
+
["behavior", behaviorMapper],
|
|
2727
|
+
["install", installMapper]
|
|
2728
|
+
]);
|
|
2729
|
+
function getCommandMapper(action) {
|
|
2730
|
+
return mappers.get(action);
|
|
2731
|
+
}
|
|
2732
|
+
function registerCommandMapper(mapper) {
|
|
2733
|
+
mappers.set(mapper.action, mapper);
|
|
2734
|
+
}
|
|
2735
|
+
function getRegisteredMappers() {
|
|
2736
|
+
return new Map(mappers);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// src/ast-builder/index.ts
|
|
2740
|
+
var ASTBuilder = class {
|
|
2741
|
+
constructor(_options = {}) {
|
|
2742
|
+
/**
|
|
2743
|
+
* Warnings collected during AST building (e.g., type inference issues).
|
|
2744
|
+
*/
|
|
2745
|
+
this.warnings = [];
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Build an AST from a SemanticNode.
|
|
2749
|
+
*
|
|
2750
|
+
* @param node - The semantic node to convert
|
|
2751
|
+
* @returns The corresponding AST node
|
|
2752
|
+
*/
|
|
2753
|
+
build(node) {
|
|
2754
|
+
switch (node.kind) {
|
|
2755
|
+
case "command":
|
|
2756
|
+
return this.buildCommand(node);
|
|
2757
|
+
case "event-handler":
|
|
2758
|
+
return this.buildEventHandler(node);
|
|
2759
|
+
case "conditional":
|
|
2760
|
+
return this.buildConditional(node);
|
|
2761
|
+
case "compound":
|
|
2762
|
+
return this.buildCompound(node);
|
|
2763
|
+
case "loop":
|
|
2764
|
+
return this.buildLoop(node);
|
|
2765
|
+
default:
|
|
2766
|
+
throw new Error(`Unknown semantic node kind: ${node.kind}`);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Build a CommandNode from a CommandSemanticNode.
|
|
2771
|
+
*/
|
|
2772
|
+
buildCommand(node) {
|
|
2773
|
+
const mapper = getCommandMapper(node.action);
|
|
2774
|
+
if (mapper) {
|
|
2775
|
+
const result = mapper.toAST(node, this);
|
|
2776
|
+
if ("ast" in result && "warnings" in result) {
|
|
2777
|
+
const mapperResult = result;
|
|
2778
|
+
this.warnings.push(...mapperResult.warnings);
|
|
2779
|
+
return mapperResult.ast;
|
|
2780
|
+
} else {
|
|
2781
|
+
return result;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return this.buildGenericCommand(node);
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* Generic command builder when no specific mapper is available.
|
|
2788
|
+
* Maps roles to args in a predictable order.
|
|
2789
|
+
*/
|
|
2790
|
+
buildGenericCommand(node) {
|
|
2791
|
+
const args = [];
|
|
2792
|
+
const modifiers = {};
|
|
2793
|
+
const argRoles = ["patient", "source", "quantity"];
|
|
2794
|
+
const modifierRoles = ["destination", "duration", "method", "style"];
|
|
2795
|
+
for (const role of argRoles) {
|
|
2796
|
+
const value = node.roles.get(role);
|
|
2797
|
+
if (value) {
|
|
2798
|
+
args.push(convertValue(value));
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
for (const role of modifierRoles) {
|
|
2802
|
+
const value = node.roles.get(role);
|
|
2803
|
+
if (value) {
|
|
2804
|
+
const modifierKey = this.roleToModifierKey(role);
|
|
2805
|
+
modifiers[modifierKey] = convertValue(value);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
const result = {
|
|
2809
|
+
type: "command",
|
|
2810
|
+
name: node.action,
|
|
2811
|
+
args
|
|
2812
|
+
};
|
|
2813
|
+
if (Object.keys(modifiers).length > 0) {
|
|
2814
|
+
return { ...result, modifiers };
|
|
2815
|
+
}
|
|
2816
|
+
return result;
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Map semantic roles to hyperscript modifier keywords.
|
|
2820
|
+
*/
|
|
2821
|
+
roleToModifierKey(role) {
|
|
2822
|
+
const mapping = {
|
|
2823
|
+
destination: "on",
|
|
2824
|
+
duration: "for",
|
|
2825
|
+
source: "from",
|
|
2826
|
+
condition: "if",
|
|
2827
|
+
method: "via",
|
|
2828
|
+
style: "with"
|
|
2829
|
+
};
|
|
2830
|
+
return mapping[role] ?? role;
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Build an EventHandlerNode from an EventHandlerSemanticNode.
|
|
2834
|
+
*/
|
|
2835
|
+
buildEventHandler(node) {
|
|
2836
|
+
const eventValue = node.roles.get("event");
|
|
2837
|
+
let event;
|
|
2838
|
+
let events;
|
|
2839
|
+
if (eventValue?.type === "literal") {
|
|
2840
|
+
const eventStr = String(eventValue.value);
|
|
2841
|
+
if (eventStr.includes("|") || eventStr.includes(" or ")) {
|
|
2842
|
+
events = eventStr.split(/\s+or\s+|\|/).map((e) => e.trim());
|
|
2843
|
+
event = events[0];
|
|
2844
|
+
} else {
|
|
2845
|
+
event = eventStr;
|
|
2846
|
+
}
|
|
2847
|
+
} else if (eventValue?.type === "reference") {
|
|
2848
|
+
event = eventValue.value;
|
|
2849
|
+
} else {
|
|
2850
|
+
event = "click";
|
|
2851
|
+
}
|
|
2852
|
+
const commands = node.body.map((child) => this.build(child));
|
|
2853
|
+
const fromValue = node.roles.get("source");
|
|
2854
|
+
let selector;
|
|
2855
|
+
let target;
|
|
2856
|
+
if (fromValue?.type === "selector") {
|
|
2857
|
+
selector = fromValue.value;
|
|
2858
|
+
target = fromValue.value;
|
|
2859
|
+
} else if (fromValue?.type === "reference") {
|
|
2860
|
+
target = fromValue.value;
|
|
2861
|
+
} else if (fromValue?.type === "literal") {
|
|
2862
|
+
target = String(fromValue.value);
|
|
2863
|
+
}
|
|
2864
|
+
const conditionValue = node.roles.get("condition");
|
|
2865
|
+
const condition = conditionValue ? convertValue(conditionValue) : void 0;
|
|
2866
|
+
const destinationValue = node.roles.get("destination");
|
|
2867
|
+
const watchTarget = destinationValue ? convertValue(destinationValue) : void 0;
|
|
2868
|
+
const modifiers = node.eventModifiers;
|
|
2869
|
+
let finalSelector = selector;
|
|
2870
|
+
if (modifiers?.from) {
|
|
2871
|
+
const fromMod = modifiers.from;
|
|
2872
|
+
if (fromMod.type === "selector" && !selector) {
|
|
2873
|
+
finalSelector = fromMod.value;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
const args = node.parameterNames ? [...node.parameterNames] : void 0;
|
|
2877
|
+
return {
|
|
2878
|
+
type: "eventHandler",
|
|
2879
|
+
event,
|
|
2880
|
+
commands,
|
|
2881
|
+
...events && events.length > 1 ? { events } : {},
|
|
2882
|
+
...finalSelector ? { selector: finalSelector } : {},
|
|
2883
|
+
...target ? { target } : {},
|
|
2884
|
+
...condition ? { condition } : {},
|
|
2885
|
+
...watchTarget ? { watchTarget } : {},
|
|
2886
|
+
...args && args.length > 0 ? { args, params: args } : {}
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Build a CommandNode from a ConditionalSemanticNode.
|
|
2891
|
+
*
|
|
2892
|
+
* Produces a command node with:
|
|
2893
|
+
* - args[0]: condition expression
|
|
2894
|
+
* - args[1]: then block (wrapped in { type: 'block', commands: [...] })
|
|
2895
|
+
* - args[2]: else block (optional, same format)
|
|
2896
|
+
*
|
|
2897
|
+
* This format matches what IfCommand.parseInput() expects.
|
|
2898
|
+
*/
|
|
2899
|
+
buildConditional(node) {
|
|
2900
|
+
const conditionValue = node.roles.get("condition");
|
|
2901
|
+
if (!conditionValue) {
|
|
2902
|
+
throw new Error("Conditional node missing condition");
|
|
2903
|
+
}
|
|
2904
|
+
const condition = convertValue(conditionValue);
|
|
2905
|
+
const thenBranch = node.thenBranch.map((child) => this.build(child));
|
|
2906
|
+
const elseBranch = node.elseBranch?.map((child) => this.build(child));
|
|
2907
|
+
const args = [
|
|
2908
|
+
condition,
|
|
2909
|
+
// args[1]: then block wrapped as block node
|
|
2910
|
+
{
|
|
2911
|
+
type: "block",
|
|
2912
|
+
commands: thenBranch
|
|
2913
|
+
}
|
|
2914
|
+
];
|
|
2915
|
+
if (elseBranch && elseBranch.length > 0) {
|
|
2916
|
+
args.push({
|
|
2917
|
+
type: "block",
|
|
2918
|
+
commands: elseBranch
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
return {
|
|
2922
|
+
type: "command",
|
|
2923
|
+
name: "if",
|
|
2924
|
+
args
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* Build AST nodes from a CompoundSemanticNode.
|
|
2929
|
+
*
|
|
2930
|
+
* Converts to CommandSequence for runtime compatibility.
|
|
2931
|
+
* The runtime recognizes 'CommandSequence' type and executes commands in order.
|
|
2932
|
+
*/
|
|
2933
|
+
buildCompound(node) {
|
|
2934
|
+
const statements = node.statements.map((child) => this.build(child));
|
|
2935
|
+
if (statements.length === 1) {
|
|
2936
|
+
return statements[0];
|
|
2937
|
+
}
|
|
2938
|
+
if (statements.length === 0) {
|
|
2939
|
+
return {
|
|
2940
|
+
type: "block",
|
|
2941
|
+
commands: []
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
const result = {
|
|
2945
|
+
type: "CommandSequence",
|
|
2946
|
+
commands: statements
|
|
2947
|
+
};
|
|
2948
|
+
return result;
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Build a CommandNode from a LoopSemanticNode.
|
|
2952
|
+
*
|
|
2953
|
+
* Produces a 'repeat' command with:
|
|
2954
|
+
* - args[0]: loop type identifier (forever, times, for, while, until)
|
|
2955
|
+
* - args[1]: count/condition/variable depending on loop type
|
|
2956
|
+
* - args[2]: collection (for 'for' loops)
|
|
2957
|
+
* - args[last]: body block
|
|
2958
|
+
*
|
|
2959
|
+
* This format matches what the repeat command parser produces.
|
|
2960
|
+
*/
|
|
2961
|
+
buildLoop(node) {
|
|
2962
|
+
const bodyCommands = node.body.map((child) => this.build(child));
|
|
2963
|
+
const args = [
|
|
2964
|
+
// args[0]: loop type identifier
|
|
2965
|
+
{
|
|
2966
|
+
type: "identifier",
|
|
2967
|
+
name: node.loopVariant
|
|
2968
|
+
}
|
|
2969
|
+
];
|
|
2970
|
+
switch (node.loopVariant) {
|
|
2971
|
+
case "times": {
|
|
2972
|
+
const quantity = node.roles.get("quantity");
|
|
2973
|
+
if (quantity) {
|
|
2974
|
+
args.push(convertValue(quantity));
|
|
2975
|
+
}
|
|
2976
|
+
break;
|
|
2977
|
+
}
|
|
2978
|
+
case "for": {
|
|
2979
|
+
if (node.loopVariable) {
|
|
2980
|
+
args.push({
|
|
2981
|
+
type: "string",
|
|
2982
|
+
value: node.loopVariable
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
const source = node.roles.get("source");
|
|
2986
|
+
if (source) {
|
|
2987
|
+
args.push(convertValue(source));
|
|
2988
|
+
}
|
|
2989
|
+
break;
|
|
2990
|
+
}
|
|
2991
|
+
case "while":
|
|
2992
|
+
case "until": {
|
|
2993
|
+
const condition = node.roles.get("condition");
|
|
2994
|
+
if (condition) {
|
|
2995
|
+
args.push(convertValue(condition));
|
|
2996
|
+
}
|
|
2997
|
+
break;
|
|
2998
|
+
}
|
|
2999
|
+
case "forever":
|
|
3000
|
+
break;
|
|
3001
|
+
}
|
|
3002
|
+
args.push({
|
|
3003
|
+
type: "block",
|
|
3004
|
+
commands: bodyCommands
|
|
3005
|
+
});
|
|
3006
|
+
return {
|
|
3007
|
+
type: "command",
|
|
3008
|
+
name: "repeat",
|
|
3009
|
+
args
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Build a BlockNode from an array of semantic nodes.
|
|
3014
|
+
* Useful for grouping commands in if/else branches.
|
|
3015
|
+
*/
|
|
3016
|
+
buildBlock(nodes) {
|
|
3017
|
+
const commands = nodes.map((child) => this.build(child));
|
|
3018
|
+
return {
|
|
3019
|
+
type: "block",
|
|
3020
|
+
commands
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
function buildAST(node) {
|
|
3025
|
+
const builder = new ASTBuilder();
|
|
3026
|
+
const ast = builder.build(node);
|
|
3027
|
+
return {
|
|
3028
|
+
ast,
|
|
3029
|
+
warnings: builder.warnings
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
export {
|
|
3033
|
+
ASTBuilder,
|
|
3034
|
+
DEFAULT_CONFIDENCE_THRESHOLD,
|
|
3035
|
+
HIGH_CONFIDENCE_THRESHOLD,
|
|
3036
|
+
SemanticAnalyzerImpl,
|
|
3037
|
+
SemanticCache,
|
|
3038
|
+
buildAST,
|
|
3039
|
+
convertExpression,
|
|
3040
|
+
convertLiteral,
|
|
3041
|
+
convertPropertyPath,
|
|
3042
|
+
convertReference,
|
|
3043
|
+
convertSelector,
|
|
3044
|
+
convertValue,
|
|
3045
|
+
createCommandNode,
|
|
3046
|
+
createEventHandler,
|
|
3047
|
+
createLiteral,
|
|
3048
|
+
createPropertyPath,
|
|
3049
|
+
createReference,
|
|
3050
|
+
createSelector,
|
|
3051
|
+
createSemanticAnalyzer,
|
|
3052
|
+
createSemanticCache,
|
|
3053
|
+
getCommandMapper,
|
|
3054
|
+
getPatternsForLanguage,
|
|
3055
|
+
getPatternsForLanguageAndCommand,
|
|
3056
|
+
getProfile,
|
|
3057
|
+
getRegisteredLanguages,
|
|
3058
|
+
getRegisteredMappers,
|
|
3059
|
+
getTokenizer,
|
|
3060
|
+
isLanguageRegistered,
|
|
3061
|
+
isLanguageSupported,
|
|
3062
|
+
mergeProfiles,
|
|
3063
|
+
registerCommandMapper,
|
|
3064
|
+
registerLanguage,
|
|
3065
|
+
registerPatterns,
|
|
3066
|
+
rolesToCommandArgs,
|
|
3067
|
+
semanticCache,
|
|
3068
|
+
setPatternGenerator,
|
|
3069
|
+
shouldUseSemanticResult,
|
|
3070
|
+
tryGetProfile,
|
|
3071
|
+
withCache
|
|
3072
|
+
};
|
|
3073
|
+
//# sourceMappingURL=core.js.map
|