@shapeshift-labs/frontier-lang-parser 0.3.44 → 0.3.46
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/action-body.js +32 -31
- package/dist/action-expression.js +316 -0
- package/dist/action-syntax-children.js +55 -11
- package/package.json +1 -1
package/dist/action-body.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFrontierNestedBlocks } from './source-syntax-report.js';
|
|
2
|
+
import { readActionValue } from './action-expression.js';
|
|
2
3
|
|
|
3
4
|
export function readActionBodyRecords(body) {
|
|
4
5
|
const records = [];
|
|
@@ -27,7 +28,8 @@ function parseActionBodyRecords(source, state) {
|
|
|
27
28
|
const close = findMatchingBrace(source, open);
|
|
28
29
|
if (close < 0) break;
|
|
29
30
|
const index = state.index++;
|
|
30
|
-
|
|
31
|
+
const record = parseActionIfBlock(ifHeader[1].trim(), source.slice(open + 1, close), index, state);
|
|
32
|
+
if (record) records.push(record);
|
|
31
33
|
offset = close + 1;
|
|
32
34
|
continue;
|
|
33
35
|
}
|
|
@@ -45,6 +47,7 @@ function parseActionBodyRecords(source, state) {
|
|
|
45
47
|
|
|
46
48
|
function parseActionIfBlock(header, body, index, state) {
|
|
47
49
|
const details = parseActionIfHeader(header, index);
|
|
50
|
+
if (!details.condition) return undefined;
|
|
48
51
|
return compactRecord({
|
|
49
52
|
kind: 'if',
|
|
50
53
|
id: details.id,
|
|
@@ -74,22 +77,34 @@ function parseActionBodyLine(line, index) {
|
|
|
74
77
|
const name = rawName && !rawName.startsWith('@') ? rawName : `${rowKind}_${index}`;
|
|
75
78
|
const rest = rawName?.startsWith('@') ? ` ${rawName}${rawRest ?? ''}` : (rawRest ?? '');
|
|
76
79
|
if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
|
|
77
|
-
|
|
80
|
+
const path = readInlineWord('path', rest);
|
|
81
|
+
const valueType = readInlineType(rest);
|
|
82
|
+
const value = readInlineActionValue('value', rest, { valueType });
|
|
83
|
+
if (!path || !value) return undefined;
|
|
84
|
+
return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path, valueType, value });
|
|
78
85
|
}
|
|
79
86
|
if (rowKind === 'remove') {
|
|
80
|
-
|
|
87
|
+
const path = readInlineWord('path', rest);
|
|
88
|
+
if (!path) return undefined;
|
|
89
|
+
return compactRecord({ kind: 'patch', op: 'remove', id: idFrom(rest, `action_body_remove_${name}`), name, path });
|
|
81
90
|
}
|
|
82
91
|
if (rowKind === 'callEffect') {
|
|
83
|
-
|
|
92
|
+
const inputText = readInlineValue('input', rest);
|
|
93
|
+
const input = inputText ? readActionValue(inputText) : undefined;
|
|
94
|
+
if (inputText && !input) return undefined;
|
|
95
|
+
return compactRecord({ kind: 'callEffect', id: idFrom(rest, `action_body_callEffect_${name}`), name, capability: readInlineWord('capability', rest) ?? readInlineWord('effect', rest) ?? name, input });
|
|
84
96
|
}
|
|
85
97
|
if (rowKind === 'let') {
|
|
86
|
-
const
|
|
98
|
+
const valueType = readInlineType(rest);
|
|
99
|
+
const value = readInlineActionBindingValue('value', rest, { valueType });
|
|
87
100
|
if (!rawName || rawName.startsWith('@') || !isActionBindingName(name) || !value) return undefined;
|
|
88
|
-
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, value });
|
|
101
|
+
return compactRecord({ kind: 'let', id: idFrom(rest, `action_body_let_${name}`), name, valueType, value });
|
|
89
102
|
}
|
|
90
103
|
if (rowKind === 'return') {
|
|
91
|
-
const valueText = rawName?.startsWith('@') ? rest
|
|
92
|
-
|
|
104
|
+
const valueText = stripIds(rawName?.startsWith('@') ? rest : `${rawName ?? ''}${rest ?? ''}`).trim();
|
|
105
|
+
const value = valueText ? readActionValue(valueText) : undefined;
|
|
106
|
+
if (valueText && !value) return undefined;
|
|
107
|
+
return compactRecord({ kind: 'return', id: idFrom(rest, `action_body_return_${index}`), value });
|
|
93
108
|
}
|
|
94
109
|
return undefined;
|
|
95
110
|
}
|
|
@@ -158,31 +173,14 @@ function findMatchingBrace(source, open) {
|
|
|
158
173
|
return -1;
|
|
159
174
|
}
|
|
160
175
|
|
|
161
|
-
function readInlineActionValue(label, text) {
|
|
162
|
-
const value =
|
|
163
|
-
return value ? readActionValue(value) : undefined;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function readInlineActionBindingValue(label, text) {
|
|
167
|
-
const value = readInlineActionValue(label, text);
|
|
168
|
-
return isSupportedBindingValue(value) ? value : undefined;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function readActionValue(value) {
|
|
172
|
-
const text = value.trim();
|
|
173
|
-
const quoted = /^["']([^"']*)["']$/.exec(text);
|
|
174
|
-
if (quoted) return { value: quoted[1] };
|
|
175
|
-
if (text === 'true') return { value: true };
|
|
176
|
-
if (text === 'false') return { value: false };
|
|
177
|
-
if (text === 'null') return { value: null };
|
|
178
|
-
if (/^-?\d+(?:\.\d+)?$/.test(text)) return { value: Number(text) };
|
|
179
|
-
return { expression: text };
|
|
176
|
+
function readInlineActionValue(label, text, options = {}) {
|
|
177
|
+
const value = readInlineValue(label, text);
|
|
178
|
+
return value ? readActionValue(value, options) : undefined;
|
|
180
179
|
}
|
|
181
180
|
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return /^[A-Za-z_$][\w$-]*(?:\.[A-Za-z_$][\w$-]*)*$/.test(String(value.expression ?? '').trim());
|
|
181
|
+
function readInlineActionBindingValue(label, text, options = {}) {
|
|
182
|
+
const value = readInlineActionValue(label, text, options);
|
|
183
|
+
return value ?? undefined;
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
function isActionBindingName(value) {
|
|
@@ -190,6 +188,9 @@ function isActionBindingName(value) {
|
|
|
190
188
|
}
|
|
191
189
|
|
|
192
190
|
function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
|
|
191
|
+
function readInlineType(text) { return readInlineWord('type', text) ?? readInlineWord('valueType', text); }
|
|
193
192
|
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
|
193
|
+
function readInlineValue(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim(); }
|
|
194
|
+
function stripIds(text) { return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim(); }
|
|
194
195
|
function firstIdentifier(text) { return /^([A-Za-z_$][\w$-]*)\b/.exec(text)?.[1]; }
|
|
195
196
|
function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0))); }
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$-]*$/;
|
|
2
|
+
const BLOCKED_REF_ROOTS = new Set(['globalThis', 'process', 'window', 'document', 'constructor', 'prototype', '__proto__', 'env']);
|
|
3
|
+
const NUMERIC_OPERATORS = new Set(['+', '-', '*', '/', '%']);
|
|
4
|
+
const NUMERIC_TYPES = new Set(['number', 'numeric', 'int', 'integer', 'float', 'double', 'decimal']);
|
|
5
|
+
|
|
6
|
+
export function readActionValue(value, options = {}) {
|
|
7
|
+
const parsed = parseActionValue(value, options);
|
|
8
|
+
return parsed.ok ? parsed.value : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseActionValue(value, options = {}) {
|
|
12
|
+
const text = String(value ?? '').trim();
|
|
13
|
+
if (!text) return fail('missing-action-expression');
|
|
14
|
+
const expression = parseActionExpression(text, options);
|
|
15
|
+
if (!expression.ok) return expression;
|
|
16
|
+
if (hasNumericOperator(expression.ast) && !isNumericType(options.valueType ?? options.type)) {
|
|
17
|
+
return fail((options.valueType ?? options.type) ? 'unsupported-action-expression-type' : 'missing-action-expression-type');
|
|
18
|
+
}
|
|
19
|
+
const valueType = options.valueType ?? options.type;
|
|
20
|
+
if (expression.literal) return { ok: true, value: compactRecord({ value: expression.ast.value, valueType }) };
|
|
21
|
+
return { ok: true, value: compactRecord({ expression: text, expressionAst: expression.ast, valueType }) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseActionExpression(value, options = {}) {
|
|
25
|
+
const text = String(value ?? '').trim();
|
|
26
|
+
if (!text) return fail('missing-action-expression');
|
|
27
|
+
const tokens = tokenizeActionExpression(text);
|
|
28
|
+
if (!tokens.ok) return tokens;
|
|
29
|
+
const parser = new ExpressionParser(tokens.tokens, options);
|
|
30
|
+
const ast = parser.parseExpression();
|
|
31
|
+
if (!parser.ok) return fail(parser.reason);
|
|
32
|
+
if (parser.peek().type !== 'eof') return fail(parser.reasonForTrailingToken());
|
|
33
|
+
return { ok: true, ast, literal: ast.kind === 'literal' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isSupportedActionValue(value, options = {}) {
|
|
37
|
+
return parseActionValue(value, options).ok;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tokenizeActionExpression(text) {
|
|
41
|
+
const tokens = [];
|
|
42
|
+
let index = 0;
|
|
43
|
+
while (index < text.length) {
|
|
44
|
+
const char = text[index];
|
|
45
|
+
if (/\s/.test(char)) {
|
|
46
|
+
index++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === '"' || char === "'") {
|
|
50
|
+
const read = readStringToken(text, index);
|
|
51
|
+
if (!read.ok) return read;
|
|
52
|
+
tokens.push(read.token);
|
|
53
|
+
index = read.next;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const canReadSignedNumber = char !== '-' || canStartSignedNumber(tokens);
|
|
57
|
+
const number = canReadSignedNumber ? /^-?\d+(?:\.\d+)?/.exec(text.slice(index)) : undefined;
|
|
58
|
+
if (number) {
|
|
59
|
+
tokens.push({ type: 'number', value: Number(number[0]), text: number[0], start: index, end: index + number[0].length });
|
|
60
|
+
index += number[0].length;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const identifier = /^[A-Za-z_$][\w$-]*/.exec(text.slice(index));
|
|
64
|
+
if (identifier) {
|
|
65
|
+
tokens.push({ type: 'identifier', text: identifier[0], start: index, end: index + identifier[0].length });
|
|
66
|
+
index += identifier[0].length;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const triple = text.slice(index, index + 3);
|
|
70
|
+
if (triple === '===' || triple === '!==') return fail('unsupported-action-expression-operator');
|
|
71
|
+
const double = text.slice(index, index + 2);
|
|
72
|
+
if (double === '&&' || double === '||' || double === '==' || double === '!=' || double === '>=' || double === '<=') {
|
|
73
|
+
tokens.push({ type: 'operator', text: double, start: index, end: index + 2 });
|
|
74
|
+
index += 2;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (char === '!' || char === '>' || char === '<' || NUMERIC_OPERATORS.has(char)) {
|
|
78
|
+
tokens.push({ type: 'operator', text: char, start: index, end: index + 1 });
|
|
79
|
+
index++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (char === '(' || char === ')' || char === '.') {
|
|
83
|
+
tokens.push({ type: 'punctuation', text: char, start: index, end: index + 1 });
|
|
84
|
+
index++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if ('=&|?:[]{}'.includes(char)) return fail('unsupported-action-expression-operator');
|
|
88
|
+
return fail('malformed-action-expression');
|
|
89
|
+
}
|
|
90
|
+
tokens.push({ type: 'eof', text: '', start: text.length, end: text.length });
|
|
91
|
+
return { ok: true, tokens };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readStringToken(text, start) {
|
|
95
|
+
const quote = text[start];
|
|
96
|
+
let value = '';
|
|
97
|
+
let index = start + 1;
|
|
98
|
+
while (index < text.length) {
|
|
99
|
+
const char = text[index];
|
|
100
|
+
if (char === quote) {
|
|
101
|
+
return { ok: true, token: { type: 'string', value, text: text.slice(start, index + 1), start, end: index + 1 }, next: index + 1 };
|
|
102
|
+
}
|
|
103
|
+
if (char === '\\') {
|
|
104
|
+
const next = text[index + 1];
|
|
105
|
+
if (next === undefined) return fail('malformed-action-expression');
|
|
106
|
+
if (next === 'n') value += '\n';
|
|
107
|
+
else if (next === 'r') value += '\r';
|
|
108
|
+
else if (next === 't') value += '\t';
|
|
109
|
+
else if (next === '\\' || next === '"' || next === "'") value += next;
|
|
110
|
+
else return fail('malformed-action-expression');
|
|
111
|
+
index += 2;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
value += char;
|
|
115
|
+
index++;
|
|
116
|
+
}
|
|
117
|
+
return fail('malformed-action-expression');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class ExpressionParser {
|
|
121
|
+
constructor(tokens, options) {
|
|
122
|
+
this.tokens = tokens;
|
|
123
|
+
this.options = options;
|
|
124
|
+
this.index = 0;
|
|
125
|
+
this.ok = true;
|
|
126
|
+
this.reason = undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
parseExpression() {
|
|
130
|
+
return this.parseLogicalOr();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
parseLogicalOr() {
|
|
134
|
+
let left = this.parseLogicalAnd();
|
|
135
|
+
while (this.matchOperator('||')) left = { kind: 'logical', op: '||', left, right: this.parseLogicalAnd() };
|
|
136
|
+
return left;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
parseLogicalAnd() {
|
|
140
|
+
let left = this.parseComparison();
|
|
141
|
+
while (this.matchOperator('&&')) left = { kind: 'logical', op: '&&', left, right: this.parseComparison() };
|
|
142
|
+
return left;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
parseComparison() {
|
|
146
|
+
let left = this.parseAdditive();
|
|
147
|
+
const operator = this.peek();
|
|
148
|
+
if (operator.type === 'operator' && ['==', '!=', '>', '>=', '<', '<='].includes(operator.text)) {
|
|
149
|
+
this.index++;
|
|
150
|
+
left = { kind: 'binary', op: operator.text, left, right: this.parseAdditive() };
|
|
151
|
+
if (this.peek().type === 'operator' && ['==', '!=', '>', '>=', '<', '<='].includes(this.peek().text)) {
|
|
152
|
+
this.reject('malformed-action-expression');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return left;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
parseAdditive() {
|
|
159
|
+
let left = this.parseMultiplicative();
|
|
160
|
+
while (this.peek().type === 'operator' && (this.peek().text === '+' || this.peek().text === '-')) {
|
|
161
|
+
const operator = this.peek().text;
|
|
162
|
+
this.index++;
|
|
163
|
+
left = { kind: 'binary', op: operator, left, right: this.parseMultiplicative() };
|
|
164
|
+
}
|
|
165
|
+
return left;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
parseMultiplicative() {
|
|
169
|
+
let left = this.parseUnary();
|
|
170
|
+
while (this.peek().type === 'operator' && (this.peek().text === '*' || this.peek().text === '/' || this.peek().text === '%')) {
|
|
171
|
+
const operator = this.peek().text;
|
|
172
|
+
this.index++;
|
|
173
|
+
left = { kind: 'binary', op: operator, left, right: this.parseUnary() };
|
|
174
|
+
}
|
|
175
|
+
return left;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
parseUnary() {
|
|
179
|
+
if (this.matchOperator('!')) return { kind: 'unary', op: '!', argument: this.parseUnary() };
|
|
180
|
+
return this.parsePrimary();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
parsePrimary() {
|
|
184
|
+
const token = this.peek();
|
|
185
|
+
if (token.type === 'number') {
|
|
186
|
+
this.index++;
|
|
187
|
+
return { kind: 'literal', value: token.value };
|
|
188
|
+
}
|
|
189
|
+
if (token.type === 'string') {
|
|
190
|
+
this.index++;
|
|
191
|
+
return { kind: 'literal', value: token.value };
|
|
192
|
+
}
|
|
193
|
+
if (token.type === 'identifier') {
|
|
194
|
+
if (token.text === 'true' || token.text === 'false') {
|
|
195
|
+
this.index++;
|
|
196
|
+
return { kind: 'literal', value: token.text === 'true' };
|
|
197
|
+
}
|
|
198
|
+
if (token.text === 'null') {
|
|
199
|
+
this.index++;
|
|
200
|
+
return { kind: 'literal', value: null };
|
|
201
|
+
}
|
|
202
|
+
return this.parseRef();
|
|
203
|
+
}
|
|
204
|
+
if (token.type === 'punctuation' && token.text === '(') {
|
|
205
|
+
this.index++;
|
|
206
|
+
const expression = this.parseExpression();
|
|
207
|
+
if (!this.matchPunctuation(')')) this.reject('malformed-action-expression');
|
|
208
|
+
return expression;
|
|
209
|
+
}
|
|
210
|
+
this.reject('malformed-action-expression');
|
|
211
|
+
return { kind: 'literal', value: null };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
parseRef() {
|
|
215
|
+
const parts = [];
|
|
216
|
+
const first = this.consumeIdentifier();
|
|
217
|
+
if (!first) return { kind: 'literal', value: null };
|
|
218
|
+
parts.push(first);
|
|
219
|
+
while (this.matchPunctuation('.')) {
|
|
220
|
+
const part = this.consumeIdentifier();
|
|
221
|
+
if (!part) {
|
|
222
|
+
this.reject('unsupported-action-expression-ref');
|
|
223
|
+
return refNode(parts);
|
|
224
|
+
}
|
|
225
|
+
parts.push(part);
|
|
226
|
+
}
|
|
227
|
+
if (this.peek().type === 'punctuation' && this.peek().text === '(') {
|
|
228
|
+
this.reject('unsupported-action-expression-ref');
|
|
229
|
+
return refNode(parts);
|
|
230
|
+
}
|
|
231
|
+
if (BLOCKED_REF_ROOTS.has(parts[0])) {
|
|
232
|
+
this.reject('unsupported-action-expression-ref');
|
|
233
|
+
return refNode(parts);
|
|
234
|
+
}
|
|
235
|
+
return refNode(parts);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
consumeIdentifier() {
|
|
239
|
+
const token = this.peek();
|
|
240
|
+
if (token.type !== 'identifier' || !IDENTIFIER.test(token.text)) {
|
|
241
|
+
this.reject('unsupported-action-expression-ref');
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
this.index++;
|
|
245
|
+
return token.text;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
matchOperator(operator) {
|
|
249
|
+
const token = this.peek();
|
|
250
|
+
if (token.type === 'operator' && token.text === operator) {
|
|
251
|
+
this.index++;
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
matchPunctuation(text) {
|
|
258
|
+
const token = this.peek();
|
|
259
|
+
if (token.type === 'punctuation' && token.text === text) {
|
|
260
|
+
this.index++;
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
peek() {
|
|
267
|
+
return this.tokens[this.index] ?? this.tokens[this.tokens.length - 1];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
reject(reason) {
|
|
271
|
+
if (!this.ok) return;
|
|
272
|
+
this.ok = false;
|
|
273
|
+
this.reason = reason;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
reasonForTrailingToken() {
|
|
277
|
+
const token = this.peek();
|
|
278
|
+
if (token.type === 'operator') return 'unsupported-action-expression-operator';
|
|
279
|
+
if (token.type === 'punctuation' && (token.text === '.' || token.text === '(' || token.text === '[')) return 'unsupported-action-expression-ref';
|
|
280
|
+
return 'malformed-action-expression';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function canStartSignedNumber(tokens) {
|
|
285
|
+
const previous = tokens[tokens.length - 1];
|
|
286
|
+
if (!previous) return true;
|
|
287
|
+
if (previous.type === 'operator') return true;
|
|
288
|
+
return previous.type === 'punctuation' && previous.text === '(';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function hasNumericOperator(node) {
|
|
292
|
+
if (!node || typeof node !== 'object') return false;
|
|
293
|
+
if (node.kind === 'binary') return NUMERIC_OPERATORS.has(node.op) || hasNumericOperator(node.left) || hasNumericOperator(node.right);
|
|
294
|
+
if (node.kind === 'logical') return hasNumericOperator(node.left) || hasNumericOperator(node.right);
|
|
295
|
+
if (node.kind === 'unary') return hasNumericOperator(node.argument);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isNumericType(value) {
|
|
300
|
+
return NUMERIC_TYPES.has(String(value ?? '').trim().toLowerCase());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function refNode(parts) {
|
|
304
|
+
const [root, ...rest] = parts;
|
|
305
|
+
const scope = root === 'input' || root === 'state' || root === 'patches' ? root : 'local';
|
|
306
|
+
const path = scope === 'local' ? parts : rest;
|
|
307
|
+
return { kind: 'ref', name: parts.join('.'), scope, path };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function fail(reason) {
|
|
311
|
+
return { ok: false, reason };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function compactRecord(record) {
|
|
315
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
316
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseActionValue } from './action-expression.js';
|
|
2
|
+
|
|
1
3
|
const ACTION_BODY_ROWS = new Set(['set', 'insert', 'remove', 'merge', 'callEffect', 'return', 'if', 'let']);
|
|
2
4
|
|
|
3
5
|
export function readActionSyntaxChildren(source, block, options) {
|
|
@@ -56,7 +58,7 @@ function actionSyntaxChild(source, block, options, line, state, parentActionBody
|
|
|
56
58
|
const rowKind = row?.[1] ?? 'unknown';
|
|
57
59
|
const name = actionRowName(rowKind, row?.[2], rowIndex);
|
|
58
60
|
const rest = row?.[2]?.startsWith('@') ? ` ${row[2]}${row[3] ?? ''}` : (row?.[3] ?? '');
|
|
59
|
-
const validation = validateActionRow(rowKind, row?.[2], rest);
|
|
61
|
+
const validation = validateActionRow(rowKind, row?.[2], rest, line.text);
|
|
60
62
|
const recognized = ACTION_BODY_ROWS.has(rowKind) && validation.ok;
|
|
61
63
|
return cleanRecord({
|
|
62
64
|
kind: recognized ? 'actionBodyRow' : 'actionUnknownRow',
|
|
@@ -87,14 +89,34 @@ function actionRowName(rowKind, rawName, rowIndex) {
|
|
|
87
89
|
return rawName && !rawName.startsWith('@') ? rawName : `${rowKind}_${rowIndex}`;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
function validateActionRow(rowKind, rawName, rest) {
|
|
92
|
+
function validateActionRow(rowKind, rawName, rest, header) {
|
|
91
93
|
if (!ACTION_BODY_ROWS.has(rowKind)) return { ok: false, reason: 'unsupported-action-body-row' };
|
|
92
|
-
if (rowKind
|
|
93
|
-
if (
|
|
94
|
-
return { ok: false, reason: '
|
|
94
|
+
if (rowKind === 'if') return validateActionExpressionText(readIfCondition(header));
|
|
95
|
+
if (rowKind === 'set' || rowKind === 'insert' || rowKind === 'merge') {
|
|
96
|
+
if (!readInlineWord('path', rest)) return { ok: false, reason: 'missing-action-path' };
|
|
97
|
+
return validateActionExpressionText(readInlineValue('value', rest), { valueType: readInlineType(rest) });
|
|
98
|
+
}
|
|
99
|
+
if (rowKind === 'remove') {
|
|
100
|
+
return readInlineWord('path', rest) ? { ok: true } : { ok: false, reason: 'missing-action-path' };
|
|
101
|
+
}
|
|
102
|
+
if (rowKind === 'callEffect') {
|
|
103
|
+
const input = readInlineValue('input', rest);
|
|
104
|
+
return input ? validateActionExpressionText(input) : { ok: true };
|
|
105
|
+
}
|
|
106
|
+
if (rowKind === 'return') {
|
|
107
|
+
const value = readReturnValue(rawName, rest);
|
|
108
|
+
return value ? validateActionExpressionText(value) : { ok: true };
|
|
95
109
|
}
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
if (rowKind === 'let') {
|
|
111
|
+
if (!rawName || rawName.startsWith('@') || !/^[A-Za-z_$][\w$-]*$/.test(rawName)) {
|
|
112
|
+
return { ok: false, reason: 'unsupported-action-binding-name' };
|
|
113
|
+
}
|
|
114
|
+
const value = readInlineValue('value', rest);
|
|
115
|
+
const parsed = value ? parseActionValue(value, { valueType: readInlineType(rest) }) : undefined;
|
|
116
|
+
if (parsed?.ok) return { ok: true };
|
|
117
|
+
if (parsed?.reason === 'missing-action-expression-type' || parsed?.reason === 'unsupported-action-expression-type') {
|
|
118
|
+
return { ok: false, reason: parsed.reason };
|
|
119
|
+
}
|
|
98
120
|
return { ok: false, reason: 'unsupported-action-binding-value' };
|
|
99
121
|
}
|
|
100
122
|
return { ok: true };
|
|
@@ -104,10 +126,32 @@ function readInlineValue(label, text) {
|
|
|
104
126
|
return new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim();
|
|
105
127
|
}
|
|
106
128
|
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
129
|
+
function readInlineWord(label, text) {
|
|
130
|
+
return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readInlineType(text) {
|
|
134
|
+
return readInlineWord('type', text) ?? readInlineWord('valueType', text);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readIfCondition(header) {
|
|
138
|
+
const text = header.replace(/^if\b/, '').replace(/\{\s*$/, '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
139
|
+
const explicit = /\bcondition\s+(.+)$/.exec(text);
|
|
140
|
+
return explicit ? explicit[1].trim() : text;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readReturnValue(rawName, rest) {
|
|
144
|
+
if (rawName?.startsWith('@')) return stripIds(`${rawName}${rest ?? ''}`).trim();
|
|
145
|
+
return stripIds(`${rawName ?? ''}${rest ?? ''}`).trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function stripIds(text) {
|
|
149
|
+
return String(text ?? '').replace(/@id\(\s*["'][^"']+["']\s*\)/g, '').trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateActionExpressionText(text, options = {}) {
|
|
153
|
+
const parsed = parseActionValue(text, options);
|
|
154
|
+
return parsed.ok ? { ok: true } : { ok: false, reason: parsed.reason };
|
|
111
155
|
}
|
|
112
156
|
|
|
113
157
|
function readNestedBodyBlocks(kind, source) {
|