this repo has no description
at main 827 lines 26 kB view raw
1<!DOCTYPE html> 2<html xmlns="http://www.w3.org/1999/xhtml"> 3<head> 4 <title>notebook.mld</title> 5 <meta charset="utf-8" /> 6 <link rel="stylesheet" href="/assets/odoc.css" /> 7 <link rel="stylesheet" href="/assets/odoc-notebook.css" /> 8 <meta name="generator" content="odoc-notebook ab3675e" /> 9 <meta name="viewport" content= 10 "width=device-width,initial-scale=1.0" /> 11 <link rel="stylesheet" href="/assets/katex.min.css" /> 12 <script src="/assets/katex.min.js"></script> 13 <script> 14 //<![CDATA[ 15 16 document.addEventListener("DOMContentLoaded", function () { 17 var elements = Array.from(document.getElementsByClassName("odoc-katex-math")); 18 for (var i = 0; i < elements.length; i++) { 19 var el = elements[i]; 20 var content = el.textContent; 21 var new_el = document.createElement("span"); 22 new_el.setAttribute("class", "odoc-katex-math-rendered"); 23 var display = el.classList.contains("display"); 24 katex.render(content, new_el, { throwOnError: false, displayMode: display }); 25 el.replaceWith(new_el); 26 } 27 }); 28 29//]]> 30</script> 31 32 <style> 33 .cm-editor { 34 border: 1px solid #3e3e42; 35 border-radius: 0; 36 font-size: 14px; 37 } 38 39 .cm-editor.cm-focused { 40 outline: 2px solid #0066cc; 41 outline-offset: -1px; 42 } 43 44 .code-editor-container { 45 margin: 10px 0; 46 border: 1px solid #3e3e42; 47 border-radius: 4px; 48 overflow: hidden; 49 } 50 51 /* Ensure the panel at the top has proper rounded corners */ 52 .code-editor-container .cm-panel-top:first-child { 53 border-radius: 4px 4px 0 0; 54 } 55 56 .cm-content { 57 padding: 8px; 58 } 59 60 .cm-scroller { 61 font-family: 'Fira Mono', 'Monaco', 'Consolas', monospace; 62 } 63 64 /* Dark theme overrides for CodeMirror */ 65 .cm-editor { 66 border-bottom: none; 67 } 68 69 /* CodeMirror panel button styles */ 70 .cm-button-panel button:hover:not(:disabled) { 71 filter: brightness(0.9); 72 } 73 74 .cm-button-panel button:disabled { 75 opacity: 0.6; 76 cursor: not-allowed; 77 } 78 79 /* Status bar styles */ 80 .kernel-status-bar { 81 display: flex; 82 align-items: center; 83 padding: 4px 12px; 84 background-color: #252526; 85 border-top: 1px solid #3e3e42; 86 font-size: 12px; 87 color: #cccccc; 88 gap: 8px; 89 } 90 91 .kernel-status-indicator { 92 width: 8px; 93 height: 8px; 94 border-radius: 50%; 95 flex-shrink: 0; 96 } 97 98 .kernel-status-indicator.ready { 99 background-color: #4caf50; 100 } 101 102 .kernel-status-indicator.busy { 103 background-color: #ff9800; 104 animation: pulse 1s infinite; 105 } 106 107 .kernel-status-indicator.disconnected { 108 background-color: #f44336; 109 } 110 111 @keyframes pulse { 112 0% { 113 opacity: 1; 114 } 115 50% { 116 opacity: 0.5; 117 } 118 100% { 119 opacity: 1; 120 } 121 } 122 123 .kernel-status-text { 124 flex: 1; 125 } 126 127 .kernel-exec-time { 128 color: #808080; 129 font-size: 11px; 130 } 131 132 /* CSS3 Keyframes for highlight fade animation */ 133 @keyframes highlight-fade { 134 0% { 135 background-color: rgba(255, 0, 0, 1.0); 136 box-shadow: 0 0 20px rgba(255, 0, 0, 0.8); 137 } 138 100% { 139 background-color: rgba(255, 0, 0, 0); 140 box-shadow: 0 0 20px rgba(255, 0, 0, 0); 141 } 142 } 143 144 /* Line highlighting styles */ 145 .cm-line-highlight { 146 position: relative; 147 animation: highlight-fade 5s ease-out forwards; 148 background-color: rgba(255, 0, 0, 1.0); 149 } 150 151 /* Dark theme specific highlight - bright red */ 152 .cm-editor.cm-dark .cm-line-highlight { 153 animation: highlight-fade 5s ease-out forwards, border-fade 5s ease-out forwards; 154 border-left: 3px solid rgba(255, 0, 0, 1.0); 155 } 156 157 @keyframes border-fade { 158 0% { 159 border-left-color: rgba(255, 0, 0, 1.0); 160 } 161 100% { 162 border-left-color: rgba(255, 0, 0, 0); 163 } 164 } 165 166 /* Read-only line styles */ 167 .cm-line-readonly { 168 background-color: rgba(100, 100, 100, 0.2) !important; 169 position: relative; 170 } 171 172 .cm-line-readonly::before { 173 content: ""; 174 position: absolute; 175 left: 0; 176 top: 0; 177 bottom: 0; 178 width: 3px; 179 background-color: #666; 180 } 181 182 /* Dark theme read-only lines */ 183 .cm-editor.cm-dark .cm-line-readonly { 184 background-color: rgba(50, 50, 50, 0.5) !important; 185 } 186 187 .cm-editor.cm-dark .cm-line-readonly::before { 188 background-color: #888; 189 } 190 191 /* Cursor style for read-only lines */ 192 .cm-line-readonly * { 193 cursor: not-allowed !important; 194 } 195 </style> 196</head> 197<body class="odoc"> 198 <nav class="odoc-nav"><a href="../index.html">Up</a> - notebooks</nav> 199 <header class="odoc-preamble"><section id="section-odoc-notebook"><h1 id="odoc-notebook"><a href="#odoc-notebook" class="anchor"></a>Odoc Notebook</h1></section><p>Here's an odoc notebook with some code snippets in it.</p> 200 201 <div class="code-editor-container"> 202 <div id="editor1"></div> 203 <div class="kernel-status-bar" id="status1"> 204 <span class="kernel-status-indicator ready"></span> 205 <span class="kernel-status-text">Kernel ready</span> 206 <span class="kernel-exec-time"></span> 207 </div> 208 </div> 209 210 <p>Here's some more text.</p> 211 212 <div class="code-editor-container"> 213 <div id="editor2"></div> 214 <div class="kernel-status-bar" id="status2"> 215 <span class="kernel-status-indicator ready"></span> 216 <span class="kernel-status-text">Kernel ready</span> 217 <span class="kernel-exec-time"></span> 218 </div> 219 </div> 220 221 </header> 222 <div class="odoc-tocs"> 223 <nav class="odoc-toc odoc-global-toc"><ul><li><a href="/notebook.html">Odoc Notebook</a></li></ul></nav> 224 </div> 225 <div class="odoc-content"></div> 226 227 <script type="module"> 228 import { 229 EditorView, 230 EditorState, 231 basicSetup, 232 keymap, 233 indentWithTab, 234 oneDark, 235 Decoration, 236 StateField, 237 StateEffect, 238 showPanel 239 } from './dist/codemirror-bundle.js'; 240 241 // Create state effects for highlighting 242 const addHighlight = StateEffect.define(); 243 const removeHighlight = StateEffect.define(); 244 245 // Create a state field to track highlighted lines 246 const highlightField = StateField.define({ 247 create() { return Decoration.none; }, 248 update(highlights, tr) { 249 highlights = highlights.map(tr.changes); 250 for (let e of tr.effects) { 251 if (e.is(addHighlight)) { 252 highlights = highlights.update({add: e.value, sort: true}); 253 } else if (e.is(removeHighlight)) { 254 highlights = highlights.update({filter: () => false}); 255 } 256 } 257 return highlights; 258 }, 259 provide: f => EditorView.decorations.from(f) 260 }); 261 262 // Kernel state management 263 const KernelState = { 264 READY: 'ready', 265 BUSY: 'busy', 266 DISCONNECTED: 'disconnected' 267 }; 268 269 let kernelState = KernelState.READY; 270 271 // State effects for execution state 272 const setExecuting = StateEffect.define(); 273 const setExecutionComplete = StateEffect.define(); 274 275 // State field to track execution state 276 const executionState = StateField.define({ 277 create() { return { executing: false, lastExecutionTime: null }; }, 278 update(state, tr) { 279 for (let e of tr.effects) { 280 if (e.is(setExecuting)) { 281 return { executing: true, lastExecutionTime: null }; 282 } else if (e.is(setExecutionComplete)) { 283 return { executing: false, lastExecutionTime: e.value }; 284 } 285 } 286 return state; 287 } 288 }); 289 290 // State effects for read-only lines 291 const setReadOnlyLines = StateEffect.define(); 292 const clearReadOnlyLines = StateEffect.define(); 293 294 // State field to track read-only line numbers 295 const readOnlyLinesField = StateField.define({ 296 create() { return new Set(); }, 297 update(lines, tr) { 298 for (let e of tr.effects) { 299 if (e.is(setReadOnlyLines)) { 300 return new Set(e.value); 301 } else if (e.is(clearReadOnlyLines)) { 302 return new Set(); 303 } 304 } 305 return lines; 306 } 307 }); 308 309 // State field for read-only line decorations 310 const readOnlyDecorations = StateField.define({ 311 create(state) { 312 // Return empty decorations initially 313 return Decoration.none; 314 }, 315 update(decorations, tr) { 316 // Always rebuild decorations based on current read-only lines 317 const readOnlyLines = tr.state.field(readOnlyLinesField); 318 const newDecorations = []; 319 320 readOnlyLines.forEach(lineNum => { 321 if (lineNum >= 1 && lineNum <= tr.state.doc.lines) { 322 const line = tr.state.doc.line(lineNum); 323 newDecorations.push( 324 Decoration.line({ 325 attributes: {class: "cm-line-readonly"} 326 }).range(line.from) 327 ); 328 } 329 }); 330 331 return Decoration.set(newDecorations); 332 }, 333 provide: f => EditorView.decorations.from(f) 334 }); 335 336 // Extension that makes certain lines read-only using change filter 337 const readOnlyLineExtension = EditorState.changeFilter.of((tr) => { 338 const readOnlyLines = tr.state.field(readOnlyLinesField); 339 if (readOnlyLines.size === 0) return true; // Allow all changes if no read-only lines 340 341 const doc = tr.state.doc; 342 343 // Build ranges for all read-only lines 344 const readOnlyRanges = []; 345 readOnlyLines.forEach(lineNum => { 346 if (lineNum >= 1 && lineNum <= doc.lines) { 347 const line = doc.line(lineNum); 348 readOnlyRanges.push({from: line.from, to: line.to}); 349 } 350 }); 351 352 // Check if any changes overlap with read-only ranges 353 let allowed = true; 354 tr.changes.iterChangedRanges((fromA, toA) => { 355 for (const range of readOnlyRanges) { 356 // Check if this change overlaps with a read-only range 357 if (fromA < range.to && toA > range.from) { 358 allowed = false; 359 break; 360 } 361 } 362 }); 363 364 return allowed; 365 }); 366 367 // Create a panel with buttons 368 function createButtonPanel(editorId, statusId) { 369 return showPanel.of(() => { 370 const dom = document.createElement("div"); 371 dom.className = "cm-button-panel"; 372 dom.style.cssText = ` 373 display: flex; 374 gap: 8px; 375 padding: 6px 12px; 376 background-color: #2d2d30; 377 border-bottom: 1px solid #3e3e42; 378 align-items: center; 379 justify-content: space-between; 380 `; 381 382 const buttonContainer = document.createElement("div"); 383 buttonContainer.style.cssText = "display: flex; gap: 8px;"; 384 385 // Run button 386 const runButton = document.createElement("button"); 387 runButton.className = "cm-run-button"; 388 runButton.innerHTML = '<svg viewBox="0 0 24 24" style="width: 14px; height: 14px; fill: currentColor;"><path d="M8 5v14l11-7z"/></svg>Run'; 389 runButton.style.cssText = ` 390 display: flex; 391 align-items: center; 392 gap: 6px; 393 padding: 4px 12px; 394 background-color: #0066cc; 395 color: white; 396 border: none; 397 border-radius: 3px; 398 font-size: 13px; 399 cursor: pointer; 400 `; 401 402 runButton.onclick = () => { 403 const editor = document.getElementById(editorId).editorView; 404 if (kernelState === KernelState.READY && editor) { 405 executeCode(editorId, statusId, editor); 406 } 407 }; 408 409 // Highlight button 410 const highlightButton = document.createElement("button"); 411 highlightButton.className = "cm-highlight-button"; 412 highlightButton.innerHTML = '<svg viewBox="0 0 24 24" style="width: 14px; height: 14px; fill: currentColor;"><path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z"/></svg>Highlight'; 413 highlightButton.style.cssText = ` 414 display: flex; 415 align-items: center; 416 gap: 6px; 417 padding: 4px 12px; 418 background-color: #00a67e; 419 color: white; 420 border: none; 421 border-radius: 3px; 422 font-size: 13px; 423 cursor: pointer; 424 `; 425 426 highlightButton.onclick = () => { 427 const editor = document.getElementById(editorId).editorView; 428 if (editor) { 429 const lineCount = editor.state.doc.lines; 430 const linesToHighlight = []; 431 for (let i = 1; i <= Math.min(3, lineCount); i++) { 432 linesToHighlight.push(i); 433 } 434 highlightLines(editorId, linesToHighlight, 5000); 435 } 436 }; 437 438 buttonContainer.appendChild(runButton); 439 buttonContainer.appendChild(highlightButton); 440 441 // Shortcut hint 442 const hint = document.createElement("span"); 443 hint.textContent = "Shift + Enter to run"; 444 hint.style.cssText = "font-size: 11px; color: #a0a0a0;"; 445 446 dom.appendChild(buttonContainer); 447 dom.appendChild(hint); 448 449 return { 450 dom, 451 top: true, // Position panel at the top 452 update(update) { 453 // Update button states based on execution state 454 const state = update.state.field(executionState); 455 runButton.disabled = state.executing || kernelState !== KernelState.READY; 456 runButton.textContent = state.executing ? 'Running...' : 'Run'; 457 if (!state.executing) { 458 runButton.innerHTML = '<svg viewBox="0 0 24 24" style="width: 14px; height: 14px; fill: currentColor;"><path d="M8 5v14l11-7z"/></svg>Run'; 459 } 460 } 461 }; 462 }); 463 } 464 465 // Define the code snippets with their original content 466 const codeSnippets = [ 467 { 468 id: 'editor1', 469 statusId: 'status1', 470 runId: 'run1', 471 highlightId: 'highlight1', 472 content: `let rec fib x = 473 match x with 474 | 0 -> 1 475 | 1 -> 1 476 | n -> fib (n-1) + fib (n-2);; 477val fib : int -> int = <fun>`, 478 readOnly: false 479 }, 480 { 481 id: 'editor2', 482 statusId: 'status2', 483 runId: 'run2', 484 highlightId: 'highlight2', 485 content: `fib 10;; 486- : int = 89`, 487 readOnly: false 488 } 489 ]; 490 491 // Custom keymap with Shift+Enter for execution 492 function createCustomKeymap(editorId, statusId) { 493 return keymap.of([ 494 indentWithTab, 495 { 496 key: "Shift-Enter", 497 run: (view) => { 498 if (kernelState === KernelState.READY) { 499 executeCode(editorId, statusId, view); 500 } 501 return true; 502 } 503 } 504 ]); 505 } 506 507 // Initialize editors 508 codeSnippets.forEach(snippet => { 509 const container = document.getElementById(snippet.id); 510 511 // Create extensions array 512 const extensions = [ 513 basicSetup, 514 oneDark, 515 highlightField, 516 executionState, 517 readOnlyLinesField, 518 readOnlyDecorations, 519 readOnlyLineExtension, 520 createButtonPanel(snippet.id, snippet.statusId), 521 createCustomKeymap(snippet.id, snippet.statusId), 522 EditorView.theme({ 523 "&": { 524 fontSize: "14px", 525 fontFamily: "'Fira Mono', 'Monaco', 'Consolas', monospace" 526 }, 527 ".cm-content": { 528 padding: "8px", 529 minHeight: "auto" 530 }, 531 ".cm-line": { 532 paddingLeft: "4px" 533 } 534 }), 535 EditorView.lineWrapping 536 ]; 537 538 // Add readonly extension if needed 539 if (snippet.readOnly) { 540 extensions.push(EditorState.readOnly.of(true)); 541 } 542 543 const state = EditorState.create({ 544 doc: snippet.content, 545 extensions: extensions 546 }); 547 548 const view = new EditorView({ 549 state, 550 parent: container 551 }); 552 553 // Store editor reference for potential future use 554 container.editorView = view; 555 }); 556 557 // Make output lines read-only by default after editors are initialized 558 setTimeout(() => { 559 // For editor1, line 6 contains the output "val fib : int -> int = <fun>" 560 makeReadOnlyLines('editor1', [6]); 561 562 // For editor2, line 2 contains the output "- : int = 89" 563 makeReadOnlyLines('editor2', [2]); 564 }, 100); 565 566 // Function to highlight lines with fade animation using decorations API 567 function highlightLines(editorId, lineNumbers, duration = 300000) { 568 const container = document.getElementById(editorId); 569 const editor = container?.editorView; 570 if (!editor) return; 571 572 const doc = editor.state.doc; 573 const decorations = []; 574 575 // Create line decorations 576 lineNumbers.forEach(lineNum => { 577 if (lineNum >= 1 && lineNum <= doc.lines) { 578 const line = doc.line(lineNum); 579 decorations.push( 580 Decoration.line({ 581 attributes: {class: "cm-line-highlight"} 582 }).range(line.from) 583 ); 584 } 585 }); 586 587 // Apply decorations 588 editor.dispatch({ 589 effects: addHighlight.of(decorations) 590 }); 591 592 // Remove decorations after animation completes 593 setTimeout(() => { 594 editor.dispatch({ 595 effects: removeHighlight.of(null) 596 }); 597 }, duration); 598 } 599 600 // Alternative approach using a more direct method 601/* window.highlightLines = function(editorId, lineNumbers, duration = 3000) { 602 const container = document.getElementById(editorId); 603 const editor = container?.editorView; 604 if (!editor) return; 605 606 const doc = editor.state.doc; 607 608 // Get line elements and add highlight class 609 lineNumbers.forEach(lineNum => { 610 if (lineNum >= 1 && lineNum <= doc.lines) { 611 const line = doc.line(lineNum); 612 const lineElement = editor.domAtPos(line.from).node.parentElement; 613 if (lineElement && lineElement.classList.contains('cm-line')) { 614 lineElement.classList.add('cm-line-highlight'); 615 616 // Remove the highlight after animation 617 setTimeout(() => { 618 lineElement.classList.add('fade-out'); 619 setTimeout(() => { 620 lineElement.classList.remove('cm-line-highlight', 'fade-out'); 621 }, duration); 622 }, 100); 623 } 624 } 625 }); 626 }; */ 627 628 // Function to update kernel status globally 629 function updateKernelStatus(state) { 630 kernelState = state; 631 const statusBars = document.querySelectorAll('.kernel-status-bar'); 632 statusBars.forEach(bar => { 633 if (!bar.classList.contains('executing')) { 634 updateStatusBar(bar, state, getStatusText(state)); 635 } 636 }); 637 } 638 639 // Function to update individual status bar 640 function updateStatusBar(statusBar, state, text, execTime = '') { 641 const indicator = statusBar.querySelector('.kernel-status-indicator'); 642 const statusText = statusBar.querySelector('.kernel-status-text'); 643 const timeText = statusBar.querySelector('.kernel-exec-time'); 644 645 // Remove all state classes and add the new one 646 indicator.classList.remove('ready', 'busy', 'disconnected'); 647 indicator.classList.add(state); 648 649 statusText.textContent = text; 650 timeText.textContent = execTime; 651 } 652 653 // Get status text for a given state 654 function getStatusText(state) { 655 switch(state) { 656 case KernelState.READY: 657 return 'Kernel ready'; 658 case KernelState.BUSY: 659 return 'Kernel busy'; 660 case KernelState.DISCONNECTED: 661 return 'Kernel disconnected'; 662 default: 663 return 'Unknown state'; 664 } 665 } 666 667 // Simulate code execution for a specific editor 668 function executeCode(editorId, statusId, editor) { 669 const statusBar = document.getElementById(statusId); 670 671 // Update state to executing 672 editor.dispatch({ 673 effects: setExecuting.of(null) 674 }); 675 676 // Mark this cell as executing 677 statusBar.classList.add('executing'); 678 updateStatusBar(statusBar, KernelState.BUSY, 'Executing...', ''); 679 680 // Highlight the lines being executed (example: highlight lines 1-3) 681 const doc = editor.state.doc; 682 const linesToHighlight = []; 683 for (let i = 1; i <= Math.min(3, doc.lines); i++) { 684 linesToHighlight.push(i); 685 } 686 highlightLines(editorId, linesToHighlight, 3000); 687 688 // Simulate execution time 689 const startTime = Date.now(); 690 setTimeout(() => { 691 const execTime = Date.now() - startTime; 692 statusBar.classList.remove('executing'); 693 updateStatusBar(statusBar, kernelState, getStatusText(kernelState), `Executed in ${execTime}ms`); 694 695 // Update state to complete 696 editor.dispatch({ 697 effects: setExecutionComplete.of(execTime) 698 }); 699 }, Math.random() * 2000 + 500); // Random execution time between 0.5-2.5s 700 } 701 702 703 // Simulate kernel connection status changes (for demo) 704 setTimeout(() => { 705 window.updateKernelStatus(KernelState.DISCONNECTED); 706 setTimeout(() => { 707 window.updateKernelStatus(KernelState.READY); 708 }, 3000); 709 }, 5000); 710 711 // Update run button states when kernel status changes 712 function updateRunButtons() { 713 const runButtons = document.querySelectorAll('.run-button'); 714 runButtons.forEach(button => { 715 if (!button.disabled || button.textContent === 'Run') { 716 button.disabled = kernelState !== KernelState.READY; 717 } 718 }); 719 } 720 721 // Enhance updateKernelStatus to also update buttons 722 const originalUpdateKernelStatus = updateKernelStatus; 723 window.updateKernelStatus = function(state) { 724 kernelState = state; 725 const statusBars = document.querySelectorAll('.kernel-status-bar'); 726 statusBars.forEach(bar => { 727 if (!bar.classList.contains('executing')) { 728 updateStatusBar(bar, state, getStatusText(state)); 729 } 730 }); 731 updateRunButtons(); 732 }; 733 734 // Function to set read-only lines 735 function makeReadOnlyLines(editorId, lineNumbers) { 736 const container = document.getElementById(editorId); 737 const editor = container?.editorView; 738 if (!editor) return; 739 740 editor.dispatch({ 741 effects: setReadOnlyLines.of(lineNumbers) 742 }); 743 } 744 745 // Function to clear read-only lines 746 function removeReadOnlyLines(editorId) { 747 const container = document.getElementById(editorId); 748 const editor = container?.editorView; 749 if (!editor) return; 750 751 editor.dispatch({ 752 effects: clearReadOnlyLines.of(null) 753 }); 754 } 755 756 // Expose functions globally 757 window.highlightLines = highlightLines; 758 window.makeReadOnlyLines = makeReadOnlyLines; 759 window.removeReadOnlyLines = removeReadOnlyLines; 760 761 window.notebookHighlight = { 762 // Highlight specific lines in an editor 763 // Usage: notebookHighlight.lines('editor1', [2, 4, 5]) 764 lines: function(editorId, lineNumbers, duration = 300000) { 765 highlightLines(editorId, lineNumbers, duration); 766 }, 767 768 // Highlight a range of lines 769 // Usage: notebookHighlight.range('editor1', 2, 5) 770 range: function(editorId, startLine, endLine, duration = 300000) { 771 const lines = []; 772 for (let i = startLine; i <= endLine; i++) { 773 lines.push(i); 774 } 775 highlightLines(editorId, lines, duration); 776 }, 777 778 // Highlight all lines 779 // Usage: notebookHighlight.all('editor1') 780 all: function(editorId, duration = 300000) { 781 const editor = document.getElementById(editorId)?.editorView; 782 if (editor) { 783 const lineCount = editor.state.doc.lines; 784 this.range(editorId, 1, lineCount, duration); 785 } 786 } 787 }; 788 789 // Read-only lines API 790 window.notebookReadOnly = { 791 // Set specific lines as read-only 792 // Usage: notebookReadOnly.set('editor1', [2, 6]) 793 set: function(editorId, lineNumbers) { 794 makeReadOnlyLines(editorId, lineNumbers); 795 }, 796 797 // Clear all read-only lines 798 // Usage: notebookReadOnly.clear('editor1') 799 clear: function(editorId) { 800 removeReadOnlyLines(editorId); 801 }, 802 803 // Toggle read-only for specific lines 804 // Usage: notebookReadOnly.toggle('editor1', [2, 6]) 805 toggle: function(editorId, lineNumbers) { 806 const editor = document.getElementById(editorId)?.editorView; 807 if (!editor) return; 808 809 const currentReadOnly = editor.state.field(readOnlyLinesField); 810 const newReadOnly = new Set(currentReadOnly); 811 812 lineNumbers.forEach(lineNum => { 813 if (newReadOnly.has(lineNum)) { 814 newReadOnly.delete(lineNum); 815 } else { 816 newReadOnly.add(lineNum); 817 } 818 }); 819 820 makeReadOnlyLines(editorId, Array.from(newReadOnly)); 821 } 822 }; 823 824 </script> 825 826</body> 827</html>