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.
- package/README.md +842 -0
- package/dist/index.cjs +7613 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +7530 -0
- package/dist/index.js.map +1 -0
- package/dist/mrmd-js.iife.js +7618 -0
- package/dist/mrmd-js.iife.js.map +1 -0
- package/package.json +47 -0
- package/src/analysis/format.js +371 -0
- package/src/analysis/index.js +18 -0
- package/src/analysis/is-complete.js +394 -0
- package/src/constants.js +44 -0
- package/src/execute/css.js +205 -0
- package/src/execute/html.js +162 -0
- package/src/execute/index.js +41 -0
- package/src/execute/interface.js +144 -0
- package/src/execute/javascript.js +197 -0
- package/src/execute/registry.js +245 -0
- package/src/index.js +136 -0
- package/src/lsp/complete.js +353 -0
- package/src/lsp/format.js +310 -0
- package/src/lsp/hover.js +126 -0
- package/src/lsp/index.js +55 -0
- package/src/lsp/inspect.js +466 -0
- package/src/lsp/parse.js +455 -0
- package/src/lsp/variables.js +283 -0
- package/src/runtime.js +518 -0
- package/src/session/console-capture.js +181 -0
- package/src/session/context/iframe.js +407 -0
- package/src/session/context/index.js +12 -0
- package/src/session/context/interface.js +38 -0
- package/src/session/context/main.js +357 -0
- package/src/session/index.js +16 -0
- package/src/session/manager.js +327 -0
- package/src/session/session.js +678 -0
- package/src/transform/async.js +133 -0
- package/src/transform/extract.js +251 -0
- package/src/transform/index.js +10 -0
- package/src/transform/persistence.js +176 -0
- package/src/types/analysis.js +24 -0
- package/src/types/capabilities.js +44 -0
- package/src/types/completion.js +47 -0
- package/src/types/execution.js +62 -0
- package/src/types/index.js +16 -0
- package/src/types/inspection.js +39 -0
- package/src/types/session.js +32 -0
- package/src/types/streaming.js +74 -0
- package/src/types/variables.js +54 -0
- package/src/utils/ansi-renderer.js +301 -0
- package/src/utils/css-applicator.js +149 -0
- package/src/utils/html-renderer.js +355 -0
- 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
|
+
}
|
package/src/constants.js
ADDED
|
@@ -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
|
+
}
|