mrmd-js 2.0.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.
Files changed (52) hide show
  1. package/README.md +842 -0
  2. package/dist/index.cjs +7613 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.js +7530 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/mrmd-js.iife.js +7618 -0
  7. package/dist/mrmd-js.iife.js.map +1 -0
  8. package/package.json +47 -0
  9. package/src/analysis/format.js +371 -0
  10. package/src/analysis/index.js +18 -0
  11. package/src/analysis/is-complete.js +394 -0
  12. package/src/constants.js +44 -0
  13. package/src/execute/css.js +205 -0
  14. package/src/execute/html.js +162 -0
  15. package/src/execute/index.js +41 -0
  16. package/src/execute/interface.js +144 -0
  17. package/src/execute/javascript.js +197 -0
  18. package/src/execute/registry.js +245 -0
  19. package/src/index.js +136 -0
  20. package/src/lsp/complete.js +353 -0
  21. package/src/lsp/format.js +310 -0
  22. package/src/lsp/hover.js +126 -0
  23. package/src/lsp/index.js +55 -0
  24. package/src/lsp/inspect.js +466 -0
  25. package/src/lsp/parse.js +455 -0
  26. package/src/lsp/variables.js +283 -0
  27. package/src/runtime.js +518 -0
  28. package/src/session/console-capture.js +181 -0
  29. package/src/session/context/iframe.js +407 -0
  30. package/src/session/context/index.js +12 -0
  31. package/src/session/context/interface.js +38 -0
  32. package/src/session/context/main.js +357 -0
  33. package/src/session/index.js +16 -0
  34. package/src/session/manager.js +327 -0
  35. package/src/session/session.js +678 -0
  36. package/src/transform/async.js +133 -0
  37. package/src/transform/extract.js +251 -0
  38. package/src/transform/index.js +10 -0
  39. package/src/transform/persistence.js +176 -0
  40. package/src/types/analysis.js +24 -0
  41. package/src/types/capabilities.js +44 -0
  42. package/src/types/completion.js +47 -0
  43. package/src/types/execution.js +62 -0
  44. package/src/types/index.js +16 -0
  45. package/src/types/inspection.js +39 -0
  46. package/src/types/session.js +32 -0
  47. package/src/types/streaming.js +74 -0
  48. package/src/types/variables.js +54 -0
  49. package/src/utils/ansi-renderer.js +301 -0
  50. package/src/utils/css-applicator.js +149 -0
  51. package/src/utils/html-renderer.js +355 -0
  52. package/src/utils/index.js +25 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Hover Information
