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,394 @@
1
+ /**
2
+ * Statement Completeness Checker
3
+ *
4
+ * Determines whether a piece of code is a complete statement that can
5
+ * be executed, or if it needs more input (like an unclosed bracket).
6
+ *
7
+ * @module analysis/is-complete
8
+ */
9
+
10
+ /**
11
+ * @typedef {import('../types/analysis.js').IsCompleteResult} IsCompleteResult
12
+ */
13
+
14
+ /**
15
+ * Check if code is a complete statement
16
+ *
17
+ * @param {string} code - The code to check
18
+ * @returns {IsCompleteResult}
19
+ */
20
+ export function isComplete(code) {
21
+ const trimmed = code.trim();
22
+
23
+ // Empty code is complete
24
+ if (!trimmed) {
25
+ return { status: 'complete', indent: '' };
26
+ }
27
+
28
+ // Check bracket balance
29
+ const bracketInfo = checkBrackets(trimmed);
30
+ if (bracketInfo.unclosed > 0) {
31
+ return {
32
+ status: 'incomplete',
33
+ indent: ' '.repeat(bracketInfo.unclosed),
34
+ };
35
+ }
36
+ if (bracketInfo.unclosed < 0) {
37
+ return { status: 'invalid', indent: '' };
38
+ }
39
+
40
+ // Check for unterminated strings
41
+ const stringInfo = checkStrings(trimmed);
42
+ if (stringInfo.unclosed) {
43
+ return { status: 'incomplete', indent: '' };
44
+ }
45
+
46
+ // Check for trailing operators that suggest continuation
47
+ if (endsWithContinuation(trimmed)) {
48
+ return { status: 'incomplete', indent: '' };
49
+ }
50
+
51
+ // Check for incomplete template literals
52
+ if (hasIncompleteTemplate(trimmed)) {
53
+ return { status: 'incomplete', indent: '' };
54
+ }
55
+
56
+ // Try to parse to verify syntax
57
+ const parseResult = tryParse(code);
58
+ return parseResult;
59
+ }
60
+
61
+ /**
62
+ * Check bracket balance
63
+ * @param {string} code
64
+ * @returns {{ unclosed: number }}
65
+ */
66
+ function checkBrackets(code) {
67
+ let depth = 0;
68
+ let inString = null;
69
+ let inTemplate = false;
70
+ let templateDepth = 0;
71
+ let inLineComment = false;
72
+ let inBlockComment = false;
73
+
74
+ for (let i = 0; i < code.length; i++) {
75
+ const char = code[i];
76
+ const prev = code[i - 1];
77
+ const next = code[i + 1];
78
+
79
+ // Handle escape sequences in strings
80
+ if ((inString || inTemplate) && prev === '\\') {
81
+ continue;
82
+ }
83
+
84
+ // Handle comments
85
+ if (!inString && !inTemplate && !inBlockComment && char === '/' && next === '/') {
86
+ inLineComment = true;
87
+ continue;
88
+ }
89
+ if (inLineComment && char === '\n') {
90
+ inLineComment = false;
91
+ continue;
92
+ }
93
+ if (inLineComment) continue;
94
+
95
+ if (!inString && !inTemplate && !inLineComment && char === '/' && next === '*') {
96
+ inBlockComment = true;
97
+ i++;
98
+ continue;
99
+ }
100
+ if (inBlockComment && char === '*' && next === '/') {
101
+ inBlockComment = false;
102
+ i++;
103
+ continue;
104
+ }
105
+ if (inBlockComment) continue;
106
+
107
+ // Handle strings
108
+ if (!inTemplate && (char === '"' || char === "'")) {
109
+ if (inString === char) {
110
+ inString = null;
111
+ } else if (!inString) {
112
+ inString = char;
113
+ }
114
+ continue;
115
+ }
116
+
117
+ // Handle template literals
118
+ if (char === '`') {
119
+ if (inTemplate && templateDepth === 0) {
120
+ inTemplate = false;
121
+ } else if (!inString) {
122
+ inTemplate = true;
123
+ templateDepth = 0;
124
+ }
125
+ continue;
126
+ }
127
+
128
+ // Handle template expressions ${...}
129
+ if (inTemplate && char === '$' && next === '{') {
130
+ templateDepth++;
131
+ continue;
132
+ }
133
+ if (inTemplate && templateDepth > 0 && char === '}') {
134
+ templateDepth--;
135
+ continue;
136
+ }
137
+
138
+ // Skip bracket counting inside strings
139
+ if (inString) continue;
140
+ if (inTemplate && templateDepth === 0) continue;
141
+
142
+ // Count brackets
143
+ if (char === '{' || char === '[' || char === '(') {
144
+ depth++;
145
+ } else if (char === '}' || char === ']' || char === ')') {
146
+ depth--;
147
+ }
148
+ }
149
+
150
+ return { unclosed: depth };
151
+ }
152
+
153
+ /**
154
+ * Check for unterminated strings
155
+ * @param {string} code
156
+ * @returns {{ unclosed: boolean }}
157
+ */
158
+ function checkStrings(code) {
159
+ let inString = null;
160
+ let inTemplate = false;
161
+
162
+ for (let i = 0; i < code.length; i++) {
163
+ const char = code[i];
164
+ const prev = code[i - 1];
165
+
166
+ // Skip escaped characters
167
+ if (prev === '\\') continue;
168
+
169
+ // Skip comments
170
+ if (!inString && !inTemplate && char === '/' && code[i + 1] === '/') {
171
+ // Find end of line
172
+ const newline = code.indexOf('\n', i);
173
+ if (newline === -1) break;
174
+ i = newline;
175
+ continue;
176
+ }
177
+
178
+ if (!inString && !inTemplate && char === '/' && code[i + 1] === '*') {
179
+ const end = code.indexOf('*/', i + 2);
180
+ if (end === -1) break;
181
+ i = end + 1;
182
+ continue;
183
+ }
184
+
185
+ // Track strings
186
+ if (!inTemplate && (char === '"' || char === "'")) {
187
+ if (inString === char) {
188
+ inString = null;
189
+ } else if (!inString) {
190
+ inString = char;
191
+ }
192
+ }
193
+
194
+ // Track template literals
195
+ if (!inString && char === '`') {
196
+ inTemplate = !inTemplate;
197
+ }
198
+ }
199
+
200
+ return { unclosed: inString !== null || inTemplate };
201
+ }
202
+
203
+ /**
204
+ * Check if code ends with a continuation operator
205
+ * @param {string} code
206
+ * @returns {boolean}
207
+ */
208
+ function endsWithContinuation(code) {
209
+ // Remove trailing whitespace and comments
210
+ let trimmed = code.trim();
211
+
212
+ // Remove trailing line comment
213
+ const lines = trimmed.split('\n');
214
+ let lastLine = lines[lines.length - 1].trim();
215
+ const commentIndex = findLineCommentStart(lastLine);
216
+ if (commentIndex !== -1) {
217
+ lastLine = lastLine.slice(0, commentIndex).trim();
218
+ if (!lastLine) {
219
+ // Line was only a comment, check previous lines
220
+ for (let i = lines.length - 2; i >= 0; i--) {
221
+ lastLine = lines[i].trim();
222
+ const ci = findLineCommentStart(lastLine);
223
+ if (ci !== -1) {
224
+ lastLine = lastLine.slice(0, ci).trim();
225
+ }
226
+ if (lastLine) break;
227
+ }
228
+ }
229
+ }
230
+
231
+ if (!lastLine) return false;
232
+
233
+ // Operators that suggest continuation
234
+ const continuationOps = [
235
+ '+', '-', '*', '/', '%', '**',
236
+ '=', '+=', '-=', '*=', '/=', '%=',
237
+ '==', '===', '!=', '!==',
238
+ '<', '>', '<=', '>=',
239
+ '&&', '||', '??',
240
+ '&', '|', '^', '~',
241
+ '<<', '>>', '>>>',
242
+ '?', ':',
243
+ ',',
244
+ '.',
245
+ '=>',
246
+ ];
247
+
248
+ for (const op of continuationOps) {
249
+ if (lastLine.endsWith(op)) {
250
+ return true;
251
+ }
252
+ }
253
+
254
+ // Keywords that suggest continuation
255
+ const continuationKeywords = [
256
+ 'return', 'throw', 'new', 'typeof', 'void', 'delete',
257
+ 'await', 'yield', 'in', 'of', 'instanceof',
258
+ 'else', 'extends', 'implements',
259
+ ];
260
+
261
+ for (const kw of continuationKeywords) {
262
+ if (lastLine === kw || lastLine.endsWith(' ' + kw)) {
263
+ return true;
264
+ }
265
+ }
266
+
267
+ return false;
268
+ }
269
+
270
+ /**
271
+ * Find line comment start, accounting for strings
272
+ * @param {string} line
273
+ * @returns {number}
274
+ */
275
+ function findLineCommentStart(line) {
276
+ let inString = null;
277
+
278
+ for (let i = 0; i < line.length; i++) {
279
+ const char = line[i];
280
+ const prev = line[i - 1];
281
+
282
+ if (prev === '\\') continue;
283
+
284
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
285
+ inString = char;
286
+ } else if (inString === char) {
287
+ inString = null;
288
+ } else if (!inString && char === '/' && line[i + 1] === '/') {
289
+ return i;
290
+ }
291
+ }
292
+
293
+ return -1;
294
+ }
295
+
296
+ /**
297
+ * Check for incomplete template literal expressions
298
+ * @param {string} code
299
+ * @returns {boolean}
300
+ */
301
+ function hasIncompleteTemplate(code) {
302
+ let inTemplate = false;
303
+ let expressionDepth = 0;
304
+
305
+ for (let i = 0; i < code.length; i++) {
306
+ const char = code[i];
307
+ const prev = code[i - 1];
308
+ const next = code[i + 1];
309
+
310
+ if (prev === '\\') continue;
311
+
312
+ if (char === '`') {
313
+ if (inTemplate && expressionDepth === 0) {
314
+ inTemplate = false;
315
+ } else if (!inTemplate) {
316
+ inTemplate = true;
317
+ expressionDepth = 0;
318
+ }
319
+ } else if (inTemplate && char === '$' && next === '{') {
320
+ expressionDepth++;
321
+ i++;
322
+ } else if (inTemplate && expressionDepth > 0) {
323
+ if (char === '{') expressionDepth++;
324
+ else if (char === '}') expressionDepth--;
325
+ }
326
+ }
327
+
328
+ return inTemplate;
329
+ }
330
+
331
+ /**
332
+ * Try to parse the code to check for syntax errors
333
+ * @param {string} code
334
+ * @returns {IsCompleteResult}
335
+ */
336
+ function tryParse(code) {
337
+ try {
338
+ // Try to parse as a function body
339
+ new Function(code);
340
+ return { status: 'complete', indent: '' };
341
+ } catch (e) {
342
+ if (e instanceof SyntaxError) {
343
+ const msg = e.message.toLowerCase();
344
+
345
+ // Patterns that indicate incomplete code
346
+ const incompletePatterns = [
347
+ 'unexpected end',
348
+ 'unterminated',
349
+ 'expected',
350
+ 'missing',
351
+ ];
352
+
353
+ for (const pattern of incompletePatterns) {
354
+ if (msg.includes(pattern)) {
355
+ return { status: 'incomplete', indent: '' };
356
+ }
357
+ }
358
+
359
+ // Other syntax errors are invalid
360
+ return { status: 'invalid', indent: '' };
361
+ }
362
+
363
+ return { status: 'unknown', indent: '' };
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Get suggested indent for continuation
369
+ * @param {string} code
370
+ * @returns {string}
371
+ */
372
+ export function getSuggestedIndent(code) {
373
+ const lines = code.split('\n');
374
+ const lastLine = lines[lines.length - 1];
375
+
376
+ // Get current indent
377
+ const match = lastLine.match(/^(\s*)/);
378
+ const currentIndent = match ? match[1] : '';
379
+
380
+ // Check if we should increase indent
381
+ const trimmed = lastLine.trim();
382
+ const shouldIncrease =
383
+ trimmed.endsWith('{') ||
384
+ trimmed.endsWith('[') ||
385
+ trimmed.endsWith('(') ||
386
+ trimmed.endsWith(':') ||
387
+ trimmed.endsWith('=>');
388
+
389
+ if (shouldIncrease) {
390
+ return currentIndent + ' ';
391
+ }
392
+
393
+ return currentIndent;
394
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Constants
3
+ *
4
+ * Runtime constants for mrmd-js.
5
+ * @module constants
6
+ */
7
+
8
+ /** Runtime name */
9
+ export const RUNTIME_NAME = 'mrmd-js';
10
+
11
+ /** Runtime version */
12
+ export const RUNTIME_VERSION = '2.0.0';
13
+
14
+ /** Default session ID */
15
+ export const DEFAULT_SESSION = 'default';
16
+
17
+ /** Default max sessions */
18
+ export const DEFAULT_MAX_SESSIONS = 10;
19
+
20
+ /** Supported languages */
21
+ export const SUPPORTED_LANGUAGES = [
22
+ 'javascript',
23
+ 'js',
24
+ 'html',
25
+ 'htm',
26
+ 'css',
27
+ 'style',
28
+ ];
29
+
30
+ /** Default features */
31
+ export const DEFAULT_FEATURES = {
32
+ execute: true,
33
+ executeStream: true,
34
+ interrupt: false, // Limited in browser
35
+ complete: true,
36
+ inspect: true,
37
+ hover: true,
38
+ variables: true,
39
+ variableExpand: true,
40
+ reset: true,
41
+ isComplete: true,
42
+ format: true,
43
+ assets: true,
44
+ };
@@ -0,0 +1,205 @@
1
+ /**
2
+ * CSS Executor
3
+ *
4
+ * Executes CSS cells by producing displayData with text/css MIME type.
5
+ * Supports optional scoping to prevent style leakage.
6
+ *
7
+ * @module execute/css
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
+ * Generate a unique scope class name
21
+ * @param {string} [id] - Optional ID to include
22
+ * @returns {string}
23
+ */
24
+ export function generateScopeClass(id) {
25
+ const suffix = id
26
+ ? id.replace(/[^a-z0-9]/gi, '')
27
+ : Math.random().toString(36).slice(2, 8);
28
+ return `mrmd-scope-${suffix}`;
29
+ }
30
+
31
+ /**
32
+ * Scope CSS selectors by prefixing them with a scope selector
33
+ *
34
+ * @param {string} css - CSS content
35
+ * @param {string} scopeSelector - Scope selector (e.g., '.mrmd-scope-abc123')
36
+ * @returns {string} Scoped CSS
37
+ *
38
+ * @example
39
+ * scopeStyles('.card { color: red; }', '.scope-123')
40
+ * // Returns: '.scope-123 .card { color: red; }'
41
+ */
42
+ export function scopeStyles(css, scopeSelector) {
43
+ return css.replace(
44
+ /([^{}]+)\{/g,
45
+ (match, selectors) => {
46
+ const scoped = selectors
47
+ .split(',')
48
+ .map((selector) => {
49
+ const trimmed = selector.trim();
50
+
51
+ // Don't scope special selectors
52
+ if (
53
+ // @-rules (media, keyframes, supports, etc.)
54
+ trimmed.startsWith('@') ||
55
+ // Keyframe percentages and keywords
56
+ trimmed.startsWith('from') ||
57
+ trimmed.startsWith('to') ||
58
+ /^\d+%$/.test(trimmed) ||
59
+ // Empty selector
60
+ !trimmed
61
+ ) {
62
+ return trimmed;
63
+ }
64
+
65
+ // Handle :root specially - replace with scope selector
66
+ if (trimmed === ':root') {
67
+ return scopeSelector;
68
+ }
69
+
70
+ // Handle :host (for shadow DOM compatibility)
71
+ if (trimmed === ':host') {
72
+ return scopeSelector;
73
+ }
74
+
75
+ // Handle * selector
76
+ if (trimmed === '*') {
77
+ return `${scopeSelector} *`;
78
+ }
79
+
80
+ // Handle html/body - scope to container instead
81
+ if (trimmed === 'html' || trimmed === 'body') {
82
+ return scopeSelector;
83
+ }
84
+
85
+ // Prefix the selector
86
+ return `${scopeSelector} ${trimmed}`;
87
+ })
88
+ .join(', ');
89
+
90
+ return `${scoped} {`;
91
+ }
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Parse CSS to extract rule information
97
+ * @param {string} css
98
+ * @returns {{ rules: number, selectors: string[], variables: string[] }}
99
+ */
100
+ function parseCssInfo(css) {
101
+ const selectors = [];
102
+ const variables = [];
103
+
104
+ // Count rules (rough estimate by counting {)
105
+ const rules = (css.match(/\{/g) || []).length;
106
+
107
+ // Extract selectors (before {)
108
+ const selectorMatches = css.match(/([^{}]+)\{/g) || [];
109
+ for (const match of selectorMatches) {
110
+ const selector = match.replace('{', '').trim();
111
+ if (selector && !selector.startsWith('@')) {
112
+ selectors.push(...selector.split(',').map((s) => s.trim()));
113
+ }
114
+ }
115
+
116
+ // Extract CSS custom properties (--var-name)
117
+ const varMatches = css.match(/--[\w-]+/g) || [];
118
+ variables.push(...new Set(varMatches));
119
+
120
+ return { rules, selectors: selectors.slice(0, 10), variables: variables.slice(0, 10) };
121
+ }
122
+
123
+ /**
124
+ * CSS executor - produces displayData for CSS content
125
+ */
126
+ export class CssExecutor extends BaseExecutor {
127
+ /** @type {readonly string[]} */
128
+ languages = ['css', 'style', 'stylesheet'];
129
+
130
+ /**
131
+ * Execute CSS cell
132
+ * @param {string} code - CSS content
133
+ * @param {ExecutionContext} context - Execution context
134
+ * @param {ExecuteOptions} [options] - Execution options
135
+ * @returns {Promise<ExecutionResult>}
136
+ */
137
+ async execute(code, context, options = {}) {
138
+ const startTime = performance.now();
139
+
140
+ // Determine if scoping is requested
141
+ const shouldScope = options.cellMeta?.scoped ?? options.cellMeta?.scope ?? false;
142
+ const scopeId = options.execId || options.cellId || `css-${Date.now()}`;
143
+ const scopeClass = shouldScope ? generateScopeClass(scopeId) : undefined;
144
+
145
+ // Apply scoping if requested
146
+ const processedCss = scopeClass ? scopeStyles(code, `.${scopeClass}`) : code;
147
+
148
+ // Parse CSS for info
149
+ const info = parseCssInfo(code);
150
+
151
+ // Build display data
152
+ /** @type {DisplayData[]} */
153
+ const displayData = [
154
+ {
155
+ data: {
156
+ 'text/css': processedCss,
157
+ },
158
+ metadata: {
159
+ // Original CSS (before scoping)
160
+ original: code !== processedCss ? code : undefined,
161
+ // Scoping info
162
+ scoped: !!scopeClass,
163
+ scopeClass,
164
+ // CSS info
165
+ ruleCount: info.rules,
166
+ selectors: info.selectors,
167
+ customProperties: info.variables,
168
+ // Client hints
169
+ inject: options.cellMeta?.inject ?? true,
170
+ target: options.cellMeta?.target,
171
+ },
172
+ },
173
+ ];
174
+
175
+ const duration = performance.now() - startTime;
176
+
177
+ // Build info message
178
+ const parts = [`${info.rules} rule${info.rules !== 1 ? 's' : ''}`];
179
+ if (scopeClass) {
180
+ parts.push(`scoped to .${scopeClass}`);
181
+ }
182
+ if (info.variables.length > 0) {
183
+ parts.push(`${info.variables.length} variable${info.variables.length !== 1 ? 's' : ''}`);
184
+ }
185
+
186
+ return {
187
+ success: true,
188
+ stdout: `CSS: ${parts.join(', ')}`,
189
+ stderr: '',
190
+ result: undefined,
191
+ displayData,
192
+ assets: [],
193
+ executionCount: 0,
194
+ duration,
195
+ };
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Create a CSS executor
201
+ * @returns {CssExecutor}
202
+ */
203
+ export function createCssExecutor() {
204
+ return new CssExecutor();
205
+ }