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