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,149 @@
1
+ /**
2
+ * CSS Applicator
3
+ *
4
+ * Utility for applying CSS displayData from cell execution.
5
+ * Manages stylesheet lifecycle and scoping.
6
+ *
7
+ * @module utils/css-applicator
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} ApplyOptions
12
+ * @property {string} [id] - Style element ID for updates
13
+ * @property {string} [scope] - Scope selector to prefix rules
14
+ * @property {boolean} [append=false] - Append instead of replace
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} ApplyResult
19
+ * @property {HTMLStyleElement} element - The style element
20
+ * @property {string} id - Style element ID
21
+ * @property {boolean} replaced - Whether an existing style was replaced
22
+ */
23
+
24
+ import { scopeStyles } from './html-renderer.js';
25
+
26
+ /**
27
+ * CSS Applicator class
28
+ */
29
+ export class CssApplicator {
30
+ /** @type {Map<string, HTMLStyleElement>} */
31
+ #styles = new Map();
32
+
33
+ /** @type {HTMLElement} */
34
+ #container;
35
+
36
+ /**
37
+ * Create CSS applicator
38
+ * @param {HTMLElement} [container=document.head] - Container for style elements
39
+ */
40
+ constructor(container) {
41
+ this.#container = container ?? document.head;
42
+ }
43
+
44
+ /**
45
+ * Apply CSS string
46
+ *
47
+ * @param {string} css - CSS string
48
+ * @param {ApplyOptions} [options]
49
+ * @returns {ApplyResult}
50
+ */
51
+ apply(css, options = {}) {
52
+ const id = options.id ?? `mrmd-style-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
53
+
54
+ // Scope CSS if requested
55
+ let processedCss = css;
56
+ if (options.scope) {
57
+ processedCss = scopeStyles(css, options.scope);
58
+ }
59
+
60
+ // Check for existing style with same ID
61
+ let element = this.#styles.get(id);
62
+ let replaced = false;
63
+
64
+ if (element) {
65
+ if (options.append) {
66
+ element.textContent += '\n' + processedCss;
67
+ } else {
68
+ element.textContent = processedCss;
69
+ replaced = true;
70
+ }
71
+ } else {
72
+ element = document.createElement('style');
73
+ element.id = id;
74
+ element.textContent = processedCss;
75
+ this.#container.appendChild(element);
76
+ this.#styles.set(id, element);
77
+ }
78
+
79
+ return { element, id, replaced };
80
+ }
81
+
82
+ /**
83
+ * Apply CSS from displayData
84
+ *
85
+ * @param {import('../types/execution.js').DisplayData} displayData
86
+ * @param {ApplyOptions} [options]
87
+ * @returns {ApplyResult | null}
88
+ */
89
+ applyDisplayData(displayData, options = {}) {
90
+ const css = displayData.data['text/css'];
91
+ if (!css) return null;
92
+
93
+ // Use metadata for options if available
94
+ const id = options.id ?? (displayData.metadata?.id ? String(displayData.metadata.id) : undefined);
95
+ const scope = options.scope ?? (displayData.metadata?.scope ? String(displayData.metadata.scope) : undefined);
96
+
97
+ return this.apply(css, { ...options, id, scope });
98
+ }
99
+
100
+ /**
101
+ * Remove a style by ID
102
+ * @param {string} id
103
+ * @returns {boolean}
104
+ */
105
+ remove(id) {
106
+ const element = this.#styles.get(id);
107
+ if (!element) return false;
108
+
109
+ element.remove();
110
+ this.#styles.delete(id);
111
+ return true;
112
+ }
113
+
114
+ /**
115
+ * Remove all managed styles
116
+ */
117
+ clear() {
118
+ for (const element of this.#styles.values()) {
119
+ element.remove();
120
+ }
121
+ this.#styles.clear();
122
+ }
123
+
124
+ /**
125
+ * Get all managed style IDs
126
+ * @returns {string[]}
127
+ */
128
+ list() {
129
+ return Array.from(this.#styles.keys());
130
+ }
131
+
132
+ /**
133
+ * Get a style element by ID
134
+ * @param {string} id
135
+ * @returns {HTMLStyleElement | undefined}
136
+ */
137
+ get(id) {
138
+ return this.#styles.get(id);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Create a CSS applicator
144
+ * @param {HTMLElement} [container]
145
+ * @returns {CssApplicator}
146
+ */
147
+ export function createCssApplicator(container) {
148
+ return new CssApplicator(container);
149
+ }
@@ -0,0 +1,355 @@
1
+ /**
2
+ * HTML Renderer
3
+ *
4
+ * Utility for rendering HTML displayData from cell execution.
5
+ * Provides three rendering modes: direct, shadow, and scoped.
6
+ *
7
+ * @module utils/html-renderer
8
+ */
9
+
10
+ /**
11
+ * @typedef {'direct' | 'shadow' | 'scoped'} RenderMode
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} RenderOptions
16
+ * @property {RenderMode} [mode='direct'] - Rendering mode
17
+ * @property {string} [scopeClass] - Scope class for 'scoped' mode
18
+ * @property {boolean} [executeScripts=true] - Execute inline scripts
19
+ * @property {(error: Error, script: string) => void} [onScriptError] - Script error callback
20
+ * @property {boolean} [clear=true] - Clear container before rendering
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} RenderResult
25
+ * @property {HTMLElement} container - Container element
26
+ * @property {ShadowRoot} [shadowRoot] - Shadow root if shadow mode
27
+ * @property {number} scriptsExecuted - Number of scripts executed
28
+ * @property {Error[]} scriptErrors - Script errors
29
+ */
30
+
31
+ /** @type {Set<string>} */
32
+ const executedScripts = new Set();
33
+
34
+ /**
35
+ * HTML Renderer class for rendering displayData
36
+ */
37
+ export class HtmlRenderer {
38
+ /** @type {Map<string, Set<string>>} */
39
+ #scriptHashes = new Map();
40
+
41
+ /**
42
+ * Render HTML string into a container
43
+ *
44
+ * @param {string} html - HTML string to render
45
+ * @param {HTMLElement} container - Target container
46
+ * @param {RenderOptions} [options]
47
+ * @returns {RenderResult}
48
+ */
49
+ render(html, container, options = {}) {
50
+ const mode = options.mode ?? 'direct';
51
+
52
+ switch (mode) {
53
+ case 'shadow':
54
+ return this.#renderShadow(html, container, options);
55
+ case 'scoped':
56
+ return this.#renderScoped(html, container, options);
57
+ case 'direct':
58
+ default:
59
+ return this.#renderDirect(html, container, options);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Render displayData into container
65
+ *
66
+ * @param {import('../types/execution.js').DisplayData} displayData
67
+ * @param {HTMLElement} container
68
+ * @param {RenderOptions} [options]
69
+ * @returns {RenderResult}
70
+ */
71
+ renderDisplayData(displayData, container, options = {}) {
72
+ const html = displayData.data['text/html'];
73
+ if (!html) {
74
+ return {
75
+ container,
76
+ scriptsExecuted: 0,
77
+ scriptErrors: [],
78
+ };
79
+ }
80
+
81
+ // Use scopeClass from metadata if available
82
+ const scopeClass = options.scopeClass ?? displayData.metadata?.scopeClass;
83
+
84
+ return this.render(html, container, {
85
+ ...options,
86
+ scopeClass: typeof scopeClass === 'string' ? scopeClass : undefined,
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Render in direct mode (no isolation)
92
+ * @param {string} html
93
+ * @param {HTMLElement} container
94
+ * @param {RenderOptions} options
95
+ * @returns {RenderResult}
96
+ */
97
+ #renderDirect(html, container, options) {
98
+ if (options.clear !== false) {
99
+ container.innerHTML = '';
100
+ }
101
+
102
+ // Extract scripts before setting innerHTML
103
+ const { content, scripts } = this.#extractScripts(html);
104
+
105
+ // Append content
106
+ const temp = document.createElement('div');
107
+ temp.innerHTML = content;
108
+ while (temp.firstChild) {
109
+ container.appendChild(temp.firstChild);
110
+ }
111
+
112
+ // Execute scripts
113
+ const { executed, errors } = this.#executeScripts(scripts, container, options);
114
+
115
+ return {
116
+ container,
117
+ scriptsExecuted: executed,
118
+ scriptErrors: errors,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Render in shadow mode (full isolation via Shadow DOM)
124
+ * @param {string} html
125
+ * @param {HTMLElement} container
126
+ * @param {RenderOptions} options
127
+ * @returns {RenderResult}
128
+ */
129
+ #renderShadow(html, container, options) {
130
+ // Create or reuse shadow root
131
+ let shadowRoot = container.shadowRoot;
132
+ if (!shadowRoot) {
133
+ shadowRoot = container.attachShadow({ mode: 'open' });
134
+ }
135
+
136
+ if (options.clear !== false) {
137
+ shadowRoot.innerHTML = '';
138
+ }
139
+
140
+ // Extract scripts
141
+ const { content, scripts } = this.#extractScripts(html);
142
+
143
+ // Set content
144
+ const temp = document.createElement('div');
145
+ temp.innerHTML = content;
146
+ while (temp.firstChild) {
147
+ shadowRoot.appendChild(temp.firstChild);
148
+ }
149
+
150
+ // Execute scripts in shadow context
151
+ const { executed, errors } = this.#executeScripts(scripts, shadowRoot, options);
152
+
153
+ return {
154
+ container,
155
+ shadowRoot,
156
+ scriptsExecuted: executed,
157
+ scriptErrors: errors,
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Render in scoped mode (CSS isolation via class prefixing)
163
+ * @param {string} html
164
+ * @param {HTMLElement} container
165
+ * @param {RenderOptions} options
166
+ * @returns {RenderResult}
167
+ */
168
+ #renderScoped(html, container, options) {
169
+ const scopeClass = options.scopeClass ?? `mrmd-scope-${Date.now()}`;
170
+
171
+ // Add scope class to container
172
+ container.classList.add(scopeClass);
173
+
174
+ if (options.clear !== false) {
175
+ container.innerHTML = '';
176
+ }
177
+
178
+ // Extract scripts and styles
179
+ const { content, scripts, styles } = this.#extractScriptsAndStyles(html);
180
+
181
+ // Scope and append styles
182
+ for (const style of styles) {
183
+ const scopedCss = scopeStyles(style, `.${scopeClass}`);
184
+ const styleEl = document.createElement('style');
185
+ styleEl.textContent = scopedCss;
186
+ container.appendChild(styleEl);
187
+ }
188
+
189
+ // Append content
190
+ const temp = document.createElement('div');
191
+ temp.innerHTML = content;
192
+ while (temp.firstChild) {
193
+ container.appendChild(temp.firstChild);
194
+ }
195
+
196
+ // Execute scripts
197
+ const { executed, errors } = this.#executeScripts(scripts, container, options);
198
+
199
+ return {
200
+ container,
201
+ scriptsExecuted: executed,
202
+ scriptErrors: errors,
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Extract scripts from HTML
208
+ * @param {string} html
209
+ * @returns {{ content: string, scripts: string[] }}
210
+ */
211
+ #extractScripts(html) {
212
+ const scripts = [];
213
+ const content = html.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, (match, code) => {
214
+ scripts.push(code.trim());
215
+ return '';
216
+ });
217
+ return { content, scripts };
218
+ }
219
+
220
+ /**
221
+ * Extract scripts and styles from HTML
222
+ * @param {string} html
223
+ * @returns {{ content: string, scripts: string[], styles: string[] }}
224
+ */
225
+ #extractScriptsAndStyles(html) {
226
+ const scripts = [];
227
+ const styles = [];
228
+
229
+ let content = html.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, (match, code) => {
230
+ scripts.push(code.trim());
231
+ return '';
232
+ });
233
+
234
+ content = content.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi, (match, css) => {
235
+ styles.push(css.trim());
236
+ return '';
237
+ });
238
+
239
+ return { content, scripts, styles };
240
+ }
241
+
242
+ /**
243
+ * Execute scripts
244
+ * @param {string[]} scripts
245
+ * @param {HTMLElement | ShadowRoot} context
246
+ * @param {RenderOptions} options
247
+ * @returns {{ executed: number, errors: Error[] }}
248
+ */
249
+ #executeScripts(scripts, context, options) {
250
+ if (options.executeScripts === false) {
251
+ return { executed: 0, errors: [] };
252
+ }
253
+
254
+ let executed = 0;
255
+ const errors = [];
256
+
257
+ for (const code of scripts) {
258
+ if (!code) continue;
259
+
260
+ // Skip already executed scripts (deduplication by content hash)
261
+ const hash = this.#hashCode(code);
262
+ if (executedScripts.has(hash)) {
263
+ continue;
264
+ }
265
+ executedScripts.add(hash);
266
+
267
+ try {
268
+ // Create script element for proper execution
269
+ const script = document.createElement('script');
270
+ script.textContent = code;
271
+
272
+ // Add to document to execute
273
+ if (context instanceof ShadowRoot) {
274
+ context.appendChild(script);
275
+ } else {
276
+ context.appendChild(script);
277
+ }
278
+
279
+ executed++;
280
+ } catch (err) {
281
+ const error = err instanceof Error ? err : new Error(String(err));
282
+ errors.push(error);
283
+ options.onScriptError?.(error, code);
284
+ }
285
+ }
286
+
287
+ return { executed, errors };
288
+ }
289
+
290
+ /**
291
+ * Simple hash function for deduplication
292
+ * @param {string} str
293
+ * @returns {string}
294
+ */
295
+ #hashCode(str) {
296
+ let hash = 0;
297
+ for (let i = 0; i < str.length; i++) {
298
+ const char = str.charCodeAt(i);
299
+ hash = ((hash << 5) - hash) + char;
300
+ hash = hash & hash;
301
+ }
302
+ return hash.toString(36);
303
+ }
304
+
305
+ /**
306
+ * Clear executed scripts cache (for re-execution)
307
+ * @param {string} [execId] - Clear specific execution, or all if not provided
308
+ */
309
+ clearScripts(execId) {
310
+ if (execId) {
311
+ this.#scriptHashes.delete(execId);
312
+ } else {
313
+ this.#scriptHashes.clear();
314
+ executedScripts.clear();
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Scope CSS selectors with a prefix
321
+ * @param {string} css
322
+ * @param {string} scopeSelector
323
+ * @returns {string}
324
+ */
325
+ export function scopeStyles(css, scopeSelector) {
326
+ // Simple CSS scoping - prefix each selector
327
+ // This is a basic implementation; a full parser would be more robust
328
+
329
+ return css.replace(
330
+ /([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)/g,
331
+ (match, selector, suffix) => {
332
+ // Don't scope @-rules
333
+ if (selector.trim().startsWith('@')) {
334
+ return match;
335
+ }
336
+
337
+ // Don't scope :root, html, body
338
+ const trimmed = selector.trim();
339
+ if (trimmed === ':root' || trimmed === 'html' || trimmed === 'body') {
340
+ return `${scopeSelector}${suffix}`;
341
+ }
342
+
343
+ // Scope the selector
344
+ return `${scopeSelector} ${selector.trim()}${suffix}`;
345
+ }
346
+ );
347
+ }
348
+
349
+ /**
350
+ * Create a new HTML renderer
351
+ * @returns {HtmlRenderer}
352
+ */
353
+ export function createHtmlRenderer() {
354
+ return new HtmlRenderer();
355
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Client Utilities
3
+ *
4
+ * Utilities for rendering execution output in browser environments.
5
+ *
6
+ * @module utils
7
+ */
8
+
9
+ export {
10
+ HtmlRenderer,
11
+ createHtmlRenderer,
12
+ scopeStyles,
13
+ } from './html-renderer.js';
14
+
15
+ export {
16
+ CssApplicator,
17
+ createCssApplicator,
18
+ } from './css-applicator.js';
19
+
20
+ export {
21
+ AnsiRenderer,
22
+ ansiToHtml,
23
+ stripAnsi,
24
+ createAnsiRenderer,
25
+ } from './ansi-renderer.js';