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,162 @@
1
+ /**
2
+ * HTML Executor
3
+ *
4
+ * Executes HTML cells by producing displayData with text/html MIME type.
5
+ * Optionally extracts and executes inline scripts.
6
+ *
7
+ * @module execute/html
8
+ */
9
+
10
+ import { BaseExecutor } from './interface.js';
11
+
12
+ /**
13
+ * @typedef {import('../session/context/interface.js').ExecutionContext} ExecutionContext
14
+ * @typedef {import('../types/execution.js').ExecuteOptions} ExecuteOptions
15
+ * @typedef {import('../types/execution.js').ExecutionResult} ExecutionResult
16
+ * @typedef {import('../types/execution.js').DisplayData} DisplayData
17
+ */
18
+
19
+ /**
20
+ * Regex to match script tags and capture their content
21
+ */
22
+ const SCRIPT_REGEX = /<script[^>]*>([\s\S]*?)<\/script>/gi;
23
+
24
+ /**
25
+ * Regex to match style tags and capture their content
26
+ */
27
+ const STYLE_REGEX = /<style[^>]*>([\s\S]*?)<\/style>/gi;
28
+
29
+ /**
30
+ * Extract script tags from HTML
31
+ * @param {string} html
32
+ * @returns {{ html: string, scripts: string[] }}
33
+ */
34
+ function extractScripts(html) {
35
+ const scripts = [];
36
+
37
+ const cleaned = html.replace(SCRIPT_REGEX, (_, content) => {
38
+ if (content.trim()) {
39
+ scripts.push(content);
40
+ }
41
+ return '';
42
+ });
43
+
44
+ return { html: cleaned, scripts };
45
+ }
46
+
47
+ /**
48
+ * Extract style tags from HTML
49
+ * @param {string} html
50
+ * @returns {{ html: string, styles: string[] }}
51
+ */
52
+ function extractStyles(html) {
53
+ const styles = [];
54
+
55
+ const cleaned = html.replace(STYLE_REGEX, (_, content) => {
56
+ if (content.trim()) {
57
+ styles.push(content);
58
+ }
59
+ return '';
60
+ });
61
+
62
+ return { html: cleaned, styles };
63
+ }
64
+
65
+ /**
66
+ * HTML executor - produces displayData for HTML content
67
+ */
68
+ export class HtmlExecutor extends BaseExecutor {
69
+ /** @type {readonly string[]} */
70
+ languages = ['html', 'htm', 'xhtml'];
71
+
72
+ /**
73
+ * Execute HTML cell
74
+ * @param {string} code - HTML content
75
+ * @param {ExecutionContext} context - Execution context
76
+ * @param {ExecuteOptions} [options] - Execution options
77
+ * @returns {Promise<ExecutionResult>}
78
+ */
79
+ async execute(code, context, options = {}) {
80
+ const startTime = performance.now();
81
+
82
+ // Extract scripts and styles
83
+ const { html: htmlWithoutScripts, scripts } = extractScripts(code);
84
+ const { html: cleanHtml, styles } = extractStyles(htmlWithoutScripts);
85
+
86
+ // Build display data
87
+ /** @type {DisplayData[]} */
88
+ const displayData = [];
89
+
90
+ // Main HTML content
91
+ displayData.push({
92
+ data: {
93
+ 'text/html': code, // Send original HTML including scripts/styles
94
+ },
95
+ metadata: {
96
+ // Metadata for client to decide how to render
97
+ hasScripts: scripts.length > 0,
98
+ hasStyles: styles.length > 0,
99
+ scriptCount: scripts.length,
100
+ styleCount: styles.length,
101
+ trusted: options.cellMeta?.trusted ?? false,
102
+ // Client can use this to decide whether to execute scripts
103
+ executeScripts: options.cellMeta?.executeScripts ?? true,
104
+ },
105
+ });
106
+
107
+ // Optionally include cleaned HTML (without scripts/styles) as alternate
108
+ if (scripts.length > 0 || styles.length > 0) {
109
+ displayData.push({
110
+ data: {
111
+ 'text/html+safe': cleanHtml.trim(),
112
+ },
113
+ metadata: {
114
+ description: 'HTML content with scripts and styles removed',
115
+ },
116
+ });
117
+ }
118
+
119
+ // Include extracted styles as separate CSS display data
120
+ if (styles.length > 0) {
121
+ displayData.push({
122
+ data: {
123
+ 'text/css': styles.join('\n\n'),
124
+ },
125
+ metadata: {
126
+ source: 'extracted',
127
+ description: 'Styles extracted from HTML',
128
+ },
129
+ });
130
+ }
131
+
132
+ const duration = performance.now() - startTime;
133
+
134
+ // Build info message
135
+ const parts = [];
136
+ if (cleanHtml.trim()) parts.push('HTML');
137
+ if (styles.length > 0) parts.push(`${styles.length} style${styles.length > 1 ? 's' : ''}`);
138
+ if (scripts.length > 0) parts.push(`${scripts.length} script${scripts.length > 1 ? 's' : ''}`);
139
+
140
+ return {
141
+ success: true,
142
+ stdout: `Rendered: ${parts.join(', ') || 'empty'}`,
143
+ stderr: '',
144
+ result: undefined,
145
+ displayData,
146
+ assets: [],
147
+ executionCount: 0,
148
+ duration,
149
+ };
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Create an HTML executor
155
+ * @returns {HtmlExecutor}
156
+ */
157
+ export function createHtmlExecutor() {
158
+ return new HtmlExecutor();
159
+ }
160
+
161
+ // Export utilities for use by clients
162
+ export { extractScripts, extractStyles };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Execute Module
3
+ *
4
+ * Provides executors for different languages and the registry to manage them.
5
+ *
6
+ * @module execute
7
+ */
8
+
9
+ // Interface
10
+ export { BaseExecutor } from './interface.js';
11
+
12
+ // Registry
13
+ export { ExecutorRegistry, createExecutorRegistry } from './registry.js';
14
+
15
+ // Executors
16
+ export { JavaScriptExecutor, createJavaScriptExecutor } from './javascript.js';
17
+ export { HtmlExecutor, createHtmlExecutor, extractScripts, extractStyles } from './html.js';
18
+ export {
19
+ CssExecutor,
20
+ createCssExecutor,
21
+ scopeStyles,
22
+ generateScopeClass,
23
+ } from './css.js';
24
+
25
+ // Import for factory function
26
+ import { ExecutorRegistry } from './registry.js';
27
+ import { JavaScriptExecutor } from './javascript.js';
28
+ import { HtmlExecutor } from './html.js';
29
+ import { CssExecutor } from './css.js';
30
+
31
+ /**
32
+ * Create a registry with default executors registered
33
+ * @returns {ExecutorRegistry}
34
+ */
35
+ export function createDefaultExecutorRegistry() {
36
+ const registry = new ExecutorRegistry();
37
+ registry.register(new JavaScriptExecutor());
38
+ registry.register(new HtmlExecutor());
39
+ registry.register(new CssExecutor());
40
+ return registry;
41
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Executor Interface
3
+ *
4
+ * Defines the contract for language executors.
5
+ * Each executor handles one or more languages and produces MRP-compliant results.
6
+ *
7
+ * @module execute/interface
8
+ */
9
+
10
+ /**
11
+ * @typedef {import('../session/context/interface.js').ExecutionContext} ExecutionContext
12
+ * @typedef {import('../types/execution.js').ExecuteOptions} ExecuteOptions
13
+ * @typedef {import('../types/execution.js').ExecutionResult} ExecutionResult
14
+ * @typedef {import('../types/streaming.js').StreamEvent} StreamEvent
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} Executor
19
+ * @property {readonly string[]} languages - Language identifiers this executor handles
20
+ * @property {function(string, ExecutionContext, ExecuteOptions=): Promise<ExecutionResult>} execute - Execute code
21
+ * @property {function(string, ExecutionContext, ExecuteOptions=): AsyncGenerator<StreamEvent>} [executeStream] - Execute with streaming
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} ExecutorConfig
26
+ * @property {string[]} languages - Language identifiers to register
27
+ */
28
+
29
+ /**
30
+ * Base class for executors (optional, executors can also be plain objects)
31
+ * @abstract
32
+ */
33
+ export class BaseExecutor {
34
+ /** @type {readonly string[]} */
35
+ languages = [];
36
+
37
+ /**
38
+ * Execute code
39
+ * @param {string} code - Code to execute
40
+ * @param {ExecutionContext} context - Execution context
41
+ * @param {ExecuteOptions} [options] - Execution options
42
+ * @returns {Promise<ExecutionResult>}
43
+ * @abstract
44
+ */
45
+ async execute(code, context, options = {}) {
46
+ throw new Error('execute() must be implemented by subclass');
47
+ }
48
+
49
+ /**
50
+ * Execute code with streaming output
51
+ * Default implementation wraps execute() result
52
+ *
53
+ * @param {string} code - Code to execute
54
+ * @param {ExecutionContext} context - Execution context
55
+ * @param {ExecuteOptions} [options] - Execution options
56
+ * @returns {AsyncGenerator<StreamEvent>}
57
+ */
58
+ async *executeStream(code, context, options = {}) {
59
+ const execId = options.execId || `exec-${Date.now()}`;
60
+ const timestamp = new Date().toISOString();
61
+
62
+ // Start event
63
+ yield /** @type {import('../types/streaming.js').StartEvent} */ ({
64
+ type: 'start',
65
+ execId,
66
+ timestamp,
67
+ });
68
+
69
+ try {
70
+ // Execute
71
+ const result = await this.execute(code, context, options);
72
+
73
+ // Stream stdout
74
+ if (result.stdout) {
75
+ yield /** @type {import('../types/streaming.js').StdoutEvent} */ ({
76
+ type: 'stdout',
77
+ content: result.stdout,
78
+ accumulated: result.stdout,
79
+ });
80
+ }
81
+
82
+ // Stream stderr
83
+ if (result.stderr) {
84
+ yield /** @type {import('../types/streaming.js').StderrEvent} */ ({
85
+ type: 'stderr',
86
+ content: result.stderr,
87
+ accumulated: result.stderr,
88
+ });
89
+ }
90
+
91
+ // Stream display data
92
+ for (const display of result.displayData) {
93
+ yield /** @type {import('../types/streaming.js').DisplayEvent} */ ({
94
+ type: 'display',
95
+ data: display.data,
96
+ metadata: display.metadata,
97
+ });
98
+ }
99
+
100
+ // Stream assets
101
+ for (const asset of result.assets) {
102
+ yield /** @type {import('../types/streaming.js').AssetEvent} */ ({
103
+ type: 'asset',
104
+ path: asset.path,
105
+ url: asset.url,
106
+ mimeType: asset.mimeType,
107
+ assetType: asset.assetType,
108
+ });
109
+ }
110
+
111
+ // Result event
112
+ yield /** @type {import('../types/streaming.js').ResultEvent} */ ({
113
+ type: 'result',
114
+ result,
115
+ });
116
+ } catch (error) {
117
+ // Error event
118
+ yield /** @type {import('../types/streaming.js').ErrorEvent} */ ({
119
+ type: 'error',
120
+ error: {
121
+ type: error instanceof Error ? error.name : 'Error',
122
+ message: error instanceof Error ? error.message : String(error),
123
+ traceback: error instanceof Error && error.stack ? error.stack.split('\n') : undefined,
124
+ },
125
+ });
126
+ }
127
+
128
+ // Done event
129
+ yield /** @type {import('../types/streaming.js').DoneEvent} */ ({
130
+ type: 'done',
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Check if this executor supports a language
136
+ * @param {string} language
137
+ * @returns {boolean}
138
+ */
139
+ supports(language) {
140
+ return this.languages.includes(language.toLowerCase());
141
+ }
142
+ }
143
+
144
+ export {};
@@ -0,0 +1,197 @@
1
+ /**
2
+ * JavaScript Executor
3
+ *
4
+ * Executes JavaScript code in an execution context.
5
+ * Handles variable persistence, async/await, and console output.
6
+ *
7
+ * @module execute/javascript
8
+ */
9
+
10
+ import { BaseExecutor } from './interface.js';
11
+ import { transformForPersistence } from '../transform/persistence.js';
12
+ import { wrapWithLastExpression } from '../transform/async.js';
13
+ import { extractDeclaredVariables } from '../transform/extract.js';
14
+
15
+ /**
16
+ * @typedef {import('../session/context/interface.js').ExecutionContext} ExecutionContext
17
+ * @typedef {import('../types/execution.js').ExecuteOptions} ExecuteOptions
18
+ * @typedef {import('../types/execution.js').ExecutionResult} ExecutionResult
19
+ * @typedef {import('../types/execution.js').ExecutionError} ExecutionError
20
+ * @typedef {import('../types/execution.js').DisplayData} DisplayData
21
+ */
22
+
23
+ /**
24
+ * Format a value for display as a string
25
+ * @param {*} value
26
+ * @param {number} [maxLength=1000]
27
+ * @returns {string | undefined}
28
+ */
29
+ function formatValue(value, maxLength = 1000) {
30
+ if (value === undefined) return undefined;
31
+ if (value === null) return 'null';
32
+
33
+ if (typeof value === 'function') {
34
+ return `[Function: ${value.name || 'anonymous'}]`;
35
+ }
36
+
37
+ if (typeof value === 'symbol') {
38
+ return value.toString();
39
+ }
40
+
41
+ if (typeof value === 'object') {
42
+ try {
43
+ const json = JSON.stringify(value, null, 2);
44
+ if (json.length > maxLength) {
45
+ return json.slice(0, maxLength) + '...';
46
+ }
47
+ return json;
48
+ } catch {
49
+ return String(value);
50
+ }
51
+ }
52
+
53
+ const str = String(value);
54
+ if (str.length > maxLength) {
55
+ return str.slice(0, maxLength) + '...';
56
+ }
57
+ return str;
58
+ }
59
+
60
+ /**
61
+ * JavaScript executor
62
+ */
63
+ export class JavaScriptExecutor extends BaseExecutor {
64
+ /** @type {readonly string[]} */
65
+ languages = ['javascript', 'js', 'ecmascript', 'es'];
66
+
67
+ /**
68
+ * Execute JavaScript code
69
+ * @param {string} code - Code to execute
70
+ * @param {ExecutionContext} context - Execution context
71
+ * @param {ExecuteOptions} [options] - Execution options
72
+ * @returns {Promise<ExecutionResult>}
73
+ */
74
+ async execute(code, context, options = {}) {
75
+ const startTime = performance.now();
76
+
77
+ // Extract and track declared variables
78
+ const declaredVars = extractDeclaredVariables(code);
79
+ for (const varName of declaredVars) {
80
+ context.trackVariable(varName);
81
+ }
82
+
83
+ // Transform code for persistence (const/let → var)
84
+ const transformed = transformForPersistence(code);
85
+
86
+ // Wrap to capture last expression value and support async
87
+ const wrapped = wrapWithLastExpression(transformed);
88
+
89
+ try {
90
+ // Execute in context (pass execId for input() support)
91
+ const rawResult = await context.execute(wrapped, { execId: options.execId });
92
+ const duration = performance.now() - startTime;
93
+
94
+ // Format result
95
+ return this.#formatResult(rawResult, context, duration, options);
96
+ } catch (error) {
97
+ const duration = performance.now() - startTime;
98
+
99
+ return {
100
+ success: false,
101
+ stdout: '',
102
+ stderr: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
103
+ error: this.#formatError(error),
104
+ displayData: [],
105
+ assets: [],
106
+ executionCount: 0,
107
+ duration,
108
+ };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Format raw execution result to MRP ExecutionResult
114
+ * @param {import('../session/context/interface.js').RawExecutionResult} raw
115
+ * @param {ExecutionContext} context
116
+ * @param {number} duration
117
+ * @param {ExecuteOptions} options
118
+ * @returns {ExecutionResult}
119
+ */
120
+ #formatResult(raw, context, duration, options) {
121
+ // Separate logs into stdout/stderr
122
+ const stdout = raw.logs
123
+ .filter((log) => log.type === 'log' || log.type === 'info')
124
+ .map((log) => log.args.map((arg) => formatValue(arg) ?? '').join(' '))
125
+ .join('\n');
126
+
127
+ const stderr = raw.logs
128
+ .filter((log) => log.type === 'error' || log.type === 'warn')
129
+ .map((log) => {
130
+ const prefix = log.type === 'error' ? 'Error: ' : 'Warning: ';
131
+ return prefix + log.args.map((arg) => formatValue(arg) ?? '').join(' ');
132
+ })
133
+ .join('\n');
134
+
135
+ // Format error if present
136
+ /** @type {ExecutionError | undefined} */
137
+ let error;
138
+ if (raw.error) {
139
+ error = this.#formatError(raw.error);
140
+ }
141
+
142
+ // Get display data from context
143
+ /** @type {DisplayData[]} */
144
+ const displayData = 'getDisplayQueue' in context ? context.getDisplayQueue() : [];
145
+
146
+ return {
147
+ success: !raw.error,
148
+ stdout,
149
+ stderr,
150
+ result: raw.result,
151
+ resultString: formatValue(raw.result),
152
+ error,
153
+ displayData,
154
+ assets: [],
155
+ executionCount: 0, // Will be set by session
156
+ duration,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Format an error
162
+ * @param {*} error
163
+ * @returns {ExecutionError}
164
+ */
165
+ #formatError(error) {
166
+ if (error instanceof Error) {
167
+ /** @type {ExecutionError} */
168
+ const formatted = {
169
+ type: error.name,
170
+ message: error.message,
171
+ traceback: error.stack?.split('\n'),
172
+ };
173
+
174
+ // Try to extract line/column from stack
175
+ const lineMatch = error.stack?.match(/:(\d+):(\d+)/);
176
+ if (lineMatch) {
177
+ formatted.line = parseInt(lineMatch[1], 10);
178
+ formatted.column = parseInt(lineMatch[2], 10);
179
+ }
180
+
181
+ return formatted;
182
+ }
183
+
184
+ return {
185
+ type: 'Error',
186
+ message: String(error),
187
+ };
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Create a JavaScript executor
193
+ * @returns {JavaScriptExecutor}
194
+ */
195
+ export function createJavaScriptExecutor() {
196
+ return new JavaScriptExecutor();
197
+ }