3
+ *
4
+ * Provides hover information (type and value preview) for symbols
5
+ * by introspecting live values in the execution context.
6
+ *
7
+ * @module lsp/hover
8
+ */
9
+
10
+ import { parseIdentifierAtPosition, splitObjectPath } from './parse.js';
11
+ import { formatValueShort, getTypeName, getFunctionSignature } from './format.js';
12
+
13
+ /**
14
+ * @typedef {import('../session/context/interface.js').ExecutionContext} ExecutionContext
15
+ * @typedef {import('../types/inspection.js').HoverResult} HoverResult
16
+ */
17
+
18
+ /**
19
+ * Get hover information at cursor position
20
+ *
21
+ * @param {string} code - The code being edited
22
+ * @param {number} cursor - Cursor position (0-indexed)
23
+ * @param {ExecutionContext} context - Execution context for live values
24
+ * @returns {HoverResult}
25
+ */
26
+ export function getHoverInfo(code, cursor, context) {
27
+ // Find identifier at cursor
28
+ const identifier = parseIdentifierAtPosition(code, cursor);
29
+
30
+ if (!identifier) {
31
+ return { found: false };
32
+ }
33
+
34
+ // Resolve the value
35
+ const value = resolveValue(identifier.full, context);
36
+
37
+ // Check if it exists
38
+ const exists = value !== undefined || hasVariable(identifier.full, context);
39
+
40
+ if (!exists) {
41
+ return { found: false };
42
+ }
43
+
44
+ /** @type {HoverResult} */
45
+ const result = {
46
+ found: true,
47
+ name: identifier.full,
48
+ type: getTypeName(value),
49
+ };
50
+
51
+ // Add signature for functions
52
+ if (typeof value === 'function') {
53
+ result.signature = getFunctionSignature(value);
54
+ } else {
55
+ // Add value preview for non-functions
56
+ result.value = formatValueShort(value, 100);
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ /**
63
+ * Resolve a value from an object path in the context
64
+ * @param {string} path
65
+ * @param {ExecutionContext} context
66
+ * @returns {*}
67
+ */
68
+ function resolveValue(path, context) {
69
+ const parts = splitObjectPath(path);
70
+ if (parts.length === 0) return undefined;
71
+
72
+ // Start with user variables or global
73
+ let value = context.getVariable(parts[0]);
74
+
75
+ if (value === undefined) {
76
+ // Try global
77
+ const global = context.getGlobal();
78
+ if (global && parts[0] in global) {
79
+ // @ts-ignore
80
+ value = global[parts[0]];
81
+ }
82
+ }
83
+
84
+ if (value === undefined) return undefined;
85
+
86
+ // Navigate path
87
+ for (let i = 1; i < parts.length; i++) {
88
+ if (value === null || value === undefined) return undefined;
89
+
90
+ try {
91
+ if (value instanceof Map) {
92
+ value = value.get(parts[i]);
93
+ } else {
94
+ // @ts-ignore
95
+ value = value[parts[i]];
96
+ }
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ }
101
+
102
+ return value;
103
+ }
104
+
105
+ /**
106
+ * Check if a variable exists in context
107
+ * @param {string} path
108
+ * @param {ExecutionContext} context
109
+ * @returns {boolean}
110
+ */
111
+ function hasVariable(path, context) {
112
+ const parts = splitObjectPath(path);
113
+ if (parts.length === 0) return false;
114
+
115
+ if (context.hasVariable(parts[0])) {
116
+ return true;
117
+ }
118
+
119
+ // Check global
120
+ const global = context.getGlobal();
121
+ if (global && parts[0] in global) {
122
+ return true;
123
+ }
124
+
125
+ return false;
126
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * LSP Features
3
+ *
4
+ * Language Server Protocol-like features for JavaScript runtime.
5
+ * These provide completions, hover, inspection, and variable listing
6
+ * based on live runtime values.
7
+ *
8
+ * @module lsp
9
+ */
10
+
11
+ // Parsing utilities
12
+ export {
13
+ parseIdentifierAtPosition,
14
+ parseCompletionContext,
15
+ getStringOrCommentContext,
16
+ getWordAtCursor,
17
+ splitObjectPath,
18
+ isIdentifierStart,
19
+ isIdentifierPart,
20
+ isKeyword,
21
+ getKeywords,
22
+ getCommonGlobals,
23
+ } from './parse.js';
24
+
25
+ // Value formatting
26
+ export {
27
+ formatValue,
28
+ formatValueShort,
29
+ getTypeName,
30
+ getCompletionKind,
31
+ isExpandable,
32
+ getFunctionSignature,
33
+ getFunctionSource,
34
+ getSizeDescription,
35
+ } from './format.js';
36
+
37
+ // Completions
38
+ export { getCompletions } from './complete.js';
39
+
40
+ // Hover
41
+ export { getHoverInfo } from './hover.js';
42
+
43
+ // Inspection
44
+ export { getInspectInfo, inspectPath } from './inspect.js';
45
+
46
+ // Variables
47
+ export {
48
+ listVariables,
49
+ getVariableDetail,
50
+ expandVariable,
51
+ formatVariableInfo,
52
+ getChildren,
53
+ getMethods,
54
+ getAttributes,
55
+ } from './variables.js';
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Symbol Inspection
3
+ *
4
+ * Provides detailed inspection information for symbols including
5
+ * signature, documentation, source code, and children.
6
+ *
7
+ * @module lsp/inspect
8
+ */
9
+
10
+ import { parseIdentifierAtPosition, splitObjectPath } from './parse.js';
11
+ import {
12
+ formatValue,
13
+ formatValueShort,
14
+ getTypeName,
15
+ getCompletionKind,
16
+ getFunctionSignature,
17
+ getFunctionSource,
18
+ isExpandable,
19
+ getSizeDescription,
20
+ } from './format.js';
21
+
22
+ /**
23
+ * @typedef {import('../session/context/interface.js').ExecutionContext} ExecutionContext
24
+ * @typedef {import('../types/inspection.js').InspectOptions} InspectOptions
25
+ * @typedef {import('../types/inspection.js').InspectResult} InspectResult
26
+ * @typedef {import('../types/variables.js').VariableInfo} VariableInfo
27
+ */
28
+
29
+ /**
30
+ * Get detailed inspection information at cursor position
31
+ *
32
+ * @param {string} code - The code being edited
33
+ * @param {number} cursor - Cursor position (0-indexed)
34
+ * @param {ExecutionContext} context - Execution context for live values
35
+ * @param {InspectOptions} [options]
36
+ * @returns {InspectResult}
37
+ */
38
+ export function getInspectInfo(code, cursor, context, options = {}) {
39
+ const detail = options.detail ?? 0;
40
+
41
+ // Find identifier at cursor
42
+ const identifier = parseIdentifierAtPosition(code, cursor);
43
+
44
+ if (!identifier) {
45
+ return { found: false, source: 'runtime' };
46
+ }
47
+
48
+ // Resolve the value
49
+ const value = resolveValue(identifier.full, context);
50
+
51
+ // Check if it exists
52
+ const exists = value !== undefined || hasVariable(identifier.full, context);
53
+
54
+ if (!exists) {
55
+ return { found: false, source: 'runtime' };
56
+ }
57
+
58
+ /** @type {InspectResult} */
59
+ const result = {
60
+ found: true,
61
+ source: 'runtime',
62
+ name: identifier.name,
63
+ kind: getInspectKind(value),
64
+ type: getTypeName(value),
65
+ value: formatValueShort(value, 200),
66
+ };
67
+
68
+ // Add function-specific info
69
+ if (typeof value === 'function') {
70
+ result.signature = getFunctionSignature(value);
71
+
72
+ // Detail level 1: add docstring
73
+ if (detail >= 1) {
74
+ result.docstring = getDocstring(value);
75
+ }
76
+
77
+ // Detail level 2: add source code
78
+ if (detail >= 2) {
79
+ result.sourceCode = getFunctionSource(value);
80
+ }
81
+ }
82
+
83
+ // Add children for expandable values
84
+ if (detail >= 1 && isExpandable(value)) {
85
+ result.children = getChildren(value);
86
+ }
87
+
88
+ return result;
89
+ }
90
+
91
+ /**
92
+ * Inspect a specific object path
93
+ *
94
+ * @param {string} path - Object path to inspect (e.g., "obj.prop")
95
+ * @param {ExecutionContext} context
96
+ * @param {InspectOptions} [options]
97
+ * @returns {InspectResult}
98
+ */
99
+ export function inspectPath(path, context, options = {}) {
100
+ const detail = options.detail ?? 0;
101
+
102
+ const value = resolveValue(path, context);
103
+ const exists = value !== undefined || hasVariable(path, context);
104
+
105
+ if (!exists) {
106
+ return { found: false, source: 'runtime' };
107
+ }
108
+
109
+ const parts = splitObjectPath(path);
110
+ const name = parts[parts.length - 1] || path;
111
+
112
+ /** @type {InspectResult} */
113
+ const result = {
114
+ found: true,
115
+ source: 'runtime',
116
+ name,
117
+ kind: getInspectKind(value),
118
+ type: getTypeName(value),
119
+ value: formatValueShort(value, 200),
120
+ };
121
+
122
+ if (typeof value === 'function') {
123
+ result.signature = getFunctionSignature(value);
124
+
125
+ if (detail >= 1) {
126
+ result.docstring = getDocstring(value);
127
+ }
128
+
129
+ if (detail >= 2) {
130
+ result.sourceCode = getFunctionSource(value);
131
+ }
132
+ }
133
+
134
+ if (detail >= 1 && isExpandable(value)) {
135
+ result.children = getChildren(value);
136
+ }
137
+
138
+ return result;
139
+ }
140
+
141
+ /**
142
+ * Get kind string for inspection
143
+ * @param {*} value
144
+ * @returns {string}
145
+ */
146
+ function getInspectKind(value) {
147
+ if (value === null) return 'null';
148
+ if (value === undefined) return 'undefined';
149
+
150
+ if (typeof value === 'function') {
151
+ const str = value.toString();
152
+ if (str.startsWith('class ')) return 'class';
153
+ if (/^(async\s+)?function\s*\*/.test(str)) return 'generator';
154
+ if (str.includes('=>')) return 'arrow-function';
155
+ return 'function';
156
+ }
157
+
158
+ if (Array.isArray(value)) return 'array';
159
+ if (value instanceof Map) return 'map';
160
+ if (value instanceof Set) return 'set';
161
+ if (value instanceof Date) return 'date';
162
+ if (value instanceof RegExp) return 'regexp';
163
+ if (value instanceof Error) return 'error';
164
+ if (value instanceof Promise) return 'promise';
165
+
166
+ const type = typeof value;
167
+ if (type === 'object') return 'object';
168
+
169
+ return type;
170
+ }
171
+
172
+ /**
173
+ * Get docstring for a function (if available)
174
+ * @param {Function} fn
175
+ * @returns {string | undefined}
176
+ */
177
+ function getDocstring(fn) {
178
+ if (typeof fn !== 'function') return undefined;
179
+
180
+ try {
181
+ const source = fn.toString();
182
+
183
+ // Try to find JSDoc-style comments
184
+ // Look for /** ... */ before function declaration
185
+ // This won't work for most runtime functions, but worth trying
186
+ const jsdocMatch = source.match(/\/\*\*([\s\S]*?)\*\//);
187
+ if (jsdocMatch) {
188
+ return jsdocMatch[1]
189
+ .split('\n')
190
+ .map(line => line.replace(/^\s*\*\s?/, '').trim())
191
+ .filter(line => line && !line.startsWith('@'))
192
+ .join('\n')
193
+ .trim();
194
+ }
195
+
196
+ // Check for built-in documentation (MDN-style)
197
+ const builtinDocs = getBuiltinDocumentation(fn);
198
+ if (builtinDocs) {
199
+ return builtinDocs;
200
+ }
201
+
202
+ return undefined;
203
+ } catch {
204
+ return undefined;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Get documentation for built-in functions
210
+ * @param {Function} fn
211
+ * @returns {string | undefined}
212
+ */
213
+ function getBuiltinDocumentation(fn) {
214
+ // Map of common built-in functions to their descriptions
215
+ const docs = {
216
+ // Array methods
217
+ 'push': 'Adds elements to the end of an array and returns the new length.',
218
+ 'pop': 'Removes the last element from an array and returns it.',
219
+ 'shift': 'Removes the first element from an array and returns it.',
220
+ 'unshift': 'Adds elements to the beginning of an array and returns the new length.',
221
+ 'slice': 'Returns a shallow copy of a portion of an array.',
222
+ 'splice': 'Changes the contents of an array by removing or replacing elements.',
223
+ 'map': 'Creates a new array with the results of calling a function on every element.',
224
+ 'filter': 'Creates a new array with all elements that pass a test.',
225
+ 'reduce': 'Executes a reducer function on each element, resulting in a single value.',
226
+ 'forEach': 'Executes a function once for each array element.',
227
+ 'find': 'Returns the first element that satisfies a testing function.',
228
+ 'findIndex': 'Returns the index of the first element that satisfies a testing function.',
229
+ 'includes': 'Determines whether an array includes a certain value.',
230
+ 'indexOf': 'Returns the first index at which a given element can be found.',
231
+ 'join': 'Joins all elements of an array into a string.',
232
+ 'sort': 'Sorts the elements of an array in place and returns the array.',
233
+ 'reverse': 'Reverses the elements of an array in place.',
234
+ 'concat': 'Merges two or more arrays into a new array.',
235
+ 'flat': 'Creates a new array with all sub-array elements concatenated.',
236
+ 'flatMap': 'Maps each element then flattens the result into a new array.',
237
+
238
+ // String methods
239
+ 'charAt': 'Returns the character at a specified index.',
240
+ 'charCodeAt': 'Returns the Unicode value of the character at an index.',
241
+ 'split': 'Splits a string into an array of substrings.',
242
+ 'substring': 'Returns a portion of the string between two indices.',
243
+ 'substr': 'Returns a portion of the string starting from an index.',
244
+ 'toLowerCase': 'Returns the string converted to lowercase.',
245
+ 'toUpperCase': 'Returns the string converted to uppercase.',
246
+ 'trim': 'Removes whitespace from both ends of a string.',
247
+ 'trimStart': 'Removes whitespace from the beginning of a string.',
248
+ 'trimEnd': 'Removes whitespace from the end of a string.',
249
+ 'replace': 'Returns a new string with some or all matches replaced.',
250
+ 'replaceAll': 'Returns a new string with all matches replaced.',
251
+ 'match': 'Retrieves the result of matching a string against a regex.',
252
+ 'search': 'Searches for a match between a regex and the string.',
253
+ 'startsWith': 'Determines whether a string begins with specified characters.',
254
+ 'endsWith': 'Determines whether a string ends with specified characters.',
255
+ 'padStart': 'Pads the string with another string until it reaches the given length.',
256
+ 'padEnd': 'Pads the string with another string at the end.',
257
+ 'repeat': 'Returns a new string with copies of the original string.',
258
+
259
+ // Object methods
260
+ 'hasOwnProperty': 'Returns a boolean indicating whether the object has the property.',
261
+ 'toString': 'Returns a string representation of the object.',
262
+ 'valueOf': 'Returns the primitive value of the object.',
263
+
264
+ // Global functions
265
+ 'parseInt': 'Parses a string argument and returns an integer.',
266
+ 'parseFloat': 'Parses a string argument and returns a floating point number.',
267
+ 'isNaN': 'Determines whether a value is NaN.',
268
+ 'isFinite': 'Determines whether a value is a finite number.',
269
+ 'encodeURI': 'Encodes a URI by replacing certain characters.',
270
+ 'decodeURI': 'Decodes a URI previously created by encodeURI.',
271
+ 'encodeURIComponent': 'Encodes a URI component by replacing certain characters.',
272
+ 'decodeURIComponent': 'Decodes a URI component.',
273
+
274
+ // JSON
275
+ 'parse': 'Parses a JSON string and returns the JavaScript value.',
276
+ 'stringify': 'Converts a JavaScript value to a JSON string.',
277
+
278
+ // Math
279
+ 'abs': 'Returns the absolute value of a number.',
280
+ 'ceil': 'Rounds a number up to the next largest integer.',
281
+ 'floor': 'Rounds a number down to the largest integer.',
282
+ 'round': 'Rounds a number to the nearest integer.',
283
+ 'max': 'Returns the largest of zero or more numbers.',
284
+ 'min': 'Returns the smallest of zero or more numbers.',
285
+ 'pow': 'Returns the base raised to the exponent power.',
286
+ 'sqrt': 'Returns the square root of a number.',
287
+ 'random': 'Returns a random number between 0 and 1.',
288
+ 'sin': 'Returns the sine of a number.',
289
+ 'cos': 'Returns the cosine of a number.',
290
+ 'tan': 'Returns the tangent of a number.',
291
+ 'log': 'Returns the natural logarithm of a number.',
292
+ 'exp': 'Returns e raised to the power of a number.',
293
+
294
+ // Console
295
+ 'log': 'Outputs a message to the console.',
296
+ 'error': 'Outputs an error message to the console.',
297
+ 'warn': 'Outputs a warning message to the console.',
298
+ 'info': 'Outputs an informational message to the console.',
299
+ 'debug': 'Outputs a debug message to the console.',
300
+ 'table': 'Displays tabular data as a table.',
301
+ 'clear': 'Clears the console.',
302
+ 'group': 'Creates a new inline group in the console.',
303
+ 'groupEnd': 'Exits the current inline group in the console.',
304
+ 'time': 'Starts a timer with a specified label.',
305
+ 'timeEnd': 'Stops a timer and logs the elapsed time.',
306
+ };
307
+
308
+ const name = fn.name;
309
+ return docs[name];
310
+ }
311
+
312
+ /**
313
+ * Get children of an expandable value as VariableInfo[]
314
+ * @param {*} value
315
+ * @param {number} [maxChildren=100]
316
+ * @returns {VariableInfo[]}
317
+ */
318
+ function getChildren(value, maxChildren = 100) {
319
+ if (value === null || value === undefined) return [];
320
+
321
+ /** @type {VariableInfo[]} */
322
+ const children = [];
323
+
324
+ if (Array.isArray(value)) {
325
+ const items = value.slice(0, maxChildren);
326
+ for (let i = 0; i < items.length; i++) {
327
+ children.push(formatVariableInfo(String(i), items[i]));
328
+ }
329
+ } else if (value instanceof Map) {
330
+ let count = 0;
331
+ for (const [k, v] of value) {
332
+ if (count >= maxChildren) break;
333
+ children.push(formatVariableInfo(String(k), v));
334
+ count++;
335
+ }
336
+ } else if (value instanceof Set) {
337
+ let count = 0;
338
+ for (const v of value) {
339
+ if (count >= maxChildren) break;
340
+ children.push(formatVariableInfo(String(count), v));
341
+ count++;
342
+ }
343
+ } else if (typeof value === 'object') {
344
+ const keys = Object.keys(value).slice(0, maxChildren);
345
+ for (const key of keys) {
346
+ try {
347
+ children.push(formatVariableInfo(key, value[key]));
348
+ } catch {
349
+ children.push({
350
+ name: key,
351
+ type: 'unknown',
352
+ value: '(inaccessible)',
353
+ expandable: false,
354
+ });
355
+ }
356
+ }
357
+ }
358
+
359
+ return children;
360
+ }
361
+
362
+ /**
363
+ * Format a variable for display
364
+ * @param {string} name
365
+ * @param {*} value
366
+ * @returns {VariableInfo}
367
+ */
368
+ function formatVariableInfo(name, value) {
369
+ /** @type {VariableInfo} */
370
+ const info = {
371
+ name,
372
+ type: getTypeName(value),
373
+ value: formatValueShort(value, 100),
374
+ expandable: isExpandable(value),
375
+ };
376
+
377
+ // Add size info
378
+ const size = getSizeDescription(value);
379
+ if (size) {
380
+ info.size = size;
381
+ }
382
+
383
+ // Add length for arrays/strings
384
+ if (Array.isArray(value)) {
385
+ info.length = value.length;
386
+ } else if (typeof value === 'string') {
387
+ info.length = value.length;
388
+ } else if (value instanceof Map || value instanceof Set) {
389
+ info.length = value.size;
390
+ }
391
+
392
+ // Add keys preview for objects
393
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
394
+ if (!(value instanceof Map) && !(value instanceof Set)) {
395
+ info.keys = Object.keys(value).slice(0, 10);
396
+ }
397
+ }
398
+
399
+ return info;
400
+ }
401
+
402
+ /**
403
+ * Resolve a value from an object path in the context
404
+ * @param {string} path
405
+ * @param {ExecutionContext} context
406
+ * @returns {*}
407
+ */
408
+ function resolveValue(path, context) {
409
+ const parts = splitObjectPath(path);
410
+ if (parts.length === 0) return undefined;
411
+
412
+ // Start with user variables or global
413
+ let value = context.getVariable(parts[0]);
414
+
415
+ if (value === undefined) {
416
+ // Try global
417
+ const global = context.getGlobal();
418
+ if (global && parts[0] in global) {
419
+ // @ts-ignore
420
+ value = global[parts[0]];
421
+ }
422
+ }
423
+
424
+ if (value === undefined) return undefined;
425
+
426
+ // Navigate path
427
+ for (let i = 1; i < parts.length; i++) {
428
+ if (value === null || value === undefined) return undefined;
429
+
430
+ try {
431
+ if (value instanceof Map) {
432
+ value = value.get(parts[i]);
433
+ } else {
434
+ // @ts-ignore
435
+ value = value[parts[i]];
436
+ }
437
+ } catch {
438
+ return undefined;
439
+ }
440
+ }
441
+
442
+ return value;
443
+ }
444
+
445
+ /**
446
+ * Check if a variable exists in context
447
+ * @param {string} path
448
+ * @param {ExecutionContext} context
449
+ * @returns {boolean}
450
+ */
451
+ function hasVariable(path, context) {
452
+ const parts = splitObjectPath(path);
453
+ if (parts.length === 0) return false;
454
+
455
+ if (context.hasVariable(parts[0])) {
456
+ return true;
457
+ }
458
+
459
+ // Check global
460
+ const global = context.getGlobal();
461
+ if (global && parts[0] in global) {
462
+ return true;
463
+ }
464
+
465
+ return false;
466
+ }