My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 791 lines 27 kB view raw
1(** Scrollycode Extension for odoc 2 3 Provides scroll-driven code tutorials. Theme styling is handled 4 externally via CSS custom properties defined in {!Scrollycode_css} 5 and set by theme files in {!Scrollycode_themes}. 6 7 Authoring format uses [@scrolly] custom tags with an ordered 8 list inside, where each list item is a tutorial step containing 9 a bold title, prose paragraphs, and a code block. 10 11 For backward compatibility, \@scrolly.warm / \@scrolly.dark / 12 \@scrolly.notebook are still accepted but the theme suffix is 13 ignored — theme selection is now a CSS concern. *) 14 15module Comment = Odoc_model.Comment 16module Location_ = Odoc_model.Location_ 17module Block = Odoc_document.Types.Block 18module Inline = Odoc_document.Types.Inline 19 20module Scrollycode_css = Scrollycode_css 21module Scrollycode_themes = Scrollycode_themes 22 23(** {1 Step Extraction} *) 24 25(** A single tutorial step extracted from the ordered list structure *) 26type step = { 27 title : string; 28 prose : string; 29 code : string; 30 focus : int list; (** 1-based line numbers to highlight *) 31} 32 33(** Extract plain text from inline elements *) 34let rec text_of_inline (el : Comment.inline_element Location_.with_location) = 35 match el.Location_.value with 36 | `Space -> " " 37 | `Word w -> w 38 | `Code_span c -> "`" ^ c ^ "`" 39 | `Math_span m -> m 40 | `Raw_markup (_, r) -> r 41 | `Styled (_, content) -> text_of_inlines content 42 | `Reference (_, content) -> text_of_link_content content 43 | `Link (_, content) -> text_of_link_content content 44 45and text_of_inlines content = 46 String.concat "" (List.map text_of_inline content) 47 48and text_of_link_content content = 49 String.concat "" (List.map text_of_non_link content) 50 51and text_of_non_link 52 (el : Comment.non_link_inline_element Location_.with_location) = 53 match el.Location_.value with 54 | `Space -> " " 55 | `Word w -> w 56 | `Code_span c -> "`" ^ c ^ "`" 57 | `Math_span m -> m 58 | `Raw_markup (_, r) -> r 59 | `Styled (_, content) -> text_of_link_content content 60 61let text_of_paragraph (p : Comment.paragraph) = 62 String.concat "" (List.map text_of_inline p) 63 64(** Extract title, prose, code and focus lines from a single list item *) 65let extract_step 66 (item : Comment.nestable_block_element Location_.with_location list) : step 67 = 68 let title = ref "" in 69 let prose_parts = ref [] in 70 let code = ref "" in 71 let focus = ref [] in 72 List.iter 73 (fun (el : Comment.nestable_block_element Location_.with_location) -> 74 match el.Location_.value with 75 | `Paragraph p -> ( 76 let text = text_of_paragraph p in 77 (* Check if the paragraph starts with bold text — that's the title *) 78 match p with 79 | first :: _ 80 when (match first.Location_.value with 81 | `Styled (`Bold, _) -> true 82 | _ -> false) -> 83 if !title = "" then title := text 84 else prose_parts := text :: !prose_parts 85 | _ -> prose_parts := text :: !prose_parts) 86 | `Code_block { content = code_content; _ } -> 87 let code_text = code_content.Location_.value in 88 (* Check for focus annotation in the code: lines starting with >>> *) 89 let lines = String.split_on_char '\n' code_text in 90 let focused_lines = ref [] in 91 let clean_lines = 92 List.mapi 93 (fun i line -> 94 if 95 String.length line >= 4 96 && String.sub line 0 4 = "(* >" 97 then ( 98 focused_lines := (i + 1) :: !focused_lines; 99 (* Remove the focus marker *) 100 let rest = String.sub line 4 (String.length line - 4) in 101 let rest = 102 if 103 String.length rest >= 4 104 && String.sub rest (String.length rest - 4) 4 = "< *)" 105 then String.sub rest 0 (String.length rest - 4) 106 else rest 107 in 108 String.trim rest) 109 else line) 110 lines 111 in 112 code := String.concat "\n" clean_lines; 113 focus := List.rev !focused_lines 114 | `Verbatim v -> prose_parts := v :: !prose_parts 115 | _ -> ()) 116 item; 117 { 118 title = !title; 119 prose = String.concat "\n\n" (List.rev !prose_parts); 120 code = !code; 121 focus = !focus; 122 } 123 124(** Extract all steps from the tag content (expects an ordered list) *) 125let extract_steps 126 (content : 127 Comment.nestable_block_element Location_.with_location list) : 128 string * step list = 129 (* First element might be a paragraph with the tutorial title *) 130 let tutorial_title = ref "Tutorial" in 131 let steps = ref [] in 132 List.iter 133 (fun (el : Comment.nestable_block_element Location_.with_location) -> 134 match el.Location_.value with 135 | `Paragraph p -> 136 let text = text_of_paragraph p in 137 if !steps = [] then tutorial_title := text 138 | `List (`Ordered, items) -> 139 steps := List.map extract_step items 140 | _ -> ()) 141 content; 142 (!tutorial_title, !steps) 143 144(** {1 HTML Escaping} *) 145 146let html_escape s = 147 let buf = Buffer.create (String.length s) in 148 String.iter 149 (function 150 | '&' -> Buffer.add_string buf "&amp;" 151 | '<' -> Buffer.add_string buf "&lt;" 152 | '>' -> Buffer.add_string buf "&gt;" 153 | '"' -> Buffer.add_string buf "&quot;" 154 | c -> Buffer.add_char buf c) 155 s; 156 Buffer.contents buf 157 158(** {1 Diff Computation} *) 159 160type diff_line = 161 | Same of string 162 | Added of string 163 | Removed of string 164 165(** Simple LCS-based line diff between two code strings *) 166let diff_lines old_code new_code = 167 let old_lines = String.split_on_char '\n' old_code |> Array.of_list in 168 let new_lines = String.split_on_char '\n' new_code |> Array.of_list in 169 let n = Array.length old_lines in 170 let m = Array.length new_lines in 171 let dp = Array.make_matrix (n + 1) (m + 1) 0 in 172 for i = 1 to n do 173 for j = 1 to m do 174 if old_lines.(i-1) = new_lines.(j-1) then 175 dp.(i).(j) <- dp.(i-1).(j-1) + 1 176 else 177 dp.(i).(j) <- max dp.(i-1).(j) dp.(i).(j-1) 178 done 179 done; 180 let result = ref [] in 181 let i = ref n and j = ref m in 182 while !i > 0 || !j > 0 do 183 if !i > 0 && !j > 0 && old_lines.(!i-1) = new_lines.(!j-1) then begin 184 result := Same old_lines.(!i-1) :: !result; 185 decr i; decr j 186 end else if !j > 0 && (!i = 0 || dp.(!i).(!j-1) >= dp.(!i-1).(!j)) then begin 187 result := Added new_lines.(!j-1) :: !result; 188 decr j 189 end else begin 190 result := Removed old_lines.(!i-1) :: !result; 191 decr i 192 end 193 done; 194 !result 195 196(** {1 OCaml Syntax Highlighting} 197 198 A simple lexer-based highlighter for OCaml code. Produces HTML spans 199 with classes for keywords, types, strings, comments, operators. *) 200 201let ocaml_keywords = 202 [ 203 "let"; "in"; "if"; "then"; "else"; "match"; "with"; "fun"; "function"; 204 "type"; "module"; "struct"; "sig"; "end"; "open"; "include"; "val"; 205 "rec"; "and"; "of"; "when"; "as"; "begin"; "do"; "done"; "for"; "to"; 206 "while"; "downto"; "try"; "exception"; "raise"; "mutable"; "ref"; 207 "true"; "false"; "assert"; "failwith"; "not"; 208 ] 209 210let ocaml_types = 211 [ 212 "int"; "float"; "string"; "bool"; "unit"; "list"; "option"; "array"; 213 "char"; "bytes"; "result"; "exn"; "ref"; 214 ] 215 216(** Tokenize and highlight OCaml code into HTML *) 217let highlight_ocaml code = 218 let len = String.length code in 219 let buf = Buffer.create (len * 2) in 220 let i = ref 0 in 221 let peek () = if !i < len then Some code.[!i] else None in 222 let advance () = incr i in 223 let current () = code.[!i] in 224 while !i < len do 225 match current () with 226 (* Comments *) 227 | '(' when !i + 1 < len && code.[!i + 1] = '*' -> 228 Buffer.add_string buf "<span class=\"hl-comment\">"; 229 Buffer.add_string buf "(*"; 230 i := !i + 2; 231 let depth = ref 1 in 232 while !depth > 0 && !i < len do 233 if !i + 1 < len && code.[!i] = '(' && code.[!i + 1] = '*' then ( 234 Buffer.add_string buf "(*"; 235 i := !i + 2; 236 incr depth) 237 else if !i + 1 < len && code.[!i] = '*' && code.[!i + 1] = ')' then ( 238 Buffer.add_string buf "*)"; 239 i := !i + 2; 240 decr depth) 241 else ( 242 Buffer.add_string buf (html_escape (String.make 1 code.[!i])); 243 advance ()) 244 done; 245 Buffer.add_string buf "</span>" 246 (* Strings *) 247 | '"' -> 248 Buffer.add_string buf "<span class=\"hl-string\">"; 249 Buffer.add_char buf '"'; 250 advance (); 251 while !i < len && current () <> '"' do 252 if current () = '\\' && !i + 1 < len then ( 253 Buffer.add_string buf (html_escape (String.make 1 (current ()))); 254 advance (); 255 Buffer.add_string buf (html_escape (String.make 1 (current ()))); 256 advance ()) 257 else ( 258 Buffer.add_string buf (html_escape (String.make 1 (current ()))); 259 advance ()) 260 done; 261 if !i < len then ( 262 Buffer.add_char buf '"'; 263 advance ()); 264 Buffer.add_string buf "</span>" 265 (* Char literals *) 266 | '\'' when !i + 2 < len && code.[!i + 2] = '\'' -> 267 Buffer.add_string buf "<span class=\"hl-string\">"; 268 Buffer.add_char buf '\''; 269 advance (); 270 Buffer.add_string buf (html_escape (String.make 1 (current ()))); 271 advance (); 272 Buffer.add_char buf '\''; 273 advance (); 274 Buffer.add_string buf "</span>" 275 (* Numbers *) 276 | '0' .. '9' -> 277 Buffer.add_string buf "<span class=\"hl-number\">"; 278 while 279 !i < len 280 && 281 match current () with 282 | '0' .. '9' | '.' | '_' | 'x' | 'o' | 'b' | 'a' .. 'f' 283 | 'A' .. 'F' -> 284 true 285 | _ -> false 286 do 287 Buffer.add_char buf (current ()); 288 advance () 289 done; 290 Buffer.add_string buf "</span>" 291 (* Identifiers and keywords *) 292 | 'a' .. 'z' | '_' -> 293 let start = !i in 294 while 295 !i < len 296 && 297 match current () with 298 | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' | '\'' -> true 299 | _ -> false 300 do 301 advance () 302 done; 303 let word = String.sub code start (!i - start) in 304 if List.mem word ocaml_keywords then 305 Buffer.add_string buf 306 (Printf.sprintf "<span class=\"hl-keyword\">%s</span>" 307 (html_escape word)) 308 else if List.mem word ocaml_types then 309 Buffer.add_string buf 310 (Printf.sprintf "<span class=\"hl-type\">%s</span>" 311 (html_escape word)) 312 else Buffer.add_string buf (html_escape word) 313 (* Module/constructor names (capitalized identifiers) *) 314 | 'A' .. 'Z' -> 315 let start = !i in 316 while 317 !i < len 318 && 319 match current () with 320 | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' | '\'' -> true 321 | _ -> false 322 do 323 advance () 324 done; 325 let word = String.sub code start (!i - start) in 326 Buffer.add_string buf 327 (Printf.sprintf "<span class=\"hl-module\">%s</span>" 328 (html_escape word)) 329 (* Operators *) 330 | '|' | '-' | '+' | '*' | '/' | '=' | '<' | '>' | '@' | '^' | '~' 331 | '!' | '?' | '%' | '&' -> 332 Buffer.add_string buf "<span class=\"hl-operator\">"; 333 Buffer.add_string buf (html_escape (String.make 1 (current ()))); 334 advance (); 335 (* Consume multi-char operators *) 336 while 337 !i < len 338 && 339 match current () with 340 | '|' | '-' | '+' | '*' | '/' | '=' | '<' | '>' | '@' | '^' 341 | '~' | '!' | '?' | '%' | '&' -> 342 true 343 | _ -> false 344 do 345 Buffer.add_string buf (html_escape (String.make 1 (current ()))); 346 advance () 347 done; 348 Buffer.add_string buf "</span>" 349 (* Punctuation *) 350 | ':' | ';' | '.' | ',' | '[' | ']' | '{' | '}' | '(' | ')' -> 351 Buffer.add_string buf 352 (Printf.sprintf "<span class=\"hl-punct\">%s</span>" 353 (html_escape (String.make 1 (current ())))); 354 advance () 355 (* Arrow special case: -> *) 356 | ' ' | '\t' | '\n' | '\r' -> 357 Buffer.add_char buf (current ()); 358 advance () 359 | _ -> 360 let _ = peek () in 361 Buffer.add_string buf (html_escape (String.make 1 (current ()))); 362 advance () 363 done; 364 Buffer.contents buf 365 366(** Render a diff as HTML with colored lines *) 367let render_diff_html diff = 368 let buf = Buffer.create 1024 in 369 List.iter (fun line -> 370 match line with 371 | Same s -> 372 Buffer.add_string buf 373 (Printf.sprintf "<div class=\"sc-diff-line sc-diff-same\">%s</div>\n" 374 (highlight_ocaml s)) 375 | Added s -> 376 Buffer.add_string buf 377 (Printf.sprintf "<div class=\"sc-diff-line sc-diff-added\">%s</div>\n" 378 (highlight_ocaml s)) 379 | Removed s -> 380 Buffer.add_string buf 381 (Printf.sprintf "<div class=\"sc-diff-line sc-diff-removed\">%s</div>\n" 382 (highlight_ocaml s))) 383 diff; 384 Buffer.contents buf 385 386(** {1 Shared JavaScript} 387 388 The scrollycode runtime handles IntersectionObserver-based step 389 detection and line-level transition animations. *) 390 391let shared_js = 392 {| 393(function() { 394 'use strict'; 395 396 function initScrollycode(container) { 397 var steps = container.querySelectorAll('.sc-step'); 398 var codeBody = container.querySelector('.sc-code-body'); 399 var stepBadge = container.querySelector('.sc-step-badge'); 400 var pips = container.querySelectorAll('.sc-pip'); 401 var currentStep = -1; 402 403 function parseLines(el) { 404 if (!el) return []; 405 var items = el.querySelectorAll('.sc-line'); 406 return Array.from(items).map(function(line) { 407 return { id: line.dataset.id, html: line.innerHTML, focused: line.classList.contains('sc-focused') }; 408 }); 409 } 410 411 function renderStep(index) { 412 if (index === currentStep || index < 0 || index >= steps.length) return; 413 414 var stepEl = steps[index]; 415 var codeSlot = stepEl.querySelector('.sc-code-slot'); 416 var newLines = parseLines(codeSlot); 417 var oldLines = parseLines(codeBody); 418 var oldById = {}; 419 oldLines.forEach(function(l) { oldById[l.id] = l; }); 420 var newById = {}; 421 newLines.forEach(function(l) { newById[l.id] = l; }); 422 423 // Determine exiting lines 424 var exiting = oldLines.filter(function(l) { return !newById[l.id]; }); 425 426 // Animate exit 427 exiting.forEach(function(l, i) { 428 var el = codeBody.querySelector('[data-id="' + l.id + '"]'); 429 if (el) { 430 el.style.animationDelay = (i * 30) + 'ms'; 431 el.classList.add('sc-exiting'); 432 } 433 }); 434 435 var exitTime = exiting.length > 0 ? 200 + exiting.length * 30 : 0; 436 437 setTimeout(function() { 438 // Rebuild DOM 439 codeBody.innerHTML = ''; 440 var firstNew = null; 441 newLines.forEach(function(l, i) { 442 var div = document.createElement('div'); 443 var isNew = !oldById[l.id]; 444 div.className = 'sc-line' + (l.focused ? ' sc-focused' : '') + (isNew ? ' sc-entering' : ''); 445 div.dataset.id = l.id; 446 div.innerHTML = '<span class="sc-line-number">' + (i + 1) + '</span>' + l.html; 447 if (isNew) { 448 div.style.animationDelay = (i * 25) + 'ms'; 449 if (!firstNew) firstNew = div; 450 } 451 codeBody.appendChild(div); 452 }); 453 454 // Scroll to first new line, with some context above 455 if (firstNew) { 456 var lineH = firstNew.offsetHeight || 24; 457 var scrollTarget = firstNew.offsetTop - lineH * 2; 458 codeBody.scrollTo({ top: Math.max(0, scrollTarget), behavior: 'smooth' }); 459 } 460 461 // Update badge and pips 462 if (stepBadge) stepBadge.textContent = (index + 1) + ' / ' + steps.length; 463 pips.forEach(function(pip, i) { 464 pip.classList.toggle('sc-active', i === index); 465 }); 466 }, exitTime); 467 468 currentStep = index; 469 } 470 471 // Set up IntersectionObserver 472 var observer = new IntersectionObserver(function(entries) { 473 entries.forEach(function(entry) { 474 if (entry.isIntersecting) { 475 var idx = parseInt(entry.target.dataset.stepIndex, 10); 476 renderStep(idx); 477 } 478 }); 479 }, { 480 rootMargin: '-30% 0px -30% 0px', 481 threshold: 0 482 }); 483 484 steps.forEach(function(step) { observer.observe(step); }); 485 486 // Initialize first step 487 renderStep(0); 488 489 // Playground overlay 490 var overlay = document.getElementById('sc-playground-overlay'); 491 var closeBtn = overlay ? overlay.querySelector('.sc-playground-close') : null; 492 493 if (overlay && closeBtn) { 494 // Close button 495 closeBtn.addEventListener('click', function() { 496 overlay.classList.remove('sc-open'); 497 }); 498 499 // ESC key closes 500 document.addEventListener('keydown', function(e) { 501 if (e.key === 'Escape') overlay.classList.remove('sc-open'); 502 }); 503 504 // Click outside closes 505 overlay.addEventListener('click', function(e) { 506 if (e.target === overlay) overlay.classList.remove('sc-open'); 507 }); 508 } 509 510 // Try it buttons 511 container.querySelectorAll('.sc-playground-btn').forEach(function(btn) { 512 btn.addEventListener('click', function() { 513 var stepIndex = parseInt(btn.dataset.step, 10); 514 // Collect code from all steps up to and including this one 515 var allCode = []; 516 for (var si = 0; si <= stepIndex; si++) { 517 var slot = steps[si].querySelector('.sc-code-slot'); 518 if (slot) { 519 var lines = slot.querySelectorAll('.sc-line'); 520 var code = Array.from(lines).map(function(l) { 521 return l.textContent.replace(/^\d+/, ''); 522 }).join('\n'); 523 allCode.push(code); 524 } 525 } 526 var fullCode = allCode.join('\n\n'); 527 528 var editor = document.getElementById('sc-playground-x-ocaml'); 529 if (editor) { 530 editor.textContent = fullCode; 531 // Trigger re-initialization if x-ocaml supports it 532 if (editor.setSource) editor.setSource(fullCode); 533 } 534 535 if (overlay) overlay.classList.add('sc-open'); 536 }); 537 }); 538 } 539 540 // Initialize any uninitialised scrollycode containers. 541 function initAll() { 542 document.querySelectorAll('.sc-container').forEach(function(c) { 543 if (!c.dataset.scInit) { 544 c.dataset.scInit = '1'; 545 initScrollycode(c); 546 } 547 }); 548 } 549 550 // Run now if DOM is ready, otherwise wait. 551 function observe() { 552 new MutationObserver(function() { initAll(); }) 553 .observe(document.body, { childList: true, subtree: true }); 554 document.addEventListener('odoc-spa-loaded', function() { initAll(); }); 555 } 556 if (document.readyState === 'loading') { 557 document.addEventListener('DOMContentLoaded', function() { 558 initAll(); 559 observe(); 560 }); 561 } else { 562 initAll(); 563 observe(); 564 } 565})(); 566|} 567 568(** {1 HTML Generation} *) 569 570(** Generate the code lines HTML for a step's code slot *) 571let generate_code_lines code focus = 572 let lines = String.split_on_char '\n' code in 573 let buf = Buffer.create 1024 in 574 List.iteri 575 (fun i line -> 576 let line_num = i + 1 in 577 let focused = focus = [] || List.mem line_num focus in 578 let highlighted = highlight_ocaml line in 579 Buffer.add_string buf 580 (Printf.sprintf 581 "<div class=\"sc-line%s\" data-id=\"L%d\">%s</div>\n" 582 (if focused then " sc-focused" else "") 583 line_num highlighted)) 584 lines; 585 Buffer.contents buf 586 587(** Generate the mobile stacked layout with diffs between steps *) 588let generate_mobile_html steps = 589 let buf = Buffer.create 8192 in 590 Buffer.add_string buf "<div class=\"sc-mobile\">\n"; 591 let prev_code = ref None in 592 List.iteri (fun i step -> 593 Buffer.add_string buf 594 (Printf.sprintf " <div class=\"sc-mobile-step\">\n"); 595 Buffer.add_string buf 596 (Printf.sprintf " <div class=\"sc-mobile-step-num\">Step %02d</div>\n" (i + 1)); 597 if step.title <> "" then 598 Buffer.add_string buf 599 (Printf.sprintf " <h2>%s</h2>\n" (html_escape step.title)); 600 if step.prose <> "" then 601 Buffer.add_string buf 602 (Printf.sprintf " <p>%s</p>\n" (html_escape step.prose)); 603 (* Diff block *) 604 Buffer.add_string buf " <div class=\"sc-diff-block\">\n"; 605 let diff = match !prev_code with 606 | None -> 607 List.map (fun l -> Added l) (String.split_on_char '\n' step.code) 608 | Some prev -> 609 diff_lines prev step.code 610 in 611 Buffer.add_string buf (render_diff_html diff); 612 Buffer.add_string buf " </div>\n"; 613 Buffer.add_string buf 614 (Printf.sprintf " <button class=\"sc-playground-btn\" data-step=\"%d\">&#9654; Try it</button>\n" i); 615 Buffer.add_string buf " </div>\n"; 616 prev_code := Some step.code) 617 steps; 618 Buffer.add_string buf "</div>\n"; 619 Buffer.contents buf 620 621(** Generate the full scrollycode HTML. 622 Theme styling is handled externally via CSS — this produces 623 theme-agnostic semantic HTML. *) 624let generate_html ~title ~filename steps = 625 let buf = Buffer.create 16384 in 626 627 (* Container — no theme class, CSS custom properties handle theming *) 628 Buffer.add_string buf "<div class=\"sc-container\">\n"; 629 630 (* The page's {0} heading serves as the title — no hero needed. *) 631 ignore title; 632 633 (* Progress pips *) 634 Buffer.add_string buf "<nav class=\"sc-progress\">\n"; 635 List.iteri 636 (fun i _step -> 637 Buffer.add_string buf 638 (Printf.sprintf " <div class=\"sc-pip%s\"></div>\n" 639 (if i = 0 then " sc-active" else ""))) 640 steps; 641 Buffer.add_string buf "</nav>\n"; 642 643 (* Desktop layout *) 644 Buffer.add_string buf "<div class=\"sc-desktop\">\n"; 645 Buffer.add_string buf "<div class=\"sc-tutorial\">\n"; 646 647 (* Steps column *) 648 Buffer.add_string buf " <div class=\"sc-steps-col\">\n"; 649 List.iteri 650 (fun i step -> 651 Buffer.add_string buf 652 (Printf.sprintf 653 " <div class=\"sc-step\" data-step-index=\"%d\">\n" i); 654 Buffer.add_string buf 655 (Printf.sprintf 656 " <div class=\"sc-step-number\">Step %02d</div>\n" (i + 1)); 657 if step.title <> "" then 658 Buffer.add_string buf 659 (Printf.sprintf " <h2>%s</h2>\n" (html_escape step.title)); 660 if step.prose <> "" then 661 Buffer.add_string buf 662 (Printf.sprintf " <p>%s</p>\n" (html_escape step.prose)); 663 (* Hidden code slot for JS to read *) 664 Buffer.add_string buf " <div class=\"sc-code-slot\">\n"; 665 Buffer.add_string buf (generate_code_lines step.code step.focus); 666 Buffer.add_string buf " </div>\n"; 667 Buffer.add_string buf 668 (Printf.sprintf " <button class=\"sc-playground-btn\" data-step=\"%d\">&#9654; Try it</button>\n" i); 669 Buffer.add_string buf " </div>\n") 670 steps; 671 Buffer.add_string buf " </div>\n"; 672 673 (* Code column *) 674 Buffer.add_string buf " <div class=\"sc-code-col\">\n"; 675 Buffer.add_string buf " <div class=\"sc-code-panel\">\n"; 676 Buffer.add_string buf " <div class=\"sc-code-header\">\n"; 677 Buffer.add_string buf 678 " <div class=\"sc-dots\"><span></span><span></span><span></span></div>\n"; 679 Buffer.add_string buf 680 (Printf.sprintf " <span class=\"sc-filename\">%s</span>\n" 681 (html_escape filename)); 682 Buffer.add_string buf 683 (Printf.sprintf 684 " <span class=\"sc-step-badge\">1 / %d</span>\n" 685 (List.length steps)); 686 Buffer.add_string buf " </div>\n"; 687 Buffer.add_string buf " <div class=\"sc-code-body\">\n"; 688 (* Initial code from first step *) 689 (match steps with 690 | first :: _ -> Buffer.add_string buf (generate_code_lines first.code first.focus) 691 | [] -> ()); 692 Buffer.add_string buf " </div>\n"; 693 Buffer.add_string buf " </div>\n"; 694 Buffer.add_string buf " </div>\n"; 695 696 Buffer.add_string buf "</div>\n"; 697 Buffer.add_string buf "</div>\n"; 698 699 (* Mobile stacked layout *) 700 Buffer.add_string buf (generate_mobile_html steps); 701 702 (* Playground overlay *) 703 Buffer.add_string buf {|<div id="sc-playground-overlay" class="sc-playground-overlay"> 704 <div class="sc-playground-container"> 705 <div class="sc-playground-header"> 706 <span class="sc-playground-title">Playground</span> 707 <button class="sc-playground-close">&times;</button> 708 </div> 709 <div class="sc-playground-editor"> 710 <x-ocaml id="sc-playground-x-ocaml" run-on="click"></x-ocaml> 711 </div> 712 </div> 713</div> 714|}; 715 716 Buffer.contents buf 717 718(** {1 Extension Registration} *) 719 720let meta_tag_script name value = 721 Printf.sprintf 722 {|(function(){var m=document.createElement('meta');m.name='%s';m.content='%s';document.head.appendChild(m)})();|} 723 name value 724 725module Scrolly : Odoc_extension_api.Extension = struct 726 let prefix = "scrolly" 727 728 let to_document ~tag:_ content = 729 let tutorial_title, steps = extract_steps content in 730 let filename = "main.ml" in 731 let html = generate_html ~title:tutorial_title ~filename steps in 732 let block : Block.t = 733 [ 734 { 735 Odoc_document.Types.Block.attr = [ "scrollycode" ]; 736 desc = Raw_markup ("html", html); 737 }; 738 ] 739 in 740 let x_ocaml_worker_url = 741 match Sys.getenv_opt "ODOC_X_OCAML_WORKER" with 742 | Some url -> url 743 | None -> "../odoc-interactive-extension/universe/worker.js" 744 in 745 { 746 Odoc_extension_api.content = block; 747 overrides = []; 748 resources = [ 749 Css_url "extensions/scrollycode.css"; 750 Js_url "extensions/scrollycode.js"; 751 Js_inline (meta_tag_script "x-ocaml-backend" "jtw"); 752 Js_inline (meta_tag_script "x-ocaml-worker" x_ocaml_worker_url); 753 Js_url "_x-ocaml/x-ocaml.js"; 754 ]; 755 assets = []; 756 } 757end 758 759(* Register extension and structural CSS support file. *) 760let () = 761 Odoc_extension_api.Registry.register (module Scrolly); 762 Odoc_extension_api.Registry.register_support_file ~prefix:"scrolly" { 763 filename = "extensions/scrollycode.css"; 764 content = Inline Scrollycode_css.structural_css; 765 }; 766 Odoc_extension_api.Registry.register_support_file ~prefix:"scrolly" { 767 filename = "extensions/scrollycode.js"; 768 content = Inline shared_js; 769 }; 770 (* Find x-ocaml.js: env var, then dune build install dir (walk up from CWD). *) 771 let x_ocaml_js_path = 772 match Sys.getenv_opt "ODOC_X_OCAML_JS_PATH" with 773 | Some p -> Some p 774 | None -> 775 let target = "_build/install/default/share/x-ocaml/x-ocaml.js" in 776 let rec walk dir = 777 let candidate = Filename.concat dir target in 778 if Sys.file_exists candidate then Some candidate 779 else 780 let parent = Filename.dirname dir in 781 if parent = dir then None else walk parent 782 in 783 walk (Sys.getcwd ()) 784 in 785 (match x_ocaml_js_path with 786 | Some path -> 787 Odoc_extension_api.Registry.register_support_file ~prefix:"scrolly" { 788 filename = "_x-ocaml/x-ocaml.js"; 789 content = Copy_from path; 790 } 791 | None -> ())