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,678 @@
1
+ /**
2
+ * Session Class
3
+ *
4
+ * A session is an isolated execution context that persists variables
5
+ * across executions. It wraps an ExecutionContext and provides the
6
+ * full MRP session API.
7
+ *
8
+ * @module session/session
9
+ */
10
+
11
+ import { IframeContext } from './context/iframe.js';
12
+ import { MainContext } from './context/main.js';
13
+ import { extractDeclaredVariables } from '../transform/extract.js';
14
+ import { JavaScriptExecutor } from '../execute/javascript.js';
15
+
16
+ // LSP Features
17
+ import { getCompletions } from '../lsp/complete.js';
18
+ import { getHoverInfo } from '../lsp/hover.js';
19
+ import { getInspectInfo } from '../lsp/inspect.js';
20
+ import {
21
+ listVariables as lspListVariables,
22
+ getVariableDetail as lspGetVariableDetail,
23
+ } from '../lsp/variables.js';
24
+
25
+ // Analysis Features
26
+ import { isComplete as analysisIsComplete } from '../analysis/is-complete.js';
27
+ import { formatCode } from '../analysis/format.js';
28
+
29
+ /**
30
+ * @typedef {import('../execute/registry.js').ExecutorRegistry} ExecutorRegistry
31
+ * @typedef {import('../execute/interface.js').Executor} Executor
32
+ */
33
+
34
+ /**
35
+ * @typedef {import('./context/interface.js').ExecutionContext} ExecutionContext
36
+ * @typedef {import('./context/interface.js').RawExecutionResult} RawExecutionResult
37
+ * @typedef {import('../types/session.js').SessionInfo} SessionInfo
38
+ * @typedef {import('../types/session.js').CreateSessionOptions} CreateSessionOptions
39
+ * @typedef {import('../types/session.js').IsolationMode} IsolationMode
40
+ * @typedef {import('../types/execution.js').ExecuteOptions} ExecuteOptions
41
+ * @typedef {import('../types/execution.js').ExecutionResult} ExecutionResult
42
+ * @typedef {import('../types/execution.js').ExecutionError} ExecutionError
43
+ * @typedef {import('../types/execution.js').DisplayData} DisplayData
44
+ * @typedef {import('../types/streaming.js').StreamEvent} StreamEvent
45
+ * @typedef {import('../types/completion.js').CompleteOptions} CompleteOptions
46
+ * @typedef {import('../types/completion.js').CompletionResult} CompletionResult
47
+ * @typedef {import('../types/inspection.js').InspectOptions} InspectOptions
48
+ * @typedef {import('../types/inspection.js').InspectResult} InspectResult
49
+ * @typedef {import('../types/inspection.js').HoverResult} HoverResult
50
+ * @typedef {import('../types/variables.js').VariableFilter} VariableFilter
51
+ * @typedef {import('../types/variables.js').VariableInfo} VariableInfo
52
+ * @typedef {import('../types/variables.js').VariableDetailOptions} VariableDetailOptions
53
+ * @typedef {import('../types/variables.js').VariableDetail} VariableDetail
54
+ * @typedef {import('../types/analysis.js').IsCompleteResult} IsCompleteResult
55
+ * @typedef {import('../types/analysis.js').FormatResult} FormatResult
56
+ */
57
+
58
+ /**
59
+ * Generate a unique execution ID
60
+ * @returns {string}
61
+ */
62
+ function generateExecId() {
63
+ return `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
64
+ }
65
+
66
+ /**
67
+ * Session class - represents an isolated execution context
68
+ */
69
+ export class Session {
70
+ /** @type {string} */
71
+ #id;
72
+
73
+ /** @type {string} */
74
+ #language;
75
+
76
+ /** @type {IsolationMode} */
77
+ #isolation;
78
+
79
+ /** @type {Date} */
80
+ #created;
81
+
82
+ /** @type {Date} */
83
+ #lastActivity;
84
+
85
+ /** @type {number} */
86
+ #executionCount = 0;
87
+
88
+ /** @type {ExecutionContext} */
89
+ #context;
90
+
91
+ /** @type {ExecutorRegistry | null} */
92
+ #executorRegistry = null;
93
+
94
+ /** @type {JavaScriptExecutor} */
95
+ #defaultJsExecutor;
96
+
97
+ /** @type {Map<string, AbortController>} */
98
+ #runningExecutions = new Map();
99
+
100
+ /** @type {Map<string, (text: string) => void>} */
101
+ #pendingInputs = new Map();
102
+
103
+ /**
104
+ * @param {string} id - Session ID
105
+ * @param {CreateSessionOptions & { executorRegistry?: ExecutorRegistry }} [options]
106
+ */
107
+ constructor(id, options = {}) {
108
+ this.#id = id;
109
+ this.#language = options.language || 'javascript';
110
+ this.#isolation = options.isolation || 'iframe';
111
+ this.#created = new Date();
112
+ this.#lastActivity = new Date();
113
+
114
+ // Store executor registry if provided
115
+ this.#executorRegistry = options.executorRegistry || null;
116
+
117
+ // Create default JS executor for fallback
118
+ this.#defaultJsExecutor = new JavaScriptExecutor();
119
+
120
+ // Create the appropriate context
121
+ this.#context = this.#createContext(options);
122
+ }
123
+
124
+ /**
125
+ * Create the execution context based on isolation mode
126
+ * @param {CreateSessionOptions} options
127
+ * @returns {ExecutionContext}
128
+ */
129
+ #createContext(options) {
130
+ switch (this.#isolation) {
131
+ case 'none':
132
+ return new MainContext({
133
+ utilities: options.utilities,
134
+ });
135
+
136
+ case 'iframe':
137
+ default:
138
+ return new IframeContext({
139
+ visible: false,
140
+ allowMainAccess: options.allowMainAccess ?? false,
141
+ utilities: options.utilities,
142
+ });
143
+ }
144
+ }
145
+
146
+ // ============================================================================
147
+ // Properties
148
+ // ============================================================================
149
+
150
+ /** @returns {string} */
151
+ get id() {
152
+ return this.#id;
153
+ }
154
+
155
+ /** @returns {string} */
156
+ get language() {
157
+ return this.#language;
158
+ }
159
+
160
+ /** @returns {IsolationMode} */
161
+ get isolation() {
162
+ return this.#isolation;
163
+ }
164
+
165
+ /** @returns {Date} */
166
+ get created() {
167
+ return this.#created;
168
+ }
169
+
170
+ /** @returns {Date} */
171
+ get lastActivity() {
172
+ return this.#lastActivity;
173
+ }
174
+
175
+ /** @returns {number} */
176
+ get executionCount() {
177
+ return this.#executionCount;
178
+ }
179
+
180
+ // ============================================================================
181
+ // Execution
182
+ // ============================================================================
183
+
184
+ /**
185
+ * Execute code and return result
186
+ * @param {string} code - Code to execute
187
+ * @param {ExecuteOptions} [options]
188
+ * @returns {Promise<ExecutionResult>}
189
+ */
190
+ async execute(code, options = {}) {
191
+ const execId = options.execId || generateExecId();
192
+ const language = options.language || this.#language;
193
+
194
+ // Update activity
195
+ this.#lastActivity = new Date();
196
+
197
+ // Track execution
198
+ const abortController = new AbortController();
199
+ this.#runningExecutions.set(execId, abortController);
200
+
201
+ try {
202
+ // Get the executor for this language
203
+ const executor = this.#getExecutor(language);
204
+
205
+ // Execute using the executor
206
+ const result = await executor.execute(code, this.#context, {
207
+ ...options,
208
+ execId,
209
+ language,
210
+ });
211
+
212
+ // Update execution count
213
+ if (options.storeHistory !== false) {
214
+ this.#executionCount++;
215
+ }
216
+
217
+ // Update result with session's execution count
218
+ result.executionCount = this.#executionCount;
219
+
220
+ return result;
221
+ } finally {
222
+ this.#runningExecutions.delete(execId);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Get the executor for a language
228
+ * @param {string} language
229
+ * @returns {Executor}
230
+ */
231
+ #getExecutor(language) {
232
+ // Try registry first
233
+ if (this.#executorRegistry) {
234
+ const executor = this.#executorRegistry.get(language);
235
+ if (executor) return executor;
236
+ }
237
+
238
+ // Fall back to default JS executor for JavaScript
239
+ const lang = language.toLowerCase();
240
+ if (['javascript', 'js', 'ecmascript', 'es'].includes(lang)) {
241
+ return this.#defaultJsExecutor;
242
+ }
243
+
244
+ // Return a no-op executor that reports unsupported language
245
+ return {
246
+ languages: [],
247
+ async execute(code, context, options) {
248
+ return {
249
+ success: false,
250
+ stdout: '',
251
+ stderr: `No executor registered for language: ${language}`,
252
+ error: {
253
+ type: 'ExecutorError',
254
+ message: `No executor registered for language: ${language}. Register an ExecutorRegistry with HTML/CSS executors to support this language.`,
255
+ },
256
+ displayData: [],
257
+ assets: [],
258
+ executionCount: 0,
259
+ duration: 0,
260
+ };
261
+ },
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Execute code with streaming output
267
+ * @param {string} code - Code to execute
268
+ * @param {ExecuteOptions} [options]
269
+ * @returns {AsyncGenerator<StreamEvent>}
270
+ */
271
+ async *executeStream(code, options = {}) {
272
+ const execId = options.execId || generateExecId();
273
+ const language = options.language || this.#language;
274
+
275
+ // Update activity
276
+ this.#lastActivity = new Date();
277
+
278
+ // Track execution
279
+ const abortController = new AbortController();
280
+ this.#runningExecutions.set(execId, abortController);
281
+
282
+ // Event queue for stdin_request events
283
+ /** @type {Array<import('../types/streaming.js').StdinRequestEvent>} */
284
+ const stdinEventQueue = [];
285
+ let stdinEventResolve = null;
286
+
287
+ // Set up stdin handler on context to capture input requests
288
+ // and yield stdin_request events
289
+ const previousHandler = this.#context.getStdinHandler?.();
290
+
291
+ if (this.#context.setStdinHandler) {
292
+ this.#context.setStdinHandler((request) => {
293
+ return new Promise((resolve, reject) => {
294
+ // Store the resolver for when sendInput is called
295
+ this.#pendingInputs.set(request.execId, resolve);
296
+
297
+ // Queue the stdin_request event to be yielded
298
+ stdinEventQueue.push({
299
+ type: 'stdin_request',
300
+ prompt: request.prompt,
301
+ password: request.password,
302
+ execId: request.execId,
303
+ });
304
+
305
+ // Wake up the event loop if waiting
306
+ if (stdinEventResolve) {
307
+ stdinEventResolve();
308
+ stdinEventResolve = null;
309
+ }
310
+
311
+ // Set up abort handling
312
+ abortController.signal.addEventListener('abort', () => {
313
+ this.#pendingInputs.delete(request.execId);
314
+ reject(new Error('Execution aborted'));
315
+ }, { once: true });
316
+ });
317
+ });
318
+ }
319
+
320
+ try {
321
+ // Get the executor for this language
322
+ const executor = this.#getExecutor(language);
323
+
324
+ // Use executor's streaming if available
325
+ if (executor.executeStream) {
326
+ let executionCount = this.#executionCount;
327
+
328
+ for await (const event of executor.executeStream(code, this.#context, {
329
+ ...options,
330
+ execId,
331
+ language,
332
+ })) {
333
+ // Yield any pending stdin_request events first
334
+ while (stdinEventQueue.length > 0) {
335
+ yield /** @type {import('../types/streaming.js').StdinRequestEvent} */ (stdinEventQueue.shift());
336
+ }
337
+
338
+ // Update execution count on result event
339
+ if (event.type === 'result' && options.storeHistory !== false) {
340
+ this.#executionCount++;
341
+ event.result.executionCount = this.#executionCount;
342
+ }
343
+ yield event;
344
+ }
345
+ } else {
346
+ // Fall back to wrapping execute()
347
+ const timestamp = new Date().toISOString();
348
+
349
+ yield /** @type {import('../types/streaming.js').StartEvent} */ ({
350
+ type: 'start',
351
+ execId,
352
+ timestamp,
353
+ });
354
+
355
+ // Start execution in background so we can yield events
356
+ const executionPromise = executor.execute(code, this.#context, {
357
+ ...options,
358
+ execId,
359
+ language,
360
+ });
361
+
362
+ // Poll for stdin events while execution is running
363
+ let result = null;
364
+ let error = null;
365
+ let done = false;
366
+
367
+ executionPromise.then(r => {
368
+ result = r;
369
+ done = true;
370
+ if (stdinEventResolve) {
371
+ stdinEventResolve();
372
+ stdinEventResolve = null;
373
+ }
374
+ }).catch(e => {
375
+ error = e;
376
+ done = true;
377
+ if (stdinEventResolve) {
378
+ stdinEventResolve();
379
+ stdinEventResolve = null;
380
+ }
381
+ });
382
+
383
+ // Wait for either stdin events or completion
384
+ while (!done) {
385
+ // Yield any pending stdin_request events
386
+ while (stdinEventQueue.length > 0) {
387
+ yield /** @type {import('../types/streaming.js').StdinRequestEvent} */ (stdinEventQueue.shift());
388
+ }
389
+
390
+ if (!done) {
391
+ // Wait for next event
392
+ await new Promise(resolve => {
393
+ stdinEventResolve = resolve;
394
+ // Also resolve on short timeout to check done flag
395
+ setTimeout(resolve, 50);
396
+ });
397
+ }
398
+ }
399
+
400
+ // Yield any remaining stdin events
401
+ while (stdinEventQueue.length > 0) {
402
+ yield /** @type {import('../types/streaming.js').StdinRequestEvent} */ (stdinEventQueue.shift());
403
+ }
404
+
405
+ if (error) {
406
+ yield /** @type {import('../types/streaming.js').ErrorEvent} */ ({
407
+ type: 'error',
408
+ error: {
409
+ type: error instanceof Error ? error.name : 'Error',
410
+ message: error instanceof Error ? error.message : String(error),
411
+ traceback: error instanceof Error && error.stack ? error.stack.split('\n') : undefined,
412
+ },
413
+ });
414
+ } else if (result) {
415
+ if (options.storeHistory !== false) {
416
+ this.#executionCount++;
417
+ }
418
+ result.executionCount = this.#executionCount;
419
+
420
+ if (result.stdout) {
421
+ yield /** @type {import('../types/streaming.js').StdoutEvent} */ ({
422
+ type: 'stdout',
423
+ content: result.stdout,
424
+ accumulated: result.stdout,
425
+ });
426
+ }
427
+
428
+ if (result.stderr) {
429
+ yield /** @type {import('../types/streaming.js').StderrEvent} */ ({
430
+ type: 'stderr',
431
+ content: result.stderr,
432
+ accumulated: result.stderr,
433
+ });
434
+ }
435
+
436
+ for (const display of result.displayData) {
437
+ yield /** @type {import('../types/streaming.js').DisplayEvent} */ ({
438
+ type: 'display',
439
+ data: display.data,
440
+ metadata: display.metadata,
441
+ });
442
+ }
443
+
444
+ for (const asset of result.assets) {
445
+ yield /** @type {import('../types/streaming.js').AssetEvent} */ ({
446
+ type: 'asset',
447
+ path: asset.path,
448
+ url: asset.url,
449
+ mimeType: asset.mimeType,
450
+ assetType: asset.assetType,
451
+ });
452
+ }
453
+
454
+ yield /** @type {import('../types/streaming.js').ResultEvent} */ ({
455
+ type: 'result',
456
+ result,
457
+ });
458
+ }
459
+
460
+ yield /** @type {import('../types/streaming.js').DoneEvent} */ ({
461
+ type: 'done',
462
+ });
463
+ }
464
+ } finally {
465
+ // Restore previous stdin handler
466
+ if (this.#context.setStdinHandler) {
467
+ this.#context.setStdinHandler(previousHandler || null);
468
+ }
469
+ this.#runningExecutions.delete(execId);
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Set the executor registry
475
+ * @param {ExecutorRegistry} registry
476
+ */
477
+ setExecutorRegistry(registry) {
478
+ this.#executorRegistry = registry;
479
+ }
480
+
481
+ /**
482
+ * Get the executor registry
483
+ * @returns {ExecutorRegistry | null}
484
+ */
485
+ getExecutorRegistry() {
486
+ return this.#executorRegistry;
487
+ }
488
+
489
+ /**
490
+ * Get supported languages
491
+ * @returns {string[]}
492
+ */
493
+ getSupportedLanguages() {
494
+ if (this.#executorRegistry) {
495
+ return this.#executorRegistry.languages();
496
+ }
497
+ return ['javascript', 'js', 'ecmascript', 'es'];
498
+ }
499
+
500
+ /**
501
+ * Send input to a waiting execution
502
+ * @param {string} execId - Execution ID
503
+ * @param {string} text - Input text
504
+ * @returns {boolean} Whether input was accepted
505
+ */
506
+ sendInput(execId, text) {
507
+ const handler = this.#pendingInputs.get(execId);
508
+ if (handler) {
509
+ handler(text);
510
+ this.#pendingInputs.delete(execId);
511
+ return true;
512
+ }
513
+ return false;
514
+ }
515
+
516
+ /**
517
+ * Interrupt a running execution
518
+ * @param {string} [execId] - Specific execution ID, or all if not provided
519
+ * @returns {boolean} Whether any execution was interrupted
520
+ */
521
+ interrupt(execId) {
522
+ if (execId) {
523
+ const controller = this.#runningExecutions.get(execId);
524
+ if (controller) {
525
+ controller.abort();
526
+ this.#runningExecutions.delete(execId);
527
+ return true;
528
+ }
529
+ return false;
530
+ }
531
+
532
+ // Interrupt all
533
+ if (this.#runningExecutions.size > 0) {
534
+ for (const controller of this.#runningExecutions.values()) {
535
+ controller.abort();
536
+ }
537
+ this.#runningExecutions.clear();
538
+ return true;
539
+ }
540
+
541
+ return false;
542
+ }
543
+
544
+ // ============================================================================
545
+ // LSP Features (delegated to lsp/ modules)
546
+ // ============================================================================
547
+
548
+ /**
549
+ * Get completions at cursor position
550
+ * @param {string} code
551
+ * @param {number} cursor
552
+ * @param {CompleteOptions} [options]
553
+ * @returns {CompletionResult}
554
+ */
555
+ complete(code, cursor, options = {}) {
556
+ return getCompletions(code, cursor, this.#context, options);
557
+ }
558
+
559
+ /**
560
+ * Get hover information at cursor position
561
+ * @param {string} code
562
+ * @param {number} cursor
563
+ * @returns {HoverResult}
564
+ */
565
+ hover(code, cursor) {
566
+ return getHoverInfo(code, cursor, this.#context);
567
+ }
568
+
569
+ /**
570
+ * Get detailed inspection at cursor position
571
+ * @param {string} code
572
+ * @param {number} cursor
573
+ * @param {InspectOptions} [options]
574
+ * @returns {InspectResult}
575
+ */
576
+ inspect(code, cursor, options = {}) {
577
+ return getInspectInfo(code, cursor, this.#context, options);
578
+ }
579
+
580
+ /**
581
+ * List all variables in session
582
+ * @param {VariableFilter} [filter]
583
+ * @returns {VariableInfo[]}
584
+ */
585
+ listVariables(filter = {}) {
586
+ return lspListVariables(this.#context, filter);
587
+ }
588
+
589
+ /**
590
+ * Get detailed information about a variable
591
+ * @param {string} name
592
+ * @param {VariableDetailOptions} [options]
593
+ * @returns {VariableDetail | null}
594
+ */
595
+ getVariable(name, options = {}) {
596
+ return lspGetVariableDetail(name, this.#context, options);
597
+ }
598
+
599
+ // ============================================================================
600
+ // Analysis
601
+ // ============================================================================
602
+
603
+ /**
604
+ * Check if code is a complete statement
605
+ * @param {string} code
606
+ * @returns {IsCompleteResult}
607
+ */
608
+ isComplete(code) {
609
+ return analysisIsComplete(code);
610
+ }
611
+
612
+ /**
613
+ * Format code
614
+ * @param {string} code
615
+ * @returns {Promise<FormatResult>}
616
+ */
617
+ async format(code) {
618
+ return formatCode(code);
619
+ }
620
+
621
+ // ============================================================================
622
+ // Lifecycle
623
+ // ============================================================================
624
+
625
+ /**
626
+ * Reset the session (clear variables but keep session)
627
+ */
628
+ reset() {
629
+ this.#context.reset();
630
+ this.#executionCount = 0;
631
+ this.#lastActivity = new Date();
632
+ }
633
+
634
+ /**
635
+ * Destroy the session and release resources
636
+ */
637
+ destroy() {
638
+ // Cancel any running executions
639
+ this.interrupt();
640
+
641
+ // Destroy context
642
+ this.#context.destroy();
643
+ }
644
+
645
+ /**
646
+ * Get session info
647
+ * @returns {SessionInfo}
648
+ */
649
+ getInfo() {
650
+ return {
651
+ id: this.#id,
652
+ language: this.#language,
653
+ created: this.#created.toISOString(),
654
+ lastActivity: this.#lastActivity.toISOString(),
655
+ executionCount: this.#executionCount,
656
+ variableCount: this.#context.getTrackedVariables().size,
657
+ isolation: this.#isolation,
658
+ };
659
+ }
660
+
661
+ /**
662
+ * Get the underlying execution context (for advanced use)
663
+ * @returns {ExecutionContext}
664
+ */
665
+ getContext() {
666
+ return this.#context;
667
+ }
668
+ }
669
+
670
+ /**
671
+ * Create a session
672
+ * @param {string} id
673
+ * @param {CreateSessionOptions} [options]
674
+ * @returns {Session}
675
+ */
676
+ export function createSession(id, options) {
677
+ return new Session(id, options);
678
+ }