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 +1 -1
- package/src/config/schema.js +4 -3
- package/src/execution.js +46 -3
- package/src/index.js +39 -15
- package/src/markdown/block-decorations.js +103 -0
- package/src/markdown/widgets/frontmatter.js +438 -0
- package/src/output-widget.js +764 -123
package/package.json
CHANGED
package/src/config/schema.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
987
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
1048
|
-
//
|
|
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
|
-
|
|
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 (
|
|
1056
|
-
|
|
1057
|
-
registry.register('javascript', javascript);
|
|
1071
|
+
} else if (jsRuntimeConfig?.type === 'custom' && jsRuntimeConfig.instance) {
|
|
1072
|
+
jsRuntime = jsRuntimeConfig.instance;
|
|
1058
1073
|
}
|
|
1059
|
-
// javascript
|
|
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
|
|