mrmd-editor 0.5.0 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mrmd-editor",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Markdown editor with realtime collaboration - the core editor package",
5
5
  "type": "module",
6
6
  "main": "dist/mrmd.cjs",
@@ -389,11 +389,12 @@ export function normalizeOptions(options = {}) {
389
389
  if (options.javascript === true) {
390
390
  config.runtimes.javascript = { type: 'builtin' };
391
391
  } else if (options.javascript === false) {
392
- // Explicitly disabled
392
+ // Explicitly disabled (even if provided via options.runtimes)
393
+ delete config.runtimes.javascript;
393
394
  } else if (options.javascript && typeof options.javascript === 'object') {
394
395
  config.runtimes.javascript = { type: 'custom', instance: options.javascript };
395
- } else if (options.javascript === undefined) {
396
- // Default: enable builtin
396
+ } else if (options.javascript === undefined && !config.runtimes.javascript) {
397
+ // Default: enable builtin only when no JS runtime was already provided
397
398
  config.runtimes.javascript = { type: 'builtin' };
398
399
  }
399
400
 
package/src/execution.js CHANGED
@@ -17,6 +17,37 @@ import { pendingStdinRequests } from './output-widget.js';
17
17
  import { MonitorCoordination, EXECUTION_STATUS } from './monitor-coordination.js';
18
18
  import * as Y from 'yjs';
19
19
 
20
+ const STATIC_IMPORT_ERROR_RE = /import declarations may only appear at top level of a module/i;
21
+ const UNDEFINED_VAR_RE = /\bis not defined\b/i;
22
+
23
+ function buildJavaScriptImportHint(errorText, runtimeLanguage) {
24
+ const language = String(runtimeLanguage || '').toLowerCase();
25
+ const isJavaScriptLike = language === 'javascript' || language === 'js' || language === 'node' || language === 'typescript' || language === 'ts';
26
+ const text = String(errorText || '');
27
+ if (!isJavaScriptLike) {
28
+ return '';
29
+ }
30
+
31
+ if (STATIC_IMPORT_ERROR_RE.test(text)) {
32
+ return [
33
+ '',
34
+ '[Hint: JavaScript cells run as scripts (eval), not ES modules.]',
35
+ "[Use dynamic import: const pkg = await import('https://cdn.skypack.dev/lodash')]",
36
+ '[For a cached helper, open Help > JavaScript Imports and copy loadPkg().]',
37
+ ].join('\n');
38
+ }
39
+
40
+ if (UNDEFINED_VAR_RE.test(text)) {
41
+ return [
42
+ '',
43
+ '[Hint: For cross-cell JS variables, bind imports to globals.]',
44
+ "[Example: d3 = await globalThis.loadPkg('d3', 'https://cdn.skypack.dev/d3@7')]",
45
+ ].join('\n');
46
+ }
47
+
48
+ return '';
49
+ }
50
+
20
51
  /**
21
52
  * Execution Manager
22
53
  *
@@ -735,7 +766,17 @@ export class ExecutionManager {
735
766
  if (m === 'mermaid') return 'html'; // Mermaid renders to HTML/SVG
736
767
  return m;
737
768
  }) : null;
738
- const outputTag = outputType ? `output:${execId}:${outputType}` : `output:${execId}`;
769
+ let outputTag;
770
+ if (!outputType) {
771
+ outputTag = `output:${execId}`;
772
+ } else {
773
+ const outputTagParts = [`output:${execId}`, outputType];
774
+ if (outputType === 'css') {
775
+ // Let CSS widgets scope selector matching to where this CSS actually targets.
776
+ outputTagParts.push(artifact && artifactName ? 'artifact' : 'main');
777
+ }
778
+ outputTag = outputTagParts.join(':');
779
+ }
739
780
 
740
781
  if (existingOutput) {
741
782
  // Replace existing output block with new one that has our execId
@@ -983,8 +1024,10 @@ export class ExecutionManager {
983
1024
  // Handle errors
984
1025
  let outputWithError = finalOutput;
985
1026
  if (result.error) {
986
- const errorText = `\n[Error: ${result.error.message || result.stderr}]`;
987
- outputWithError = finalOutput + errorText;
1027
+ const rawError = result.error.message || result.stderr || String(result.error);
1028
+ const errorText = `\n[Error: ${rawError}]`;
1029
+ const runtimeHint = buildJavaScriptImportHint(rawError, runtimeLanguage);
1030
+ outputWithError = finalOutput + errorText + runtimeHint;
988
1031
  this._emit('cellError', index, result.error, execId);
989
1032
  }
990
1033
 
package/src/index.js CHANGED
@@ -407,6 +407,9 @@ function createJavaScriptRuntime(options = {}) {
407
407
  // Prefer HTML representation
408
408
  if (display.data['text/html']) {
409
409
  output = display.data['text/html'];
410
+ } else if (display.data['text/css']) {
411
+ // Preserve CSS source so the output widget can show selector impact
412
+ output = display.data['text/css'];
410
413
  } else if (display.data['text/plain']) {
411
414
  output = display.data['text/plain'];
412
415
  }
@@ -507,12 +510,14 @@ function createJavaScriptRuntime(options = {}) {
507
510
  },
508
511
 
509
512
  /**
510
- * List all variables in the session namespace.
513
+ * List all variables in a session namespace.
511
514
  *
515
+ * @param {Object} [filter]
516
+ * @param {string} [sessionName='default']
512
517
  * @returns {Array<{name: string, type: string, value: string, expandable?: boolean}>}
513
518
  */
514
- listVariables() {
515
- return defaultSession.listVariables();
519
+ listVariables(filter = {}, sessionName = 'default') {
520
+ return getOrCreateSession(sessionName).listVariables(filter);
516
521
  },
517
522
 
518
523
  /**
@@ -823,8 +828,6 @@ function create(target, options = {}) {
823
828
  // MRP and builtin types are handled separately below
824
829
  }
825
830
 
826
- // JavaScript runtime option (backward compat)
827
- const javascript = options.javascript !== undefined ? options.javascript : true;
828
831
  // JavaScript isolation mode: 'iframe' (default, sandboxed) or 'none' (main window context)
829
832
  const javascriptIsolation = options.javascriptIsolation || 'iframe';
830
833
 
@@ -1021,6 +1024,7 @@ function create(target, options = {}) {
1021
1024
  // Event handlers
1022
1025
  const changeHandlers = [];
1023
1026
  const saveHandlers = [];
1027
+ const frontmatterTitleCommitHandlers = [];
1024
1028
  const viewSourceHandlers = [];
1025
1029
  const cellRunHandlers = [];
1026
1030
  const cellOutputHandlers = [];
@@ -1036,6 +1040,19 @@ function create(target, options = {}) {
1036
1040
  }
1037
1041
  });
1038
1042
 
1043
+ const handleFrontmatterTitleCommit = (event) => {
1044
+ const title = event?.detail?.title;
1045
+ if (!title) return;
1046
+ for (const handler of frontmatterTitleCommitHandlers) {
1047
+ try {
1048
+ handler(title, event.detail || {});
1049
+ } catch (err) {
1050
+ console.warn('[mrmd] frontmatter title commit handler failed:', err);
1051
+ }
1052
+ }
1053
+ };
1054
+ element.addEventListener('mrmd:frontmatter-title-commit', handleFrontmatterTitleCommit);
1055
+
1039
1056
  // Create runtime registry
1040
1057
  const registry = createRuntimeRegistry();
1041
1058
 
@@ -1044,19 +1061,17 @@ function create(target, options = {}) {
1044
1061
  registry.register(name, runtime);
1045
1062
  }
1046
1063
 
1047
- // Built-in JavaScript runtime (mrmd-js)
1048
- // - true: use built-in mrmd-js runtime (default)
1049
- // - false: disable JavaScript execution
1050
- // - object: use custom runtime (must implement supports/execute/executeStreaming)
1064
+ // JavaScript runtime for editor + LSP features.
1065
+ // Use normalized config so options.runtimes.javascript is not shadowed by a second runtime.
1051
1066
  let jsRuntime = null;
1052
- if (javascript === true) {
1067
+ const jsRuntimeConfig = config.runtimes.javascript;
1068
+ if (jsRuntimeConfig?.type === 'builtin') {
1053
1069
  jsRuntime = createJavaScriptRuntime({ defaultIsolation: javascriptIsolation });
1054
1070
  registry.register('javascript', jsRuntime);
1055
- } else if (javascript && typeof javascript === 'object') {
1056
- // Custom runtime provided
1057
- registry.register('javascript', javascript);
1071
+ } else if (jsRuntimeConfig?.type === 'custom' && jsRuntimeConfig.instance) {
1072
+ jsRuntime = jsRuntimeConfig.instance;
1058
1073
  }
1059
- // javascript === false means no JS runtime
1074
+ // If javascript is disabled, jsRuntime remains null.
1060
1075
 
1061
1076
  // =========================================================================
1062
1077
  // RUNTIME LSP PROVIDERS
@@ -1066,7 +1081,7 @@ function create(target, options = {}) {
1066
1081
  const runtimeLspProviders = new Map();
1067
1082
 
1068
1083
  // Add JS runtime LSP provider if available
1069
- if (jsRuntime) {
1084
+ if (jsRuntime?.getLSPProvider) {
1070
1085
  const jsProvider = jsRuntime.getLSPProvider();
1071
1086
  runtimeLspProviders.set('javascript', jsProvider);
1072
1087
  }
@@ -2106,6 +2121,14 @@ function create(target, options = {}) {
2106
2121
  };
2107
2122
  },
2108
2123
 
2124
+ onFrontmatterTitleCommit(callback) {
2125
+ frontmatterTitleCommitHandlers.push(callback);
2126
+ return () => {
2127
+ const idx = frontmatterTitleCommitHandlers.indexOf(callback);
2128
+ if (idx >= 0) frontmatterTitleCommitHandlers.splice(idx, 1);
2129
+ };
2130
+ },
2131
+
2109
2132
  onCellRun(callback) {
2110
2133
  return this.execution.on('cellRun', callback);
2111
2134
  },
@@ -2179,6 +2202,7 @@ function create(target, options = {}) {
2179
2202
  }
2180
2203
  // Clean up theme watcher
2181
2204
  unwatchTheme();
2205
+ element.removeEventListener('mrmd:frontmatter-title-commit', handleFrontmatterTitleCommit);
2182
2206
  // Clean up undo manager
2183
2207
  undoManager.destroy();
2184
2208
  view.destroy();
@@ -64,6 +64,9 @@ import {
64
64
  extractDisplayMath,
65
65
  generateMathId,
66
66
  } from './widgets/math.js';
67
+ import {
68
+ FrontmatterWidget,
69
+ } from './widgets/frontmatter.js';
67
70
 
68
71
  // =============================================================================
69
72
  // Height Cache for Stable Layout
@@ -370,6 +373,64 @@ function findDisplayMathRanges(state) {
370
373
  return ranges;
371
374
  }
372
375
 
376
+ /**
377
+ * FrontmatterWidget wrapper that caches its rendered height for stable layout.
378
+ */
379
+ class FrontmatterWidgetWithHeightCache extends FrontmatterWidget {
380
+ constructor(yamlContent, contentHash, sourceFrom, sourceTo) {
381
+ super(yamlContent, contentHash, sourceFrom, sourceTo);
382
+ }
383
+
384
+ toDOM(view) {
385
+ const dom = super.toDOM(view);
386
+ const contentHash = this.contentHash;
387
+
388
+ requestAnimationFrame(() => {
389
+ const line = dom.closest('.cm-line');
390
+ const height = line ? line.offsetHeight : dom.offsetHeight;
391
+ if (height > 0) {
392
+ cacheWidgetHeight(contentHash, height);
393
+ }
394
+ });
395
+
396
+ return dom;
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Find frontmatter range at the start of the document (--- ... ---)
402
+ */
403
+ function findFrontmatterRange(state) {
404
+ const doc = state.doc;
405
+ if (doc.lines < 2) return null;
406
+
407
+ const firstLine = doc.line(1);
408
+ if (firstLine.text.trim() !== '---') return null;
409
+
410
+ // Find closing ---
411
+ for (let i = 2; i <= doc.lines; i++) {
412
+ const line = doc.line(i);
413
+ if (line.text.trim() === '---') {
414
+ // YAML content is between line 2 and line i-1
415
+ const yamlLines = [];
416
+ for (let j = 2; j < i; j++) {
417
+ yamlLines.push(doc.line(j).text);
418
+ }
419
+ return {
420
+ type: 'frontmatter',
421
+ from: firstLine.from,
422
+ to: line.to,
423
+ startLine: 1,
424
+ endLine: i,
425
+ content: yamlLines.join('\n'),
426
+ };
427
+ }
428
+ // If we hit a line that looks like content (not YAML), stop
429
+ // Frontmatter can't contain blank lines followed by markdown
430
+ }
431
+ return null;
432
+ }
433
+
373
434
  /**
374
435
  * Build decorations for all block elements
375
436
  */
@@ -471,6 +532,48 @@ function buildBlockDecorations(state) {
471
532
  }
472
533
  }
473
534
 
535
+ // Find and process frontmatter
536
+ const fmRange = findFrontmatterRange(state);
537
+
538
+ if (fmRange) {
539
+ const cursorInFrontmatter = cursorLine >= fmRange.startLine && cursorLine <= fmRange.endLine;
540
+ const contentHash = 'fm-' + hashContent(fmRange.content);
541
+
542
+ if (!cursorInFrontmatter) {
543
+ decorations.push(
544
+ Decoration.replace({
545
+ widget: new FrontmatterWidgetWithHeightCache(
546
+ fmRange.content,
547
+ contentHash,
548
+ fmRange.from,
549
+ fmRange.to
550
+ ),
551
+ }).range(fmRange.from, fmRange.to)
552
+ );
553
+ } else {
554
+ // Cursor inside: show raw YAML with spacer for stable height
555
+ const cachedHeight = getCachedHeight(contentHash);
556
+ if (cachedHeight) {
557
+ const lineCount = fmRange.endLine - fmRange.startLine + 1;
558
+ const lineHeight = getLineHeight();
559
+ const rawHeight = lineCount * lineHeight;
560
+ const padding = cachedHeight - rawHeight;
561
+
562
+ if (padding > 0) {
563
+ const lastLine = doc.line(fmRange.endLine);
564
+ decorations.push(
565
+ Decoration.line({
566
+ attributes: {
567
+ class: 'cm-block-spacer-line',
568
+ style: `padding-bottom: ${padding}px`
569
+ }
570
+ }).range(lastLine.from)
571
+ );
572
+ }
573
+ }
574
+ }
575
+ }
576
+
474
577
  return Decoration.set(decorations, true);
475
578
  }
476
579