mrmd-editor 0.6.0 → 0.7.1

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/src/index.js CHANGED
@@ -304,55 +304,55 @@ function parseProgress(output) {
304
304
  function createJavaScriptRuntime(options = {}) {
305
305
  const rt = createMrmdJsRuntime(options);
306
306
 
307
- // Track named sessions - each can have different isolation
308
- // Session naming:
309
- // null/undefined/'default'/'main'/'none' → main context (no isolation)
307
+ // Track named execution contexts - each can have different isolation
308
+ // Context naming:
309
+ // null/undefined/'default'/'main'/'none' → configured default isolation
310
310
  // 'sandbox'/'iframe' → sandboxed iframe
311
311
  // other names → sandboxed iframe with separate scope
312
- const sessions = new Map();
312
+ const contexts = new Map();
313
313
  const defaultIsolation = options.defaultIsolation || 'iframe';
314
314
 
315
315
  /**
316
- * Get or create a session by name
317
- * @param {string|null} sessionName
316
+ * Get or create a context by name
317
+ * @param {string|null} contextName
318
318
  * @returns {Session}
319
319
  */
320
- function getOrCreateSession(sessionName) {
321
- // Normalize session name
322
- const name = sessionName || 'default';
320
+ function getOrCreateContext(contextName) {
321
+ // Normalize context name
322
+ const name = contextName || 'default';
323
323
 
324
- // Return existing session
325
- if (sessions.has(name)) {
326
- return sessions.get(name);
324
+ // Return existing context
325
+ if (contexts.has(name)) {
326
+ return contexts.get(name);
327
327
  }
328
328
 
329
- // Determine isolation mode based on session name
329
+ // Determine isolation mode based on context name
330
330
  let isolation;
331
- if (!sessionName || sessionName === 'default' || sessionName === 'main' || sessionName === 'none') {
332
- // Default session uses the configured default isolation
331
+ if (!contextName || contextName === 'default' || contextName === 'main' || contextName === 'none') {
332
+ // Default context uses the configured default isolation
333
333
  isolation = defaultIsolation;
334
- } else if (sessionName === 'sandbox' || sessionName === 'iframe') {
334
+ } else if (contextName === 'sandbox' || contextName === 'iframe') {
335
335
  // Explicit sandbox request
336
336
  isolation = 'iframe';
337
337
  } else {
338
- // Named sessions are sandboxed by default (separate scope)
338
+ // Named contexts are sandboxed by default (separate scope)
339
339
  isolation = 'iframe';
340
340
  }
341
341
 
342
- // Create new session with appropriate isolation
343
- const session = rt.createSession({
342
+ // Create new execution context with appropriate isolation
343
+ const context = rt.createSession({
344
344
  language: 'javascript',
345
345
  isolation,
346
346
  id: name,
347
347
  });
348
348
 
349
- sessions.set(name, session);
350
- console.log(`[JS Runtime] Created session '${name}' with isolation: ${isolation}`);
351
- return session;
349
+ contexts.set(name, context);
350
+ console.log(`[JS Runtime] Created context '${name}' with isolation: ${isolation}`);
351
+ return context;
352
352
  }
353
353
 
354
- // Create default session eagerly
355
- const defaultSession = getOrCreateSession('default');
354
+ // Create default context eagerly
355
+ const defaultContext = getOrCreateContext('default');
356
356
 
357
357
  // Languages supported by mrmd-js
358
358
  const supportedLanguages = {
@@ -379,8 +379,9 @@ function createJavaScriptRuntime(options = {}) {
379
379
  /** Execute code (non-streaming) */
380
380
  async execute(code, language, execOptions = {}) {
381
381
  const lang = supportedLanguages[language.toLowerCase()] || 'javascript';
382
- const session = getOrCreateSession(execOptions.session);
383
- const result = await session.execute(code, { language: lang });
382
+ const contextName = execOptions.context ?? execOptions.session;
383
+ const context = getOrCreateContext(contextName);
384
+ const result = await context.execute(code, { language: lang });
384
385
  return {
385
386
  success: result.success,
386
387
  stdout: result.stdout || '',
@@ -394,8 +395,9 @@ function createJavaScriptRuntime(options = {}) {
394
395
  /** Execute code with streaming output */
395
396
  async executeStreaming(code, language, onChunk, onStdinRequest, execOptions = {}) {
396
397
  const lang = supportedLanguages[language.toLowerCase()] || 'javascript';
397
- const session = getOrCreateSession(execOptions.session);
398
- const result = await session.execute(code, { language: lang });
398
+ const contextName = execOptions.context ?? execOptions.session;
399
+ const context = getOrCreateContext(contextName);
400
+ const result = await context.execute(code, { language: lang });
399
401
 
400
402
  // Handle different output types
401
403
  let output = result.stdout || '';
@@ -434,18 +436,18 @@ function createJavaScriptRuntime(options = {}) {
434
436
  };
435
437
  },
436
438
 
437
- /** Reset a session (clear all variables) */
438
- reset(sessionName) {
439
- const session = sessions.get(sessionName || 'default');
440
- if (session) {
441
- session.reset();
439
+ /** Reset a context (clear all variables) */
440
+ reset(contextName) {
441
+ const context = contexts.get(contextName || 'default');
442
+ if (context) {
443
+ context.reset();
442
444
  }
443
445
  },
444
446
 
445
- /** Reset all sessions */
447
+ /** Reset all contexts */
446
448
  resetAll() {
447
- for (const session of sessions.values()) {
448
- session.reset();
449
+ for (const context of contexts.values()) {
450
+ context.reset();
449
451
  }
450
452
  },
451
453
 
@@ -454,23 +456,32 @@ function createJavaScriptRuntime(options = {}) {
454
456
  return rt;
455
457
  },
456
458
 
457
- /** Get a session by name (default if not specified) */
458
- getSession(sessionName) {
459
- return getOrCreateSession(sessionName);
459
+ /** Get a context by name (default if not specified) */
460
+ getContext(contextName) {
461
+ return getOrCreateContext(contextName);
462
+ },
463
+
464
+ /** List all context names */
465
+ listContexts() {
466
+ return Array.from(contexts.keys());
467
+ },
468
+
469
+ // Legacy aliases (kept for compatibility inside monorepo)
470
+ getSession(contextName) {
471
+ return getOrCreateContext(contextName);
460
472
  },
461
473
 
462
- /** List all session names */
463
474
  listSessions() {
464
- return Array.from(sessions.keys());
475
+ return Array.from(contexts.keys());
465
476
  },
466
477
 
467
- /** Destroy the runtime and all sessions */
478
+ /** Destroy the runtime and all contexts */
468
479
  destroy() {
469
480
  rt.destroy();
470
481
  },
471
482
 
472
483
  // =========================================================================
473
- // LSP Features (powered by mrmd-js session)
484
+ // LSP Features (powered by mrmd-js default context)
474
485
  // =========================================================================
475
486
 
476
487
  /**
@@ -482,7 +493,7 @@ function createJavaScriptRuntime(options = {}) {
482
493
  * @returns {{found: boolean, name?: string, type?: string, value?: string, signature?: string}|null}
483
494
  */
484
495
  hover(code, cursor) {
485
- return defaultSession.hover(code, cursor);
496
+ return defaultContext.hover(code, cursor);
486
497
  },
487
498
 
488
499
  /**
@@ -494,7 +505,7 @@ function createJavaScriptRuntime(options = {}) {
494
505
  * @returns {{matches: Array, cursorStart: number, cursorEnd: number}}
495
506
  */
496
507
  complete(code, cursor) {
497
- return defaultSession.complete(code, cursor);
508
+ return defaultContext.complete(code, cursor);
498
509
  },
499
510
 
500
511
  /**
@@ -506,18 +517,18 @@ function createJavaScriptRuntime(options = {}) {
506
517
  * @returns {Object|null}
507
518
  */
508
519
  inspect(code, cursor, options = {}) {
509
- return defaultSession.inspect(code, cursor, options);
520
+ return defaultContext.inspect(code, cursor, options);
510
521
  },
511
522
 
512
523
  /**
513
- * List all variables in a session namespace.
524
+ * List all variables in a context namespace.
514
525
  *
515
526
  * @param {Object} [filter]
516
- * @param {string} [sessionName='default']
527
+ * @param {string} [contextName='default']
517
528
  * @returns {Array<{name: string, type: string, value: string, expandable?: boolean}>}
518
529
  */
519
- listVariables(filter = {}, sessionName = 'default') {
520
- return getOrCreateSession(sessionName).listVariables(filter);
530
+ listVariables(filter = {}, contextName = 'default') {
531
+ return getOrCreateContext(contextName).listVariables(filter);
521
532
  },
522
533
 
523
534
  /**
@@ -528,7 +539,7 @@ function createJavaScriptRuntime(options = {}) {
528
539
  * @returns {Object}
529
540
  */
530
541
  getVariable(name, options = {}) {
531
- return defaultSession.getVariable(name, options);
542
+ return defaultContext.getVariable(name, options);
532
543
  },
533
544
 
534
545
  /**
@@ -538,7 +549,7 @@ function createJavaScriptRuntime(options = {}) {
538
549
  * @returns {{status: 'complete'|'incomplete'|'invalid'|'unknown', indent?: string}}
539
550
  */
540
551
  isComplete(code) {
541
- return defaultSession.isComplete(code);
552
+ return defaultContext.isComplete(code);
542
553
  },
543
554
 
544
555
  /**
@@ -548,7 +559,7 @@ function createJavaScriptRuntime(options = {}) {
548
559
  * @returns {Promise<{formatted: string, changed: boolean}>}
549
560
  */
550
561
  format(code) {
551
- return defaultSession.format(code);
562
+ return defaultContext.format(code);
552
563
  },
553
564
 
554
565
  /**
@@ -556,7 +567,7 @@ function createJavaScriptRuntime(options = {}) {
556
567
  * @returns {import('./runtime-lsp.js').RuntimeLSPProvider}
557
568
  */
558
569
  getLSPProvider() {
559
- return adaptMrmdJsSession(defaultSession);
570
+ return adaptMrmdJsSession(defaultContext);
560
571
  },
561
572
  };
562
573
  }
@@ -731,6 +742,15 @@ const codeBlockStyles = EditorView.theme({
731
742
  '.cm-codeblock-fence::selection, .cm-codeblock-fence *::selection': {
732
743
  backgroundColor: 'var(--editor-selection, #264f78) !important',
733
744
  },
745
+ // Mobile: code blocks need to be larger and scroll horizontally
746
+ '@media (max-width: 768px)': {
747
+ '.cm-codeblock-line': {
748
+ fontSize: 'max(var(--code-font-size, 0.8em), 13px)',
749
+ },
750
+ '.cm-codeblock-fence': {
751
+ fontSize: '0.6em', // Slightly larger than desktop's 0.5em for visibility
752
+ },
753
+ },
734
754
  });
735
755
  // #endregion CODE_BLOCK_BACKGROUND
736
756
 
@@ -771,6 +791,50 @@ class Writer {
771
791
  }
772
792
  // #endregion WRITER
773
793
 
794
+ // #region INITIAL_CURSOR
795
+ /**
796
+ * Find the ideal initial cursor position for a markdown document.
797
+ *
798
+ * When opening a file, placing the cursor at position 0 shows raw frontmatter
799
+ * YAML which looks ugly. Instead, we find the first empty line after any
800
+ * frontmatter block — this causes the frontmatter to render as a nice widget
801
+ * and gives a clean first impression.
802
+ *
803
+ * @param {string} content - Document content
804
+ * @returns {number} Character position for the cursor
805
+ */
806
+ function findInitialCursorPosition(content) {
807
+ if (!content) return 0;
808
+
809
+ const lines = content.split('\n');
810
+ let i = 0;
811
+
812
+ // Skip YAML frontmatter if present (--- ... ---)
813
+ if (lines[0]?.trim() === '---') {
814
+ i = 1;
815
+ while (i < lines.length && lines[i]?.trim() !== '---') {
816
+ i++;
817
+ }
818
+ if (i < lines.length) i++; // skip closing ---
819
+ }
820
+
821
+ // Find first empty line from current position
822
+ while (i < lines.length) {
823
+ if (lines[i]?.trim() === '') {
824
+ // Calculate character position (start of this empty line)
825
+ let pos = 0;
826
+ for (let j = 0; j < i; j++) {
827
+ pos += lines[j].length + 1; // +1 for \n
828
+ }
829
+ return pos;
830
+ }
831
+ i++;
832
+ }
833
+
834
+ return 0; // fallback to start
835
+ }
836
+ // #endregion INITIAL_CURSOR
837
+
774
838
  // #region CREATE
775
839
  /**
776
840
  * Create a standalone markdown editor
@@ -896,6 +960,11 @@ function create(target, options = {}) {
896
960
  '.cm-gutters': { display: 'none' },
897
961
  '.cm-activeLineGutter': { backgroundColor: 'transparent' },
898
962
  '&.cm-focused': { outline: 'none' },
963
+ // Mobile: slightly larger text, comfortable line-height
964
+ '@media (max-width: 768px)': {
965
+ '&': { fontSize: '17px' },
966
+ '.cm-scroller': { lineHeight: '1.7' },
967
+ },
899
968
  });
900
969
 
901
970
  // Inject CSS styles
@@ -2046,17 +2115,16 @@ function create(target, options = {}) {
2046
2115
  * Refresh variables from all MRP runtimes
2047
2116
  * Fetches current variable state and updates state.variables
2048
2117
  *
2049
- * @param {string} [sessionId] - Specific session to refresh (optional)
2050
2118
  * @returns {Promise<void>}
2051
2119
  */
2052
- async refreshVariables(sessionId) {
2120
+ async refreshVariables() {
2053
2121
  for (const [name, runtime] of registry.runtimes) {
2054
2122
  // Check if runtime is an MRP client (has getVariables method)
2055
2123
  if (typeof runtime.getVariables === 'function') {
2056
2124
  try {
2057
- const result = await runtime.getVariables(sessionId);
2125
+ const result = await runtime.getVariables();
2058
2126
  if (result && result.variables) {
2059
- const session = sessionId || 'default';
2127
+ const session = 'default';
2060
2128
  const variables = {};
2061
2129
  for (const v of result.variables) {
2062
2130
  variables[v.name] = {
@@ -2546,12 +2614,12 @@ function create(target, options = {}) {
2546
2614
  }
2547
2615
  // #endregion CREATE
2548
2616
 
2549
- // #region SESSION
2617
+ // #region RUNTIME
2550
2618
  /**
2551
- * Create an editor session via orchestrator.
2619
+ * Create an editor runtime attachment via orchestrator.
2552
2620
  *
2553
2621
  * This is the simplest way to use mrmd with full features:
2554
- * - Automatically creates session with orchestrator
2622
+ * - Automatically creates/attaches runtime with orchestrator
2555
2623
  * - Connects to sync server
2556
2624
  * - Sets up Python runtime (shared or dedicated)
2557
2625
  * - Enables monitor mode for long-running executions
@@ -2562,28 +2630,28 @@ function create(target, options = {}) {
2562
2630
  * @param {string} options.doc - Document name (required)
2563
2631
  * @param {string} [options.python='shared'] - 'shared' or 'dedicated'
2564
2632
  * @param {Object} [options.editor] - Additional editor options
2565
- * @returns {Promise<Object>} Editor instance with destroySession() method
2633
+ * @returns {Promise<Object>} Editor instance with destroyRuntime() method
2566
2634
  *
2567
2635
  * @example
2568
2636
  * // Basic usage
2569
- * const editor = await mrmd.session('http://localhost:8080', '#editor', {
2637
+ * const editor = await mrmd.runtime('http://localhost:8080', '#editor', {
2570
2638
  * doc: 'my-notebook',
2571
2639
  * });
2572
2640
  *
2573
2641
  * // With dedicated Python runtime
2574
- * const editor = await mrmd.session('http://localhost:8080', '#editor', {
2642
+ * const editor = await mrmd.runtime('http://localhost:8080', '#editor', {
2575
2643
  * doc: 'my-notebook',
2576
2644
  * python: 'dedicated',
2577
2645
  * });
2578
2646
  *
2579
2647
  * // Clean up when done
2580
- * await editor.destroySession();
2648
+ * await editor.destroyRuntime();
2581
2649
  */
2582
- async function session(orchestratorUrl, target, options = {}) {
2650
+ async function runtime(orchestratorUrl, target, options = {}) {
2583
2651
  const { doc, python = 'shared', editor: editorOptions = {} } = options;
2584
2652
 
2585
2653
  if (!doc) {
2586
- throw new Error('mrmd.session: doc option is required');
2654
+ throw new Error('mrmd.runtime: doc option is required');
2587
2655
  }
2588
2656
 
2589
2657
  // Normalize orchestrator URL
@@ -2592,24 +2660,33 @@ async function session(orchestratorUrl, target, options = {}) {
2592
2660
  baseUrl = baseUrl.slice(0, -1);
2593
2661
  }
2594
2662
 
2595
- // Create session with orchestrator
2596
- console.log(`[mrmd.session] Creating session for '${doc}' (python=${python})`);
2663
+ // Create runtime attachment with orchestrator
2664
+ console.log(`[mrmd.runtime] Creating runtime for '${doc}' (python=${python})`);
2597
2665
 
2598
- const response = await fetch(`${baseUrl}/api/sessions`, {
2666
+ // Prefer /api/runtimes, fall back to /api/sessions for legacy orchestrators
2667
+ let response = await fetch(`${baseUrl}/api/runtimes`, {
2599
2668
  method: 'POST',
2600
2669
  headers: { 'Content-Type': 'application/json' },
2601
2670
  body: JSON.stringify({ doc, python }),
2602
2671
  });
2603
2672
 
2673
+ if (!response.ok && response.status === 404) {
2674
+ response = await fetch(`${baseUrl}/api/sessions`, {
2675
+ method: 'POST',
2676
+ headers: { 'Content-Type': 'application/json' },
2677
+ body: JSON.stringify({ doc, python }),
2678
+ });
2679
+ }
2680
+
2604
2681
  if (!response.ok) {
2605
2682
  const error = await response.text();
2606
- throw new Error(`Failed to create session: ${error}`);
2683
+ throw new Error(`Failed to create runtime attachment: ${error}`);
2607
2684
  }
2608
2685
 
2609
2686
  const sessionInfo = await response.json();
2610
- console.log('[mrmd.session] Session created:', sessionInfo);
2687
+ console.log('[mrmd.runtime] Runtime created:', sessionInfo);
2611
2688
 
2612
- // Extract URLs from session info
2689
+ // Extract URLs from response
2613
2690
  const syncUrl = sessionInfo.sync;
2614
2691
  const runtimeUrl = sessionInfo.runtimes?.python?.url;
2615
2692
 
@@ -2634,17 +2711,16 @@ async function session(orchestratorUrl, target, options = {}) {
2634
2711
  ydoc: editor.ydoc,
2635
2712
  runtimeUrl,
2636
2713
  awareness: editor.awareness,
2637
- session: doc,
2638
2714
  });
2639
2715
  }
2640
2716
 
2641
- // Store session info on editor
2717
+ // Store runtime info on editor
2642
2718
  editor._sessionInfo = sessionInfo;
2643
2719
  editor._orchestratorUrl = baseUrl;
2644
2720
 
2645
- // Add destroySession method
2646
- editor.destroySession = async function() {
2647
- console.log(`[mrmd.session] Destroying session for '${doc}'`);
2721
+ // Add destroyRuntime method
2722
+ editor.destroyRuntime = async function() {
2723
+ console.log(`[mrmd.runtime] Destroying runtime for '${doc}'`);
2648
2724
 
2649
2725
  // Disconnect from sync
2650
2726
  if (editor.disconnect) {
@@ -2653,28 +2729,33 @@ async function session(orchestratorUrl, target, options = {}) {
2653
2729
 
2654
2730
  // Call orchestrator to clean up
2655
2731
  try {
2656
- const resp = await fetch(`${baseUrl}/api/sessions/${encodeURIComponent(doc)}`, {
2732
+ let resp = await fetch(`${baseUrl}/api/runtimes/${encodeURIComponent(doc)}`, {
2657
2733
  method: 'DELETE',
2658
2734
  });
2735
+ if (!resp.ok && resp.status === 404) {
2736
+ resp = await fetch(`${baseUrl}/api/sessions/${encodeURIComponent(doc)}`, {
2737
+ method: 'DELETE',
2738
+ });
2739
+ }
2659
2740
  if (!resp.ok) {
2660
- console.warn(`[mrmd.session] Failed to destroy session: ${resp.statusText}`);
2741
+ console.warn(`[mrmd.runtime] Failed to destroy runtime: ${resp.statusText}`);
2661
2742
  }
2662
2743
  } catch (err) {
2663
- console.warn(`[mrmd.session] Failed to destroy session:`, err);
2744
+ console.warn(`[mrmd.runtime] Failed to destroy runtime:`, err);
2664
2745
  }
2665
2746
 
2666
2747
  // Destroy editor
2667
2748
  editor.destroy();
2668
2749
  };
2669
2750
 
2670
- // Add method to get session info
2671
- editor.getSessionInfo = function() {
2751
+ // Add method to get runtime info
2752
+ editor.getRuntimeInfo = function() {
2672
2753
  return this._sessionInfo;
2673
2754
  };
2674
2755
 
2675
2756
  return editor;
2676
2757
  }
2677
- // #endregion SESSION
2758
+ // #endregion RUNTIME
2678
2759
 
2679
2760
  // #region DRIVE
2680
2761
  /**
@@ -3038,7 +3119,8 @@ const mrmd = {
3038
3119
  version: VERSION,
3039
3120
  create,
3040
3121
  drive,
3041
- session,
3122
+ runtime,
3123
+ findInitialCursorPosition,
3042
3124
  yjs,
3043
3125
  codemirror,
3044
3126
  terminal,
@@ -3096,7 +3178,8 @@ export default mrmd;
3096
3178
  export {
3097
3179
  create,
3098
3180
  drive,
3099
- session,
3181
+ runtime,
3182
+ findInitialCursorPosition,
3100
3183
  yjs,
3101
3184
  codemirror,
3102
3185
  terminal,
@@ -3232,4 +3315,8 @@ export {
3232
3315
 
3233
3316
  // Re-export shell components for direct imports
3234
3317
  export const { createStudio, OrchestratorClient, Drive, createDrive, ShellStateManager, injectShellStyles } = shellModule;
3318
+
3319
+ // Document language detection and frontmatter updater
3320
+ export { getDocumentLanguages, getLanguageDisplay, isExecutableLanguage } from './document-languages.js';
3321
+ export { parseFrontmatter, updateFrontmatterSession, readFrontmatterSession, getEffectiveSessionConfig } from './frontmatter-updater.js';
3235
3322
  // #endregion EXPORTS
@@ -121,6 +121,33 @@ function extractWikiLinks(text) {
121
121
  return results;
122
122
  }
123
123
 
124
+ /**
125
+ * Find YAML frontmatter at the top of the document (--- ... ---).
126
+ *
127
+ * We use this to suppress markdown rendering inside frontmatter since the
128
+ * opening/closing `---` lines are parsed as HorizontalRule nodes by markdown.
129
+ */
130
+ function findFrontmatterRange(doc) {
131
+ if (doc.lines < 2) return null;
132
+
133
+ const firstLine = doc.line(1);
134
+ if (firstLine.text.trim() !== '---') return null;
135
+
136
+ for (let i = 2; i <= doc.lines; i++) {
137
+ const line = doc.line(i);
138
+ if (line.text.trim() === '---') {
139
+ return {
140
+ from: firstLine.from,
141
+ to: line.to,
142
+ startLine: 1,
143
+ endLine: i,
144
+ };
145
+ }
146
+ }
147
+
148
+ return null;
149
+ }
150
+
124
151
  /**
125
152
  * BlockImageWidget wrapper that caches its rendered height for stable layout.
126
153
  */
@@ -223,6 +250,7 @@ function buildDecorations(view) {
223
250
  const doc = view.state.doc;
224
251
  const cursorPos = view.state.selection.main.head;
225
252
  const cursorLine = doc.lineAt(cursorPos).number;
253
+ const frontmatterRange = findFrontmatterRange(doc);
226
254
 
227
255
  // Get asset resolver from facet (may be null)
228
256
  const assetResolver = view.state.facet(assetResolverFacet);
@@ -249,6 +277,21 @@ function buildDecorations(view) {
249
277
  to: view.viewport.to,
250
278
  enter: (node) => {
251
279
  const lineNum = doc.lineAt(node.from).number;
280
+
281
+ // Never apply markdown rendering/styling inside YAML frontmatter.
282
+ // Frontmatter is rendered separately by block-decorations.
283
+ // Use line-based start detection so we still skip nodes that may extend
284
+ // past the line boundary (e.g., HorizontalRule tokens including newline).
285
+ if (frontmatterRange && node.name !== 'Document') {
286
+ const nodeStartLine = doc.lineAt(node.from).number;
287
+ if (
288
+ nodeStartLine >= frontmatterRange.startLine &&
289
+ nodeStartLine <= frontmatterRange.endLine
290
+ ) {
291
+ return false;
292
+ }
293
+ }
294
+
252
295
  const isActiveLine = lineNum === cursorLine;
253
296
 
254
297
  // Marker class: hidden on blur, muted on focus
@@ -735,6 +778,9 @@ function buildDecorations(view) {
735
778
  const line = doc.line(i);
736
779
  const isActiveLine = i === cursorLine;
737
780
 
781
+ // Skip frontmatter lines
782
+ if (frontmatterRange && i >= frontmatterRange.startLine && i <= frontmatterRange.endLine) continue;
783
+
738
784
  // Skip lines inside code blocks
739
785
  if (codeBlockLines.has(i)) continue;
740
786
 
@@ -770,6 +816,9 @@ function buildDecorations(view) {
770
816
  const line = doc.line(i);
771
817
  const isActiveLine = i === cursorLine;
772
818
 
819
+ // Skip frontmatter lines
820
+ if (frontmatterRange && i >= frontmatterRange.startLine && i <= frontmatterRange.endLine) continue;
821
+
773
822
  // Skip lines inside code blocks (using syntax tree detection)
774
823
  if (codeBlockLines.has(i)) continue;
775
824
 
@@ -810,12 +859,12 @@ function buildDecorations(view) {
810
859
  const line = doc.line(i);
811
860
  const isActiveLine = i === cursorLine;
812
861
 
862
+ // Skip frontmatter lines
863
+ if (frontmatterRange && i >= frontmatterRange.startLine && i <= frontmatterRange.endLine) continue;
864
+
813
865
  // Skip lines inside code blocks (using syntax tree detection)
814
866
  if (codeBlockLines.has(i)) continue;
815
867
 
816
- // Skip frontmatter (YAML between ---)
817
- // This is a simple check - a full solution would track state
818
-
819
868
  const htmlElements = extractHtmlElements(line.text);
820
869
 
821
870
  for (const el of htmlElements) {