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,357 @@
1
+ /**
2
+ * Main Execution Context
3
+ *
4
+ * Executes JavaScript in the main window context (no isolation).
5
+ * Provides full access to the page's DOM and state.
6
+ *
7
+ * @module session/context/main
8
+ */
9
+
10
+ import { ConsoleCapture } from '../console-capture.js';
11
+
12
+ /**
13
+ * @typedef {import('./interface.js').ExecutionContext} ExecutionContext
14
+ * @typedef {import('./interface.js').RawExecutionResult} RawExecutionResult
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} StdinRequest
19
+ * @property {string} prompt - Prompt text to display
20
+ * @property {boolean} password - Whether to hide input
21
+ * @property {string} execId - Execution ID for this request
22
+ */
23
+
24
+ /**
25
+ * @callback OnStdinRequestCallback
26
+ * @param {StdinRequest} request - The stdin request
27
+ * @returns {Promise<string>} - Resolves with user input
28
+ */
29
+
30
+ /**
31
+ * @typedef {Object} MainContextOptions
32
+ * @property {Record<string, *>} [utilities] - Custom utilities to inject
33
+ */
34
+
35
+ /**
36
+ * Main window execution context (no isolation)
37
+ * @implements {ExecutionContext}
38
+ */
39
+ export class MainContext {
40
+ /** @type {Set<string>} */
41
+ #trackedVars = new Set();
42
+
43
+ /** @type {ConsoleCapture | null} */
44
+ #consoleCapture = null;
45
+
46
+ /** @type {MainContextOptions} */
47
+ #options;
48
+
49
+ /** @type {boolean} */
50
+ #initialized = false;
51
+
52
+ /** @type {Array<{data: Record<string, string>, metadata: Record<string, *>}>} */
53
+ #displayQueue = [];
54
+
55
+ /** @type {OnStdinRequestCallback | null} */
56
+ #onStdinRequest = null;
57
+
58
+ /** @type {string} */
59
+ #currentExecId = '';
60
+
61
+ /**
62
+ * @param {MainContextOptions} [options]
63
+ */
64
+ constructor(options = {}) {
65
+ this.#options = options;
66
+ }
67
+
68
+ /**
69
+ * Initialize the context
70
+ */
71
+ #initialize() {
72
+ if (this.#initialized) return;
73
+
74
+ // Set up utilities on window
75
+ this.#setupUtilities();
76
+
77
+ // Set up console capture
78
+ this.#consoleCapture = new ConsoleCapture(window);
79
+ this.#consoleCapture.start();
80
+
81
+ this.#initialized = true;
82
+ }
83
+
84
+ /**
85
+ * Set up utility functions
86
+ */
87
+ #setupUtilities() {
88
+ const self = this;
89
+
90
+ // Sleep helper (if not already defined)
91
+ if (!('sleep' in window)) {
92
+ /** @type {*} */ (window).sleep = (ms) =>
93
+ new Promise((resolve) => setTimeout(resolve, ms));
94
+ }
95
+
96
+ // Print helper
97
+ if (!('print' in window) || typeof window.print !== 'function') {
98
+ /** @type {*} */ (window).print = (...args) => {
99
+ console.log(...args);
100
+ };
101
+ }
102
+
103
+ // Input helper - prompts for user input (like Python's input())
104
+ /** @type {*} */ (window).__mrmd_input__ = async (prompt = '', options = {}) => {
105
+ // Print prompt to console (like Python does)
106
+ if (prompt) {
107
+ console.log(prompt);
108
+ }
109
+
110
+ // If no stdin handler is set, fall back to browser prompt()
111
+ if (!self.#onStdinRequest) {
112
+ const result = window.prompt(prompt) ?? '';
113
+ return result;
114
+ }
115
+
116
+ // Request input from the external handler
117
+ const request = {
118
+ prompt: prompt,
119
+ password: options.password ?? false,
120
+ execId: self.#currentExecId,
121
+ };
122
+
123
+ try {
124
+ const response = await self.#onStdinRequest(request);
125
+ // Remove trailing newline if present (input() in Python strips it)
126
+ return response.replace(/\n$/, '');
127
+ } catch (error) {
128
+ // If cancelled, throw an error like Python's KeyboardInterrupt
129
+ throw new Error('Input cancelled');
130
+ }
131
+ };
132
+
133
+ // Expose as 'input' if not already defined
134
+ if (!('input' in window)) {
135
+ /** @type {*} */ (window).input = /** @type {*} */ (window).__mrmd_input__;
136
+ }
137
+
138
+ // Display helper
139
+ /** @type {*} */ (window).__mrmd_display__ = (data, mimeType = 'text/plain') => {
140
+ let content;
141
+ if (typeof data === 'string') {
142
+ content = data;
143
+ } else if (data instanceof HTMLElement) {
144
+ content = data.outerHTML;
145
+ mimeType = 'text/html';
146
+ } else {
147
+ try {
148
+ content = JSON.stringify(data, null, 2);
149
+ mimeType = 'application/json';
150
+ } catch {
151
+ content = String(data);
152
+ }
153
+ }
154
+
155
+ this.#displayQueue.push({ data: { [mimeType]: content }, metadata: {} });
156
+ };
157
+
158
+ // Inject custom utilities
159
+ if (this.#options.utilities) {
160
+ for (const [key, value] of Object.entries(this.#options.utilities)) {
161
+ /** @type {*} */ (window)[key] = value;
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Set the stdin request handler
168
+ * @param {OnStdinRequestCallback | null} handler
169
+ */
170
+ setStdinHandler(handler) {
171
+ this.#onStdinRequest = handler;
172
+ }
173
+
174
+ /**
175
+ * Get the current stdin request handler
176
+ * @returns {OnStdinRequestCallback | null}
177
+ */
178
+ getStdinHandler() {
179
+ return this.#onStdinRequest;
180
+ }
181
+
182
+ /**
183
+ * Execute code in main context
184
+ * @param {string} code - Already transformed/wrapped code from executor
185
+ * @param {{ execId?: string }} [options] - Execution options
186
+ * @returns {Promise<RawExecutionResult>}
187
+ */
188
+ async execute(code, options = {}) {
189
+ this.#initialize();
190
+
191
+ // Set current execution ID for input() calls
192
+ this.#currentExecId = options.execId || '';
193
+
194
+ // Clear display queue
195
+ this.#displayQueue = [];
196
+
197
+ // Clear console capture
198
+ this.#consoleCapture?.clear();
199
+
200
+ const startTime = performance.now();
201
+
202
+ try {
203
+ // Execute - code is already transformed/wrapped by the executor
204
+ const result = await eval(code);
205
+ const duration = performance.now() - startTime;
206
+
207
+ // Get logs
208
+ const logs = this.#consoleCapture?.flush() || [];
209
+
210
+ return {
211
+ result,
212
+ logs,
213
+ duration,
214
+ };
215
+ } catch (error) {
216
+ const duration = performance.now() - startTime;
217
+ const logs = this.#consoleCapture?.flush() || [];
218
+
219
+ return {
220
+ result: undefined,
221
+ logs,
222
+ error: error instanceof Error ? error : new Error(String(error)),
223
+ duration,
224
+ };
225
+ } finally {
226
+ // Clear current exec ID
227
+ this.#currentExecId = '';
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Get all user-defined variables
233
+ * @returns {Record<string, *>}
234
+ */
235
+ getVariables() {
236
+ const vars = {};
237
+ for (const name of this.#trackedVars) {
238
+ try {
239
+ vars[name] = /** @type {*} */ (window)[name];
240
+ } catch {
241
+ // Skip inaccessible
242
+ }
243
+ }
244
+ return vars;
245
+ }
246
+
247
+ /**
248
+ * Get a specific variable
249
+ * @param {string} name
250
+ * @returns {*}
251
+ */
252
+ getVariable(name) {
253
+ return /** @type {*} */ (window)[name];
254
+ }
255
+
256
+ /**
257
+ * Check if variable exists
258
+ * @param {string} name
259
+ * @returns {boolean}
260
+ */
261
+ hasVariable(name) {
262
+ return name in window;
263
+ }
264
+
265
+ /**
266
+ * Get the global object
267
+ * @returns {Window}
268
+ */
269
+ getGlobal() {
270
+ return window;
271
+ }
272
+
273
+ /**
274
+ * Track a declared variable
275
+ * @param {string} name
276
+ */
277
+ trackVariable(name) {
278
+ this.#trackedVars.add(name);
279
+ }
280
+
281
+ /**
282
+ * Get tracked variable names
283
+ * @returns {Set<string>}
284
+ */
285
+ getTrackedVariables() {
286
+ return this.#trackedVars;
287
+ }
288
+
289
+ /**
290
+ * Reset the context
291
+ */
292
+ reset() {
293
+ // Delete tracked variables from window
294
+ for (const name of this.#trackedVars) {
295
+ try {
296
+ delete /** @type {*} */ (window)[name];
297
+ } catch {
298
+ // Some properties can't be deleted
299
+ }
300
+ }
301
+ this.#trackedVars = new Set();
302
+ this.#displayQueue = [];
303
+ }
304
+
305
+ /**
306
+ * Destroy the context
307
+ */
308
+ destroy() {
309
+ this.#consoleCapture?.stop();
310
+ this.#consoleCapture = null;
311
+
312
+ // Clean up tracked variables
313
+ this.reset();
314
+
315
+ // Clean up utilities we added
316
+ try {
317
+ delete /** @type {*} */ (window).__mrmd_display__;
318
+ } catch {
319
+ // Ignore
320
+ }
321
+
322
+ this.#initialized = false;
323
+ }
324
+
325
+ /**
326
+ * Check if this is main context
327
+ * @returns {boolean}
328
+ */
329
+ isMainContext() {
330
+ return true;
331
+ }
332
+
333
+ /**
334
+ * Get the iframe element
335
+ * @returns {null}
336
+ */
337
+ getIframe() {
338
+ return null;
339
+ }
340
+
341
+ /**
342
+ * Get display data queue
343
+ * @returns {Array<{data: Record<string, string>, metadata: Record<string, *>}>}
344
+ */
345
+ getDisplayQueue() {
346
+ return this.#displayQueue;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Create a main context
352
+ * @param {MainContextOptions} [options]
353
+ * @returns {MainContext}
354
+ */
355
+ export function createMainContext(options) {
356
+ return new MainContext(options);
357
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Session Management
3
+ *
4
+ * Session and context management for mrmd-js.
5
+ * @module session
6
+ */
7
+
8
+ // Session classes
9
+ export { Session, createSession } from './session.js';
10
+ export { SessionManager, createSessionManager } from './manager.js';
11
+
12
+ // Context infrastructure
13
+ export { ConsoleCapture, createConsoleCapture } from './console-capture.js';
14
+ export { IframeContext, createIframeContext } from './context/iframe.js';
15
+ export { MainContext, createMainContext } from './context/main.js';
16
+ export * from './context/interface.js';
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Session Manager
3
+ *
4
+ * Manages multiple sessions, handles creation/destruction,
5
+ * and enforces limits.
6
+ *
7
+ * @module session/manager
8
+ */
9
+
10
+ import { Session } from './session.js';
11
+
12
+ /**
13
+ * @typedef {import('../types/session.js').SessionInfo} SessionInfo
14
+ * @typedef {import('../types/session.js').CreateSessionOptions} CreateSessionOptions
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} SessionManagerOptions
19
+ * @property {number} [maxSessions=10] - Maximum number of concurrent sessions
20
+ * @property {string} [defaultLanguage='javascript'] - Default language for new sessions
21
+ * @property {import('../types/session.js').IsolationMode} [defaultIsolation='iframe'] - Default isolation mode
22
+ * @property {boolean} [defaultAllowMainAccess=false] - Default main access setting
23
+ */
24
+
25
+ /**
26
+ * Generate a unique session ID
27
+ * @returns {string}
28
+ */
29
+ function generateSessionId() {
30
+ return `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
31
+ }
32
+
33
+ /**
34
+ * Session Manager - manages multiple execution sessions
35
+ */
36
+ export class SessionManager {
37
+ /** @type {Map<string, Session>} */
38
+ #sessions = new Map();
39
+
40
+ /** @type {SessionManagerOptions} */
41
+ #options;
42
+
43
+ /**
44
+ * @param {SessionManagerOptions} [options]
45
+ */
46
+ constructor(options = {}) {
47
+ this.#options = {
48
+ maxSessions: 10,
49
+ defaultLanguage: 'javascript',
50
+ defaultIsolation: 'iframe',
51
+ defaultAllowMainAccess: false,
52
+ ...options,
53
+ };
54
+ }
55
+
56
+ // ============================================================================
57
+ // Session Lifecycle
58
+ // ============================================================================
59
+
60
+ /**
61
+ * Create a new session
62
+ * @param {CreateSessionOptions} [options]
63
+ * @returns {Session}
64
+ * @throws {Error} If max sessions reached
65
+ */
66
+ create(options = {}) {
67
+ // Check limits
68
+ if (this.#sessions.size >= this.#options.maxSessions) {
69
+ throw new Error(
70
+ `Maximum sessions (${this.#options.maxSessions}) reached. ` +
71
+ 'Destroy existing sessions before creating new ones.'
72
+ );
73
+ }
74
+
75
+ // Generate ID if not provided
76
+ const id = options.id || generateSessionId();
77
+
78
+ // Check for duplicate ID
79
+ if (this.#sessions.has(id)) {
80
+ throw new Error(`Session with ID '${id}' already exists`);
81
+ }
82
+
83
+ // Merge with defaults
84
+ const sessionOptions = {
85
+ language: options.language || this.#options.defaultLanguage,
86
+ isolation: options.isolation || this.#options.defaultIsolation,
87
+ allowMainAccess: options.allowMainAccess ?? this.#options.defaultAllowMainAccess,
88
+ utilities: options.utilities,
89
+ executorRegistry: options.executorRegistry,
90
+ };
91
+
92
+ // Create session
93
+ const session = new Session(id, sessionOptions);
94
+ this.#sessions.set(id, session);
95
+
96
+ return session;
97
+ }
98
+
99
+ /**
100
+ * Get an existing session by ID
101
+ * @param {string} id
102
+ * @returns {Session | undefined}
103
+ */
104
+ get(id) {
105
+ return this.#sessions.get(id);
106
+ }
107
+
108
+ /**
109
+ * Get a session, creating it if it doesn't exist
110
+ * @param {string} id
111
+ * @param {CreateSessionOptions} [options]
112
+ * @returns {Session}
113
+ */
114
+ getOrCreate(id, options = {}) {
115
+ const existing = this.#sessions.get(id);
116
+ if (existing) {
117
+ return existing;
118
+ }
119
+ return this.create({ ...options, id });
120
+ }
121
+
122
+ /**
123
+ * Check if a session exists
124
+ * @param {string} id
125
+ * @returns {boolean}
126
+ */
127
+ has(id) {
128
+ return this.#sessions.has(id);
129
+ }
130
+
131
+ /**
132
+ * Destroy a session by ID
133
+ * @param {string} id
134
+ * @returns {boolean} Whether a session was destroyed
135
+ */
136
+ destroy(id) {
137
+ const session = this.#sessions.get(id);
138
+ if (session) {
139
+ session.destroy();
140
+ this.#sessions.delete(id);
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ /**
147
+ * Destroy all sessions
148
+ */
149
+ destroyAll() {
150
+ for (const session of this.#sessions.values()) {
151
+ session.destroy();
152
+ }
153
+ this.#sessions.clear();
154
+ }
155
+
156
+ // ============================================================================
157
+ // Session Queries
158
+ // ============================================================================
159
+
160
+ /**
161
+ * List all sessions
162
+ * @returns {SessionInfo[]}
163
+ */
164
+ list() {
165
+ return Array.from(this.#sessions.values()).map((s) => s.getInfo());
166
+ }
167
+
168
+ /**
169
+ * Get number of active sessions
170
+ * @returns {number}
171
+ */
172
+ get size() {
173
+ return this.#sessions.size;
174
+ }
175
+
176
+ /**
177
+ * Get all session IDs
178
+ * @returns {string[]}
179
+ */
180
+ get ids() {
181
+ return Array.from(this.#sessions.keys());
182
+ }
183
+
184
+ /**
185
+ * Get the maximum number of sessions allowed
186
+ * @returns {number}
187
+ */
188
+ get maxSessions() {
189
+ return this.#options.maxSessions;
190
+ }
191
+
192
+ /**
193
+ * Iterate over sessions
194
+ * @returns {IterableIterator<Session>}
195
+ */
196
+ [Symbol.iterator]() {
197
+ return this.#sessions.values();
198
+ }
199
+
200
+ /**
201
+ * Iterate over session entries
202
+ * @returns {IterableIterator<[string, Session]>}
203
+ */
204
+ entries() {
205
+ return this.#sessions.entries();
206
+ }
207
+
208
+ // ============================================================================
209
+ // Bulk Operations
210
+ // ============================================================================
211
+
212
+ /**
213
+ * Reset all sessions (clear variables but keep sessions)
214
+ */
215
+ resetAll() {
216
+ for (const session of this.#sessions.values()) {
217
+ session.reset();
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Interrupt all running executions across all sessions
223
+ * @returns {number} Number of sessions that had executions interrupted
224
+ */
225
+ interruptAll() {
226
+ let count = 0;
227
+ for (const session of this.#sessions.values()) {
228
+ if (session.interrupt()) {
229
+ count++;
230
+ }
231
+ }
232
+ return count;
233
+ }
234
+
235
+ /**
236
+ * Get sessions by language
237
+ * @param {string} language
238
+ * @returns {Session[]}
239
+ */
240
+ getByLanguage(language) {
241
+ const results = [];
242
+ for (const session of this.#sessions.values()) {
243
+ if (session.language === language) {
244
+ results.push(session);
245
+ }
246
+ }
247
+ return results;
248
+ }
249
+
250
+ /**
251
+ * Get the most recently active session
252
+ * @returns {Session | undefined}
253
+ */
254
+ getMostRecent() {
255
+ let mostRecent = undefined;
256
+ let latestTime = 0;
257
+
258
+ for (const session of this.#sessions.values()) {
259
+ const time = session.lastActivity.getTime();
260
+ if (time > latestTime) {
261
+ latestTime = time;
262
+ mostRecent = session;
263
+ }
264
+ }
265
+
266
+ return mostRecent;
267
+ }
268
+
269
+ // ============================================================================
270
+ // Cleanup
271
+ // ============================================================================
272
+
273
+ /**
274
+ * Destroy sessions that have been inactive for a certain time
275
+ * @param {number} maxIdleMs - Maximum idle time in milliseconds
276
+ * @returns {number} Number of sessions destroyed
277
+ */
278
+ cleanupIdle(maxIdleMs) {
279
+ const now = Date.now();
280
+ const toDestroy = [];
281
+
282
+ for (const [id, session] of this.#sessions) {
283
+ if (now - session.lastActivity.getTime() > maxIdleMs) {
284
+ toDestroy.push(id);
285
+ }
286
+ }
287
+
288
+ for (const id of toDestroy) {
289
+ this.destroy(id);
290
+ }
291
+
292
+ return toDestroy.length;
293
+ }
294
+
295
+ /**
296
+ * Destroy oldest sessions to get under a certain count
297
+ * @param {number} targetCount - Target number of sessions
298
+ * @returns {number} Number of sessions destroyed
299
+ */
300
+ trimToCount(targetCount) {
301
+ if (this.#sessions.size <= targetCount) {
302
+ return 0;
303
+ }
304
+
305
+ // Sort by last activity (oldest first)
306
+ const sorted = Array.from(this.#sessions.entries()).sort(
307
+ ([, a], [, b]) => a.lastActivity.getTime() - b.lastActivity.getTime()
308
+ );
309
+
310
+ const toDestroy = sorted.slice(0, this.#sessions.size - targetCount);
311
+
312
+ for (const [id] of toDestroy) {
313
+ this.destroy(id);
314
+ }
315
+
316
+ return toDestroy.length;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Create a session manager
322
+ * @param {SessionManagerOptions} [options]
323
+ * @returns {SessionManager}
324
+ */
325
+ export function createSessionManager(options) {
326
+ return new SessionManager(options);
327
+ }