mrmd-editor 0.6.0 → 0.7.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/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
 
@@ -896,6 +916,11 @@ function create(target, options = {}) {
896
916
  '.cm-gutters': { display: 'none' },
897
917
  '.cm-activeLineGutter': { backgroundColor: 'transparent' },
898
918
  '&.cm-focused': { outline: 'none' },
919
+ // Mobile: slightly larger text, comfortable line-height
920
+ '@media (max-width: 768px)': {
921
+ '&': { fontSize: '17px' },
922
+ '.cm-scroller': { lineHeight: '1.7' },
923
+ },
899
924
  });
900
925
 
901
926
  // Inject CSS styles
@@ -2046,17 +2071,16 @@ function create(target, options = {}) {
2046
2071
  * Refresh variables from all MRP runtimes
2047
2072
  * Fetches current variable state and updates state.variables
2048
2073
  *
2049
- * @param {string} [sessionId] - Specific session to refresh (optional)
2050
2074
  * @returns {Promise<void>}
2051
2075
  */
2052
- async refreshVariables(sessionId) {
2076
+ async refreshVariables() {
2053
2077
  for (const [name, runtime] of registry.runtimes) {
2054
2078
  // Check if runtime is an MRP client (has getVariables method)
2055
2079
  if (typeof runtime.getVariables === 'function') {
2056
2080
  try {
2057
- const result = await runtime.getVariables(sessionId);
2081
+ const result = await runtime.getVariables();
2058
2082
  if (result && result.variables) {
2059
- const session = sessionId || 'default';
2083
+ const session = 'default';
2060
2084
  const variables = {};
2061
2085
  for (const v of result.variables) {
2062
2086
  variables[v.name] = {
@@ -2546,12 +2570,12 @@ function create(target, options = {}) {
2546
2570
  }
2547
2571
  // #endregion CREATE
2548
2572
 
2549
- // #region SESSION
2573
+ // #region RUNTIME
2550
2574
  /**
2551
- * Create an editor session via orchestrator.
2575
+ * Create an editor runtime attachment via orchestrator.
2552
2576
  *
2553
2577
  * This is the simplest way to use mrmd with full features:
2554
- * - Automatically creates session with orchestrator
2578
+ * - Automatically creates/attaches runtime with orchestrator
2555
2579
  * - Connects to sync server
2556
2580
  * - Sets up Python runtime (shared or dedicated)
2557
2581
  * - Enables monitor mode for long-running executions
@@ -2562,28 +2586,28 @@ function create(target, options = {}) {
2562
2586
  * @param {string} options.doc - Document name (required)
2563
2587
  * @param {string} [options.python='shared'] - 'shared' or 'dedicated'
2564
2588
  * @param {Object} [options.editor] - Additional editor options
2565
- * @returns {Promise<Object>} Editor instance with destroySession() method
2589
+ * @returns {Promise<Object>} Editor instance with destroyRuntime() method
2566
2590
  *
2567
2591
  * @example
2568
2592
  * // Basic usage
2569
- * const editor = await mrmd.session('http://localhost:8080', '#editor', {
2593
+ * const editor = await mrmd.runtime('http://localhost:8080', '#editor', {
2570
2594
  * doc: 'my-notebook',
2571
2595
  * });
2572
2596
  *
2573
2597
  * // With dedicated Python runtime
2574
- * const editor = await mrmd.session('http://localhost:8080', '#editor', {
2598
+ * const editor = await mrmd.runtime('http://localhost:8080', '#editor', {
2575
2599
  * doc: 'my-notebook',
2576
2600
  * python: 'dedicated',
2577
2601
  * });
2578
2602
  *
2579
2603
  * // Clean up when done
2580
- * await editor.destroySession();
2604
+ * await editor.destroyRuntime();
2581
2605
  */
2582
- async function session(orchestratorUrl, target, options = {}) {
2606
+ async function runtime(orchestratorUrl, target, options = {}) {
2583
2607
  const { doc, python = 'shared', editor: editorOptions = {} } = options;
2584
2608
 
2585
2609
  if (!doc) {
2586
- throw new Error('mrmd.session: doc option is required');
2610
+ throw new Error('mrmd.runtime: doc option is required');
2587
2611
  }
2588
2612
 
2589
2613
  // Normalize orchestrator URL
@@ -2592,24 +2616,33 @@ async function session(orchestratorUrl, target, options = {}) {
2592
2616
  baseUrl = baseUrl.slice(0, -1);
2593
2617
  }
2594
2618
 
2595
- // Create session with orchestrator
2596
- console.log(`[mrmd.session] Creating session for '${doc}' (python=${python})`);
2619
+ // Create runtime attachment with orchestrator
2620
+ console.log(`[mrmd.runtime] Creating runtime for '${doc}' (python=${python})`);
2597
2621
 
2598
- const response = await fetch(`${baseUrl}/api/sessions`, {
2622
+ // Prefer /api/runtimes, fall back to /api/sessions for legacy orchestrators
2623
+ let response = await fetch(`${baseUrl}/api/runtimes`, {
2599
2624
  method: 'POST',
2600
2625
  headers: { 'Content-Type': 'application/json' },
2601
2626
  body: JSON.stringify({ doc, python }),
2602
2627
  });
2603
2628
 
2629
+ if (!response.ok && response.status === 404) {
2630
+ response = await fetch(`${baseUrl}/api/sessions`, {
2631
+ method: 'POST',
2632
+ headers: { 'Content-Type': 'application/json' },
2633
+ body: JSON.stringify({ doc, python }),
2634
+ });
2635
+ }
2636
+
2604
2637
  if (!response.ok) {
2605
2638
  const error = await response.text();
2606
- throw new Error(`Failed to create session: ${error}`);
2639
+ throw new Error(`Failed to create runtime attachment: ${error}`);
2607
2640
  }
2608
2641
 
2609
2642
  const sessionInfo = await response.json();
2610
- console.log('[mrmd.session] Session created:', sessionInfo);
2643
+ console.log('[mrmd.runtime] Runtime created:', sessionInfo);
2611
2644
 
2612
- // Extract URLs from session info
2645
+ // Extract URLs from response
2613
2646
  const syncUrl = sessionInfo.sync;
2614
2647
  const runtimeUrl = sessionInfo.runtimes?.python?.url;
2615
2648
 
@@ -2634,17 +2667,16 @@ async function session(orchestratorUrl, target, options = {}) {
2634
2667
  ydoc: editor.ydoc,
2635
2668
  runtimeUrl,
2636
2669
  awareness: editor.awareness,
2637
- session: doc,
2638
2670
  });
2639
2671
  }
2640
2672
 
2641
- // Store session info on editor
2673
+ // Store runtime info on editor
2642
2674
  editor._sessionInfo = sessionInfo;
2643
2675
  editor._orchestratorUrl = baseUrl;
2644
2676
 
2645
- // Add destroySession method
2646
- editor.destroySession = async function() {
2647
- console.log(`[mrmd.session] Destroying session for '${doc}'`);
2677
+ // Add destroyRuntime method
2678
+ editor.destroyRuntime = async function() {
2679
+ console.log(`[mrmd.runtime] Destroying runtime for '${doc}'`);
2648
2680
 
2649
2681
  // Disconnect from sync
2650
2682
  if (editor.disconnect) {
@@ -2653,28 +2685,33 @@ async function session(orchestratorUrl, target, options = {}) {
2653
2685
 
2654
2686
  // Call orchestrator to clean up
2655
2687
  try {
2656
- const resp = await fetch(`${baseUrl}/api/sessions/${encodeURIComponent(doc)}`, {
2688
+ let resp = await fetch(`${baseUrl}/api/runtimes/${encodeURIComponent(doc)}`, {
2657
2689
  method: 'DELETE',
2658
2690
  });
2691
+ if (!resp.ok && resp.status === 404) {
2692
+ resp = await fetch(`${baseUrl}/api/sessions/${encodeURIComponent(doc)}`, {
2693
+ method: 'DELETE',
2694
+ });
2695
+ }
2659
2696
  if (!resp.ok) {
2660
- console.warn(`[mrmd.session] Failed to destroy session: ${resp.statusText}`);
2697
+ console.warn(`[mrmd.runtime] Failed to destroy runtime: ${resp.statusText}`);
2661
2698
  }
2662
2699
  } catch (err) {
2663
- console.warn(`[mrmd.session] Failed to destroy session:`, err);
2700
+ console.warn(`[mrmd.runtime] Failed to destroy runtime:`, err);
2664
2701
  }
2665
2702
 
2666
2703
  // Destroy editor
2667
2704
  editor.destroy();
2668
2705
  };
2669
2706
 
2670
- // Add method to get session info
2671
- editor.getSessionInfo = function() {
2707
+ // Add method to get runtime info
2708
+ editor.getRuntimeInfo = function() {
2672
2709
  return this._sessionInfo;
2673
2710
  };
2674
2711
 
2675
2712
  return editor;
2676
2713
  }
2677
- // #endregion SESSION
2714
+ // #endregion RUNTIME
2678
2715
 
2679
2716
  // #region DRIVE
2680
2717
  /**
@@ -3038,7 +3075,7 @@ const mrmd = {
3038
3075
  version: VERSION,
3039
3076
  create,
3040
3077
  drive,
3041
- session,
3078
+ runtime,
3042
3079
  yjs,
3043
3080
  codemirror,
3044
3081
  terminal,
@@ -3096,7 +3133,7 @@ export default mrmd;
3096
3133
  export {
3097
3134
  create,
3098
3135
  drive,
3099
- session,
3136
+ runtime,
3100
3137
  yjs,
3101
3138
  codemirror,
3102
3139
  terminal,
@@ -3232,4 +3269,8 @@ export {
3232
3269
 
3233
3270
  // Re-export shell components for direct imports
3234
3271
  export const { createStudio, OrchestratorClient, Drive, createDrive, ShellStateManager, injectShellStyles } = shellModule;
3272
+
3273
+ // Document language detection and frontmatter updater
3274
+ export { getDocumentLanguages, getLanguageDisplay, isExecutableLanguage } from './document-languages.js';
3275
+ export { parseFrontmatter, updateFrontmatterSession, readFrontmatterSession, getEffectiveSessionConfig } from './frontmatter-updater.js';
3235
3276
  // #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) {