@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.
@@ -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
- records.push(parseActionIfBlock(ifHeader[1].trim(), source.slice(open + 1, close), index, state));
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
- return compactRecord({ kind: 'patch', op: rowKind, id: idFrom(rest, `action_body_${rowKind}_${name}`), name, path: readInlineWord('path', rest), value: readInlineActionValue('value', rest) });
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
- return compactRecord({ kind: 'patch', op: 'remove', id: idFrom(rest, `action_body_remove_${name}`), name, path: readInlineWord('path', rest) });
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
- return compactRecord({ kind: 'callEffect', id: idFrom(rest, `action_body_callEffect_${name}`), name, capability: readInlineWord('capability', rest) ?? readInlineWord('effect', rest) ?? name, input: readInlineActionValue('input', rest) });
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 value = readInlineActionBindingValue('value', rest);
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.trim() : `${rawName ?? ''}${rest ?? ''}`.trim();
92
- return compactRecord({ kind: 'return', id: idFrom(rest, `action_body_return_${index}`), value: valueText ? readActionValue(valueText) : undefined });
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 = new RegExp('(?:^|\\s)' + label + '\\s+(.+?)(?=\\s+[A-Za-z_$][\\w$-]*\\s+|$)').exec(text)?.[1]?.trim();
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 isSupportedBindingValue(value) {
183
- if (!value) return false;
184
- if (Object.prototype.hasOwnProperty.call(value, 'value')) return true;
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 !== 'let') return { ok: true };
93
- if (!rawName || rawName.startsWith('@') || !/^[A-Za-z_$][\w$-]*$/.test(rawName)) {
94
- return { ok: false, reason: 'unsupported-action-binding-name' };
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
- const value = readInlineValue('value', rest);
97
- if (!value || !isSupportedBindingValue(value)) {
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 isSupportedBindingValue(value) {
108
- if (/^["'][^"']*["']$/.test(value)) return true;
109
- if (/^(true|false|null|-?\d+(?:\.\d+)?)$/.test(value)) return true;
110
- return /^[A-Za-z_$][\w$-]*(?:\.[A-Za-z_$][\w$-]*)*$/.test(value);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.44",
3
+ "version": "0.3.46",
4
4
  "description": "Parser for the first Frontier Lang .frontier syntax slice.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",