My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

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

Remove

-995
-28
odoc-interactive-extension/cors_server.py
··· 1 - #!/usr/bin/env python3 2 - """HTTP server with CORS headers for cross-origin demo testing.""" 3 - import sys 4 - from http.server import HTTPServer, SimpleHTTPRequestHandler 5 - 6 - class CORSHandler(SimpleHTTPRequestHandler): 7 - def end_headers(self): 8 - self.send_header("Access-Control-Allow-Origin", "*") 9 - self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") 10 - self.send_header("Access-Control-Allow-Headers", "*") 11 - super().end_headers() 12 - 13 - def do_OPTIONS(self): 14 - self.send_response(200) 15 - self.end_headers() 16 - 17 - def log_message(self, format, *args): 18 - # Suppress request logging to keep test output clean 19 - pass 20 - 21 - if __name__ == "__main__": 22 - port = int(sys.argv[1]) if len(sys.argv) > 1 else 9090 23 - directory = sys.argv[2] if len(sys.argv) > 2 else "." 24 - import os 25 - os.chdir(directory) 26 - server = HTTPServer(("", port), CORSHandler) 27 - print(f"CORS server on http://localhost:{port} serving {directory}") 28 - server.serve_forever()
-231
odoc-interactive-extension/deploy.sh
··· 1 - #!/bin/bash 2 - # Deploy interactive OCaml demo pages. 3 - # 4 - # This script builds 6 demo pages that showcase in-browser OCaml evaluation 5 - # using the odoc-interactive-extension and js_top_worker (jtw). 6 - # 7 - # ─── One-time setup ────────────────────────────────────────────────────── 8 - # 9 - # This script uses `jtw opam` to build universes from local opam switches. 10 - # In production, `day10 batch --with-jtw` does this at scale inside 11 - # containers (see docs/jtw-admin-guide.md). The manual switch setup 12 - # below is the local-dev path for running these demos. 13 - # 14 - # 1. Create the opam switches. The "default" switch is used for building 15 - # the monorepo and for the yojson 3.x universes. 16 - # 17 - # # default switch (should already exist) 18 - # opam switch create default ocaml-base-compiler.5.4.1 19 - # eval $(opam env --switch default --set-switch) 20 - # opam install yojson 21 - # 22 - # # yojson 2.x switch (for demo2_v2) 23 - # opam switch create demo-yojson-v2 ocaml-base-compiler.5.4.1 24 - # eval $(opam env --switch demo-yojson-v2 --set-switch) 25 - # opam install yojson.2.2.2 26 - # 27 - # # OxCaml switch (for demo3_oxcaml) 28 - # opam switch create 5.2.0+ox \ 29 - # --repos ox=git+https://github.com/oxcaml/opam-repository.git,default 30 - # 31 - # 2. Pin and install js_top_worker packages in every switch that needs a 32 - # universe. From the monorepo root: 33 - # 34 - # for sw in default demo-yojson-v2 5.2.0+ox; do 35 - # eval $(opam env --switch $sw --set-switch) 36 - # opam pin add js_top_worker . --no-action 37 - # opam pin add js_top_worker-web . --no-action 38 - # opam install js_top_worker js_top_worker-web 39 - # done 40 - # 41 - # 3. Build and install the monorepo tools (jtw, odoc, the extension) 42 - # into the default switch: 43 - # 44 - # eval $(opam env --switch default --set-switch) 45 - # dune build @install && dune install 46 - # 47 - # After this, `jtw opam --help` should work. 48 - # 49 - # ─── Production alternative (day10) ───────────────────────────────────── 50 - # 51 - # For building universes at scale (e.g. all of opam), use day10 instead 52 - # of manual switches. day10 runs builds in OCI containers with cached 53 - # overlay layers. See day10/docs/ADMIN_GUIDE.md for full details. 54 - # 55 - # dune exec -- day10 batch \ 56 - # --cache-dir /var/cache/day10 \ 57 - # --opam-repository /var/cache/opam-repository \ 58 - # --local-repo /path/to/js_top_worker \ 59 - # --with-jtw \ 60 - # --jtw-output /var/www/jtw \ 61 - # --html-output /var/www/docs \ 62 - # --with-doc \ 63 - # @packages.json 64 - # 65 - # --local-repo pins js_top_worker packages from a local checkout instead 66 - # of the default remote git repo. day10 discovers *.opam files in that 67 - # directory, bind-mounts it into the container, and uses it for pinning. 68 - # This is the easiest way to test local changes to js_top_worker. 69 - # 70 - # day10 handles switch creation, dependency solving, js_top_worker 71 - # installation, and per-package universe assembly automatically. 72 - # 73 - # ─── Usage ─────────────────────────────────────────────────────────────── 74 - # 75 - # ./deploy.sh # build everything and serve on port 8080 76 - # ./deploy.sh --no-serve # build only, don't start HTTP server 77 - 78 - set -euo pipefail 79 - 80 - MONO=$(cd "$(dirname "$0")/.." && pwd) 81 - DOC_HTML="$MONO/_build/default/_doc/_html/odoc-interactive-extension" 82 - ODOCL="$MONO/_build/default/_doc/_odocl/odoc-interactive-extension" 83 - OPAM_ODOC="$HOME/.opam/default/bin/odoc" 84 - UNIVERSES=$(mktemp -d) 85 - SERVE=true 86 - 87 - if [[ "${1:-}" == "--no-serve" ]]; then 88 - SERVE=false 89 - fi 90 - 91 - echo "=== Step 1: Install odoc extension into opam switch ===" 92 - cd "$MONO" 93 - export OPAMSWITCH=default 94 - eval "$(opam env)" 95 - dune build @install 96 - dune install 2>&1 | tail -5 97 - 98 - echo "" 99 - echo "=== Step 2: Build odoc docs (generates .odocl + base HTML) ===" 100 - # @doc may exit non-zero due to warnings in other packages (e.g. odoc's own 101 - # cheatsheet referencing cmdliner). We tolerate that as long as the odocl 102 - # files we need were actually produced. 103 - dune build @doc 2>&1 | tail -5 || true 104 - if [ ! -f "$ODOCL/page-demo1.odocl" ]; then 105 - echo "ERROR: odocl files not generated — dune build @doc failed." >&2 106 - exit 1 107 - fi 108 - 109 - echo "" 110 - echo "=== Step 3: Regenerate demo HTML with opam odoc (has extension) ===" 111 - # dune's workspace-local odoc can't find dune-site plugins, so we use the 112 - # opam-installed one which has the extension registered. 113 - for page in demo1 demo2_v2 demo2_v3 demo3_oxcaml demo4_crossorigin demo5_multiverse; do 114 - chmod u+w "$DOC_HTML/${page}.html" 2>/dev/null || true 115 - "$OPAM_ODOC" html-generate "$ODOCL/page-${page}.odocl" \ 116 - -o "$DOC_HTML/.." \ 117 - --support-uri=_odoc_support 2>&1 118 - echo " regenerated ${page}.html" 119 - done 120 - 121 - echo "" 122 - echo "=== Step 4: Build x-ocaml.js ===" 123 - dune build x-ocaml/src/x_ocaml.bc.js 124 - 125 - echo "" 126 - echo "=== Step 5: Build universes ===" 127 - 128 - # 5a. Default universe (yojson 3.0 — used by demo1) 129 - echo " building default universe (yojson, default switch)..." 130 - jtw opam --switch=default -o "$UNIVERSES/default" yojson 131 - 132 - # 5b. Yojson v2 universe 133 - echo " building yojson-v2 universe (demo-yojson-v2 switch)..." 134 - jtw opam --switch=demo-yojson-v2 -o "$UNIVERSES/v2" yojson 135 - 136 - # 5c. Yojson v3 universe (same as default, but separate dir for isolation) 137 - echo " building yojson-v3 universe (default switch)..." 138 - jtw opam --switch=default -o "$UNIVERSES/v3" yojson 139 - 140 - # 5d. OxCaml universe (stdlib only) 141 - echo " building oxcaml universe (5.2.0+ox switch)..." 142 - jtw opam --switch=5.2.0+ox -o "$UNIVERSES/oxcaml" 143 - 144 - echo "" 145 - echo "=== Step 6: Deploy assets into doc HTML output ===" 146 - 147 - # _x-ocaml runtime (shared by all pages) 148 - mkdir -p "$DOC_HTML/_x-ocaml" 149 - chmod -R u+w "$DOC_HTML/_x-ocaml" 2>/dev/null || true 150 - cp "$MONO/_build/default/x-ocaml/src/x_ocaml.bc.js" "$DOC_HTML/_x-ocaml/x-ocaml.js" 151 - cp "$UNIVERSES/default/worker.js" "$DOC_HTML/_x-ocaml/worker.js" 152 - echo " deployed _x-ocaml/" 153 - 154 - # Make deployed dirs writable so re-runs can overwrite them. 155 - chmod -R u+w "$DOC_HTML/universe" "$DOC_HTML/universe-v2" \ 156 - "$DOC_HTML/universe-v3" "$DOC_HTML/universe-oxcaml" 2>/dev/null || true 157 - 158 - # demo1: ./universe 159 - rm -rf "$DOC_HTML/universe" 160 - cp -r "$UNIVERSES/default" "$DOC_HTML/universe" 161 - echo " deployed universe/ (demo1)" 162 - 163 - # demo2_v2: ./universe-v2 164 - rm -rf "$DOC_HTML/universe-v2" 165 - cp -r "$UNIVERSES/v2" "$DOC_HTML/universe-v2" 166 - echo " deployed universe-v2/ (demo2_v2)" 167 - 168 - # demo2_v3: ./universe-v3 169 - rm -rf "$DOC_HTML/universe-v3" 170 - cp -r "$UNIVERSES/v3" "$DOC_HTML/universe-v3" 171 - echo " deployed universe-v3/ (demo2_v3)" 172 - 173 - # demo3_oxcaml: ./universe-oxcaml 174 - rm -rf "$DOC_HTML/universe-oxcaml" 175 - cp -r "$UNIVERSES/oxcaml" "$DOC_HTML/universe-oxcaml" 176 - echo " deployed universe-oxcaml/ (demo3_oxcaml)" 177 - 178 - # Copy x-ocaml.js into each universe (for cross-origin blob: fallback) 179 - for d in universe universe-v2 universe-v3 universe-oxcaml; do 180 - cp "$DOC_HTML/_x-ocaml/x-ocaml.js" "$DOC_HTML/$d/x-ocaml.js" 181 - done 182 - 183 - # Cross-origin universe (same content as default, served on port 9090) 184 - CROSSORIGIN_DIR="$DOC_HTML/../_crossorigin_universes" 185 - chmod -R u+w "$CROSSORIGIN_DIR" 2>/dev/null || true 186 - rm -rf "$CROSSORIGIN_DIR" 187 - mkdir -p "$CROSSORIGIN_DIR" 188 - cp -r "$DOC_HTML/universe" "$CROSSORIGIN_DIR/universe" 189 - echo " deployed _crossorigin_universes/ (for port 9090)" 190 - 191 - # Multiverse (per-package layout with universe linking) 192 - echo "" 193 - echo "=== Step 7: Build multiverse (per-package layout) ===" 194 - MULTIVERSE_DIR="$DOC_HTML/../_multiverse" 195 - chmod -R u+w "$MULTIVERSE_DIR" 2>/dev/null || true 196 - rm -rf "$MULTIVERSE_DIR" 197 - jtw opam-all --switch=default yojson -o "$MULTIVERSE_DIR" 198 - cp "$MONO/_build/default/x-ocaml/src/x_ocaml.bc.js" "$MULTIVERSE_DIR/x-ocaml.js" 199 - echo " deployed _multiverse/ (for port 9090)" 200 - 201 - echo "" 202 - echo "=== Done ===" 203 - echo "Demo pages at: $DOC_HTML/" 204 - echo "" 205 - echo " demo1.html — basic OCaml + yojson (default switch)" 206 - echo " demo2_v2.html — yojson 2.2.2 (demo-yojson-v2 switch)" 207 - echo " demo2_v3.html — yojson 3.0.0 (default switch)" 208 - echo " demo3_oxcaml.html — OxCaml extensions (5.2.0+ox switch)" 209 - echo " demo4_crossorigin.html — cross-origin loading (needs port 9090)" 210 - echo " demo5_multiverse.html — multiverse per-package layout (needs port 9090)" 211 - echo "" 212 - echo "Cross-origin demos (demo4, demo5) require a CORS-enabled server on port 9090." 213 - echo "They serve different directories, so you can only test one at a time:" 214 - echo "" 215 - echo " demo4 (cross-origin):" 216 - echo " python3 $MONO/odoc-interactive-extension/cors_server.py 9090 $DOC_HTML/../_crossorigin_universes" 217 - echo "" 218 - echo " demo5 (multiverse):" 219 - echo " python3 $MONO/odoc-interactive-extension/cors_server.py 9090 $DOC_HTML/../_multiverse" 220 - 221 - # Clean up 222 - rm -rf "$UNIVERSES" 223 - 224 - if $SERVE; then 225 - echo "" 226 - echo "Starting HTTP server on http://localhost:8080" 227 - echo "Visit: http://localhost:8080/odoc-interactive-extension/demo1.html" 228 - echo "" 229 - cd "$DOC_HTML/.." 230 - exec python3 -m http.server 8080 231 - fi
-39
odoc-interactive-extension/doc/demo1.mld
··· 1 - {0 Interactive OCaml Demo} 2 - 3 - @x-ocaml.universe ./universe 4 - @x-ocaml.worker ./universe/worker.js 5 - 6 - This page demonstrates interactive OCaml code cells powered by 7 - [x-ocaml] and [js_top_worker]. 8 - 9 - {1 Basic Expressions} 10 - 11 - Try evaluating some OCaml expressions: 12 - 13 - {@ocaml[ 14 - 1 + 2 * 3 15 - ]} 16 - 17 - {@ocaml[ 18 - let greet name = Printf.sprintf "Hello, %s!" name 19 - 20 - let () = print_endline (greet "World") 21 - ]} 22 - 23 - {1 Using Yojson} 24 - 25 - These cells use the [yojson] library loaded from the universe: 26 - 27 - {@ocaml[ 28 - #require "yojson" 29 - ]} 30 - 31 - {@ocaml[ 32 - let json = `Assoc [ 33 - ("name", `String "OCaml"); 34 - ("version", `Float 5.4); 35 - ("features", `List [`String "modules"; `String "types"]) 36 - ] 37 - 38 - let () = print_endline (Yojson.Safe.pretty_to_string json) 39 - ]}
-24
odoc-interactive-extension/doc/demo2_v2.mld
··· 1 - {0 Yojson v2 Demo} 2 - 3 - @x-ocaml.universe ./universe-v2 4 - @x-ocaml.worker ./universe-v2/worker.js 5 - 6 - This page uses {b yojson 2.2.2} from a separate universe directory. 7 - 8 - {@ocaml[ 9 - #require "yojson" 10 - ]} 11 - 12 - {@ocaml[ 13 - (* Yojson 2.x API *) 14 - let json = `Assoc [("key", `String "value")] 15 - let s = Yojson.Safe.to_string json 16 - let () = print_endline s 17 - ]} 18 - 19 - {@ocaml[ 20 - (* Yojson 2.x: Yojson.Safe.prettify is a string->string function *) 21 - let ugly = Yojson.Safe.to_string (`Assoc [("compact", `Bool true); ("data", `List [`Int 1; `Int 2; `Int 3])]) 22 - let pretty = Yojson.Safe.prettify ugly 23 - let () = print_endline pretty 24 - ]}
-24
odoc-interactive-extension/doc/demo2_v3.mld
··· 1 - {0 Yojson v3 Demo} 2 - 3 - @x-ocaml.universe ./universe-v3 4 - @x-ocaml.worker ./universe-v3/worker.js 5 - 6 - This page uses {b yojson 3.0.0} from a separate universe directory. 7 - 8 - {@ocaml[ 9 - #require "yojson" 10 - ]} 11 - 12 - {@ocaml[ 13 - (* Yojson 3.0 API *) 14 - let json = `Assoc [("key", `String "value")] 15 - let s = Yojson.Safe.to_string json 16 - let () = print_endline s 17 - ]} 18 - 19 - {@ocaml[ 20 - (* Build and query JSON *) 21 - let parsed = `Assoc [("x", `Int 42); ("y", `String "hello")] 22 - let x = Yojson.Safe.Util.member "x" parsed 23 - let () = Printf.printf "x = %s\n" (Yojson.Safe.to_string x) 24 - ]}
-84
odoc-interactive-extension/doc/demo3_oxcaml.mld
··· 1 - {0 OxCaml Interactive Demo} 2 - 3 - @x-ocaml.universe ./universe-oxcaml 4 - @x-ocaml.worker ./universe-oxcaml/worker.js 5 - 6 - This page demonstrates OxCaml language extensions running interactively 7 - in the browser via [x-ocaml] and [js_top_worker]. 8 - 9 - {1 List Comprehensions} 10 - 11 - OxCaml adds Python/Haskell-style list and array comprehensions: 12 - 13 - {@ocaml[ 14 - let squares = [ x * x for x = 1 to 10 ] 15 - 16 - let () = List.iter (fun x -> Printf.printf "%d " x) squares 17 - ]} 18 - 19 - {@ocaml[ 20 - let evens = [ x for x = 1 to 20 when x mod 2 = 0 ] 21 - 22 - let () = Printf.printf "Evens: %s\n" 23 - (String.concat ", " (List.map string_of_int evens)) 24 - ]} 25 - 26 - Nested comprehensions produce the cartesian product: 27 - 28 - {@ocaml[ 29 - let pairs = [ (x, y) for x = 1 to 3 for y = 1 to 3 when x <> y ] 30 - 31 - let () = List.iter (fun (x, y) -> Printf.printf "(%d,%d) " x y) pairs 32 - ]} 33 - 34 - {1 Array Comprehensions} 35 - 36 - Array comprehensions create arrays using the same syntax as list comprehensions: 37 - 38 - {@ocaml[ 39 - let squares = [| x * x for x = 1 to 10 |] 40 - 41 - let () = Array.iter (fun x -> Printf.printf "%d " x) squares 42 - ]} 43 - 44 - {@ocaml[ 45 - let fibs = 46 - let a = Array.make 10 0 in 47 - a.(0) <- 1; a.(1) <- 1; 48 - for i = 2 to 9 do a.(i) <- a.(i-1) + a.(i-2) done; 49 - [| a.(i) for i = 0 to 9 |] 50 - 51 - let () = Array.iter (fun x -> Printf.printf "%d " x) fibs 52 - ]} 53 - 54 - {1 Let Mutable} 55 - 56 - [let mutable] provides mutable local variables without heap allocation: 57 - 58 - {@ocaml[ 59 - let triangle n = 60 - let mutable total = 0 in 61 - for i = 1 to n do 62 - total <- total + i 63 - done; 64 - total 65 - 66 - let () = Printf.printf "triangle 10 = %d\n" (triangle 10) 67 - ]} 68 - 69 - {@ocaml[ 70 - let fizzbuzz n = 71 - let mutable result = [] in 72 - for i = n downto 1 do 73 - let s = match i mod 3, i mod 5 with 74 - | 0, 0 -> "FizzBuzz" 75 - | 0, _ -> "Fizz" 76 - | _, 0 -> "Buzz" 77 - | _ -> string_of_int i 78 - in 79 - result <- s :: result 80 - done; 81 - result 82 - 83 - let () = print_endline (String.concat " " (fizzbuzz 15)) 84 - ]}
-36
odoc-interactive-extension/doc/demo4_crossorigin.mld
··· 1 - {0 Cross-Origin Demo} 2 - 3 - @x-ocaml.universe http://localhost:9090/universe 4 - @x-ocaml.worker http://localhost:9090/universe/worker.js 5 - 6 - This page demonstrates {b cross-origin} loading of OCaml universes. 7 - The page is served from [localhost:8080] while the worker and libraries 8 - are loaded from [localhost:9090], exercising the blob: URL worker 9 - creation and sync XHR + eval library loading code paths. 10 - 11 - {1 Basic Expression} 12 - 13 - {@ocaml[ 14 - 1 + 2 * 3 15 - ]} 16 - 17 - {@ocaml[ 18 - let greet name = Printf.sprintf "Hello, %s!" name 19 - 20 - let () = print_endline (greet "Cross-Origin World") 21 - ]} 22 - 23 - {1 Loading a Library} 24 - 25 - {@ocaml[ 26 - #require "yojson" 27 - ]} 28 - 29 - {@ocaml[ 30 - let json = `Assoc [ 31 - ("origin", `String "cross-origin"); 32 - ("port", `Int 9090) 33 - ] 34 - 35 - let () = print_endline (Yojson.Safe.pretty_to_string json) 36 - ]}
-46
odoc-interactive-extension/doc/demo5_multiverse.mld
··· 1 - {0 Multiverse Demo} 2 - 3 - @x-ocaml.universe http://localhost:9090/yojson 4 - @x-ocaml.worker http://localhost:9090/worker.js 5 - 6 - This page demonstrates a {b multiverse} layout where each package is 7 - built and hosted independently. The universe URL points at 8 - [localhost:9090/yojson], which contains only yojson's own artifacts. 9 - Stdlib is discovered automatically via the ["universes"] link in 10 - yojson's [findlib_index.json]: 11 - 12 - {v 13 - yojson/findlib_index.json: 14 - {"meta_files": ["lib/yojson/META"], "universes": ["../stdlib"]} 15 - v} 16 - 17 - This is how a large-scale host like [ocaml.org] would serve packages: 18 - each package is a small self-contained directory, with links to its 19 - dependencies. 20 - 21 - {1 Basic Expression} 22 - 23 - {@ocaml[ 24 - 1 + 2 * 3 25 - ]} 26 - 27 - {@ocaml[ 28 - let greet name = Printf.sprintf "Hello, %s!" name 29 - 30 - let () = print_endline (greet "Multiverse World") 31 - ]} 32 - 33 - {1 Loading a Library} 34 - 35 - {@ocaml[ 36 - #require "yojson" 37 - ]} 38 - 39 - {@ocaml[ 40 - let json = `Assoc [ 41 - ("source", `String "multiverse"); 42 - ("linked_universes", `Int 2) 43 - ] 44 - 45 - let () = print_endline (Yojson.Safe.pretty_to_string json) 46 - ]}
-2
odoc-interactive-extension/doc/dune
··· 1 - (documentation 2 - (package odoc-interactive-extension))
-11
odoc-interactive-extension/dune-project
··· 1 - (lang dune 3.18) 2 - (using dune_site 0.1) 3 - (name odoc-interactive-extension) 4 - (generate_opam_files true) 5 - 6 - (package 7 - (name odoc-interactive-extension) 8 - (synopsis "Interactive OCaml code cells for odoc documentation") 9 - (depends 10 - (ocaml (>= 4.14)) 11 - odoc))
-25
odoc-interactive-extension/odoc-interactive-extension.opam
··· 1 - # This file is generated by dune, edit dune-project instead 2 - opam-version: "2.0" 3 - synopsis: "Interactive OCaml code cells for odoc documentation" 4 - depends: [ 5 - "dune" {>= "3.18"} 6 - "ocaml" {>= "4.14"} 7 - "odoc" 8 - ] 9 - build: [ 10 - ["dune" "subst"] {dev} 11 - [ 12 - "dune" 13 - "build" 14 - "-p" 15 - name 16 - "-j" 17 - jobs 18 - "--promote-install-files=false" 19 - "@install" 20 - "@runtest" {with-test} 21 - "@doc" {with-doc} 22 - ] 23 - ["dune" "install" "-p" name "--create-install-files" name] 24 - ] 25 - x-maintenance-intent: ["(latest)"]
-10
odoc-interactive-extension/src/dune
··· 1 - (library 2 - (name interactive_extension) 3 - (public_name odoc-interactive-extension.impl) 4 - (libraries odoc.extension_api)) 5 - 6 - (plugin 7 - (name odoc-interactive-extension) 8 - (package odoc-interactive-extension) 9 - (libraries odoc-interactive-extension.impl) 10 - (site (odoc extensions)))
-185
odoc-interactive-extension/src/interactive_extension.ml
··· 1 - (** Interactive OCaml extension for odoc. 2 - 3 - Provides two extension handlers: 4 - 5 - - {b Code block}: [{@ocaml mode id=name [...]}] code blocks are 6 - transformed to [<x-ocaml>] custom elements with the appropriate 7 - data attributes. 8 - 9 - - {b Tag}: [@x-ocaml.universe], [@x-ocaml.requires], etc. configure 10 - page-level settings emitted as [<meta>] tags in the HTML head. *) 11 - 12 - module Api = Odoc_extension_api 13 - module Block = Api.Block 14 - module Inline = Api.Inline 15 - 16 - (** {1 Page-level configuration} 17 - 18 - Accumulated during tag processing and consumed when emitting 19 - code block resources. *) 20 - 21 - let universe_url = ref None 22 - let worker_url = ref None 23 - let requires : string list ref = ref [] 24 - 25 - (** {1 HTML helpers} *) 26 - 27 - let html_escape s = 28 - let buf = Buffer.create (String.length s) in 29 - String.iter (fun c -> 30 - match c with 31 - | '&' -> Buffer.add_string buf "&amp;" 32 - | '<' -> Buffer.add_string buf "&lt;" 33 - | '>' -> Buffer.add_string buf "&gt;" 34 - | '"' -> Buffer.add_string buf "&quot;" 35 - | c -> Buffer.add_char buf c 36 - ) s; 37 - Buffer.contents buf 38 - 39 - (** {1 Config tag handler} *) 40 - 41 - let js_escape s = 42 - let buf = Buffer.create (String.length s) in 43 - String.iter (fun c -> 44 - match c with 45 - | '\'' -> Buffer.add_string buf "\\'" 46 - | '\\' -> Buffer.add_string buf "\\\\" 47 - | c -> Buffer.add_char buf c 48 - ) s; 49 - Buffer.contents buf 50 - 51 - let meta_tag_script name value = 52 - Printf.sprintf 53 - {|(function(){var m=document.createElement('meta');m.name='%s';m.content='%s';document.head.appendChild(m)})();|} 54 - (js_escape name) (js_escape value) 55 - 56 - module X_ocaml_config : Api.Extension = struct 57 - let prefix = "x-ocaml" 58 - 59 - let to_document ~tag content = 60 - let subtag = 61 - match String.split_on_char '.' tag with 62 - | _ :: rest -> String.concat "." rest 63 - | _ -> tag 64 - in 65 - let text = Api.text_of_nestable_block_elements content in 66 - let text = String.trim text in 67 - let resources = match subtag with 68 - | "universe" -> 69 - universe_url := Some text; 70 - [ Api.Js_inline (meta_tag_script "x-ocaml-universe" text) ] 71 - | "worker" -> 72 - worker_url := Some text; 73 - [ Api.Js_inline (meta_tag_script "x-ocaml-worker" text) ] 74 - | "requires" -> 75 - let pkgs = 76 - List.filter (fun s -> s <> "") 77 - (List.map String.trim (String.split_on_char ',' text)) 78 - in 79 - requires := pkgs; 80 - [ Api.Js_inline 81 - (meta_tag_script "x-ocaml-packages" (String.concat "," pkgs)) ] 82 - | _ -> [] 83 - in 84 - { Api.content = []; overrides = []; resources; assets = [] } 85 - end 86 - 87 - (** {1 Code block handler} *) 88 - 89 - (** Recognised cell modes — first bare tag matching one of these wins. *) 90 - let mode_tags = [ "interactive"; "exercise"; "test"; "hidden" ] 91 - 92 - module X_ocaml_code : Api.Code_Block_Extension = struct 93 - let prefix = "ocaml" 94 - 95 - let to_document meta code = 96 - let tags = meta.Api.tags in 97 - (* Mode: first bare tag in mode_tags, default "interactive" *) 98 - let mode = 99 - let bare = Api.get_all_tags tags in 100 - match List.find_opt (fun t -> List.mem t mode_tags) bare with 101 - | Some m -> m 102 - | None -> "interactive" 103 - in 104 - let id_attr = Api.get_binding "id" tags in 105 - let for_attr = Api.get_binding "for" tags in 106 - let env_attr = Api.get_binding "env" tags in 107 - let merlin_off = Api.has_tag "no-merlin" tags in 108 - let run_on = Api.get_binding "run-on" tags in 109 - let esc = html_escape in 110 - (* Build attribute string *) 111 - let attrs = 112 - List.filter_map Fun.id [ 113 - Some (Printf.sprintf "mode=\"%s\"" (esc mode)); 114 - Option.map (fun v -> Printf.sprintf "data-id=\"%s\"" (esc v)) id_attr; 115 - Option.map (fun v -> Printf.sprintf "data-for=\"%s\"" (esc v)) for_attr; 116 - Option.map (fun v -> Printf.sprintf "data-env=\"%s\"" (esc v)) env_attr; 117 - (if merlin_off then Some "data-merlin=\"false\"" else None); 118 - Option.map (fun v -> Printf.sprintf "run-on=\"%s\"" (esc v)) run_on; 119 - ] 120 - in 121 - let attr_str = String.concat " " attrs in 122 - let html = 123 - Printf.sprintf "<x-ocaml %s>%s</x-ocaml>" attr_str (html_escape code) 124 - in 125 - let block : Block.t = [{ 126 - attr = [ "x-ocaml-cell" ]; 127 - desc = Raw_markup ("html", html); 128 - }] in 129 - (* Resources: inject the x-ocaml.js script tag with configuration 130 - attributes. The script uses document.currentScript to read 131 - src-worker, backend, etc. so we need a real <script> element 132 - rather than a plain Js_url. We use Js_inline with a guard so 133 - it only executes once (resources are de-duplicated by odoc, but 134 - the guard is belt-and-braces for any edge cases). 135 - 136 - x-ocaml.js and worker.js are always loaded from a local path 137 - (same origin as the page) to avoid cross-origin Worker restrictions. 138 - The universe URL (which may be cross-origin) is communicated via 139 - the <meta name="x-ocaml-universe"> tag, which x-ocaml.js reads 140 - to configure findlib_index fetching. *) 141 - let local_base = "./_x-ocaml" in 142 - let backend = match !universe_url with Some _ -> "jtw" | None -> "builtin" in 143 - let script_loader = Printf.sprintf 144 - {|(function(){if(window.__xOcamlLoaded)return;window.__xOcamlLoaded=true;var s=document.createElement('script');s.src='%s/x-ocaml.js';s.setAttribute('src-worker','%s/worker.js');s.setAttribute('backend','%s');document.head.appendChild(s)})();|} 145 - (js_escape local_base) (js_escape local_base) (js_escape backend) 146 - in 147 - let resources = [ Api.Js_inline script_loader ] in 148 - Some { Api.content = block; overrides = []; resources; assets = [] } 149 - end 150 - 151 - (** {1 Extension documentation} *) 152 - 153 - let config_info : Api.extension_info = { 154 - info_kind = `Tag; 155 - info_prefix = "x-ocaml"; 156 - info_description = 157 - "Page-level configuration for interactive OCaml cells. \ 158 - Sub-tags: .universe (base URL), .requires (comma-separated packages)."; 159 - info_options = []; 160 - info_example = Some "@x-ocaml.universe ./universe"; 161 - } 162 - 163 - let code_info : Api.extension_info = { 164 - info_kind = `Code_block; 165 - info_prefix = "ocaml"; 166 - info_description = 167 - "Interactive OCaml code cell. Bare tags set the mode \ 168 - (interactive, exercise, test, hidden). Key=value bindings \ 169 - set id, for, env, and run-on attributes."; 170 - info_options = [ 171 - { opt_name = "id"; opt_description = "Cell identifier for test linking"; opt_default = None }; 172 - { opt_name = "for"; opt_description = "Target exercise cell id (for test cells)"; opt_default = None }; 173 - { opt_name = "env"; opt_description = "Environment scope"; opt_default = None }; 174 - { opt_name = "run-on"; opt_description = "When to run: load or click"; opt_default = Some "load" }; 175 - ]; 176 - info_example = Some "{@ocaml exercise id=double[let double x = x * 2]}"; 177 - } 178 - 179 - (** {1 Registration} *) 180 - 181 - let () = 182 - Api.Registry.register (module X_ocaml_config); 183 - Api.Registry.register_code_block (module X_ocaml_code); 184 - Api.Registry.register_extension_info config_info; 185 - Api.Registry.register_extension_info code_info
-82
odoc-interactive-extension/test_crossorigin.js
··· 1 - // test_crossorigin.js — Playwright test for cross-origin demo 2 - const { chromium } = require('playwright'); 3 - 4 - (async () => { 5 - const browser = await chromium.launch(); 6 - const page = await browser.newPage(); 7 - 8 - const consoleMessages = []; 9 - page.on('console', msg => consoleMessages.push(msg.text())); 10 - 11 - const errors = []; 12 - page.on('pageerror', err => errors.push(err.message)); 13 - 14 - console.log('Navigating to demo4_crossorigin.html...'); 15 - await page.goto('http://localhost:8080/odoc-interactive-extension/demo4_crossorigin.html'); 16 - 17 - // Wait for cells to render and execute (worker init + eval) 18 - console.log('Waiting for cells to execute...'); 19 - await page.waitForTimeout(15000); 20 - 21 - // Check for cross-origin indicators in console 22 - const hasCrossOriginWorker = consoleMessages.some(m => 23 - m.includes('9090')); 24 - const hasCrossOriginFetch = consoleMessages.some(m => 25 - m.includes('Cross-origin import via fetch+eval')); 26 - const hasInitFinished = consoleMessages.some(m => 27 - m.includes('init() finished')); 28 - const hasSetupFinished = consoleMessages.some(m => 29 - m.includes('setup() finished')); 30 - 31 - // Check for init_error 32 - const hasInitError = consoleMessages.some(m => 33 - m.includes('init_error')); 34 - 35 - // Check for correct output in page (x-ocaml uses Shadow DOM, so we 36 - // look at worker output messages in the console instead) 37 - const hasInt7 = consoleMessages.some(m => 38 - m.includes('int = 7')); 39 - const hasGreeting = consoleMessages.some(m => 40 - m.includes('Hello, Cross-Origin World')); 41 - 42 - console.log(''); 43 - console.log('=== Results ==='); 44 - console.log(` Worker loaded from port 9090: ${hasCrossOriginWorker ? 'YES' : 'NO'}`); 45 - console.log(` Cross-origin fetch+eval used: ${hasCrossOriginFetch ? 'YES' : 'NO'}`); 46 - console.log(` Init completed: ${hasInitFinished ? 'YES' : 'NO'}`); 47 - console.log(` Setup completed: ${hasSetupFinished ? 'YES' : 'NO'}`); 48 - console.log(` Init error: ${hasInitError ? 'YES (BAD)' : 'NO (good)'}`); 49 - console.log(` Output "int = 7": ${hasInt7 ? 'YES' : 'NO'}`); 50 - console.log(` Output "Hello, Cross-Origin": ${hasGreeting ? 'YES' : 'NO'}`); 51 - 52 - let passed = true; 53 - 54 - if (!hasInitFinished) { 55 - console.log('\nFAIL: Worker init did not complete'); 56 - passed = false; 57 - } 58 - if (!hasSetupFinished) { 59 - console.log('\nFAIL: Worker setup did not complete'); 60 - passed = false; 61 - } 62 - if (hasInitError) { 63 - console.log('\nFAIL: Worker reported init_error'); 64 - const errMsg = consoleMessages.find(m => m.includes('init_error')); 65 - console.log(` ${errMsg}`); 66 - passed = false; 67 - } 68 - if (!hasInt7 || !hasGreeting) { 69 - console.log('\nFAIL: Expected output not found in page'); 70 - passed = false; 71 - } 72 - 73 - if (passed) { 74 - console.log('\nPASS: Cross-origin demo working correctly'); 75 - } else { 76 - console.log('\n--- Console messages (last 40) ---'); 77 - consoleMessages.slice(-40).forEach(m => console.log(` ${m.substring(0, 150)}`)); 78 - } 79 - 80 - await browser.close(); 81 - process.exit(passed ? 0 : 1); 82 - })();
-54
odoc-interactive-extension/test_crossorigin.sh
··· 1 - #!/bin/bash 2 - # Test cross-origin demo end-to-end. 3 - # Starts page server (8080) and CORS universe server (9090), 4 - # then runs Playwright to verify cross-origin loading works. 5 - set -euo pipefail 6 - 7 - SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 8 - MONO=$(cd "$SCRIPT_DIR/.." && pwd) 9 - DOC_HTML="$MONO/_build/default/_doc/_html/odoc-interactive-extension" 10 - CROSSORIGIN_DIR="$MONO/_build/default/_doc/_html/_crossorigin_universes" 11 - 12 - cleanup() { 13 - [[ -n "${PAGE_PID:-}" ]] && kill "$PAGE_PID" 2>/dev/null || true 14 - [[ -n "${CORS_PID:-}" ]] && kill "$CORS_PID" 2>/dev/null || true 15 - wait 2>/dev/null || true 16 - } 17 - trap cleanup EXIT 18 - 19 - echo "=== Building demos ===" 20 - bash "$SCRIPT_DIR/deploy.sh" --no-serve 21 - 22 - echo "" 23 - echo "=== Starting servers ===" 24 - 25 - # Page server on port 8080 26 - cd "$DOC_HTML/.." 27 - python3 -m http.server 8080 &>/dev/null & 28 - PAGE_PID=$! 29 - 30 - # CORS universe server on port 9090 31 - python3 "$SCRIPT_DIR/cors_server.py" 9090 "$CROSSORIGIN_DIR" &>/dev/null & 32 - CORS_PID=$! 33 - 34 - sleep 2 35 - echo " Page server: http://localhost:8080 (PID $PAGE_PID)" 36 - echo " CORS server: http://localhost:9090 (PID $CORS_PID)" 37 - 38 - # Verify both servers respond 39 - curl -sf http://localhost:8080/odoc-interactive-extension/demo4_crossorigin.html > /dev/null \ 40 - || { echo "FAIL: page server not responding"; exit 1; } 41 - curl -sf http://localhost:9090/universe/findlib_index.json > /dev/null \ 42 - || { echo "FAIL: CORS server not responding"; exit 1; } 43 - 44 - # Verify CORS headers 45 - CORS_HEADER=$(curl -sI http://localhost:9090/universe/findlib_index.json | grep -i 'access-control-allow-origin' || true) 46 - if [[ -z "$CORS_HEADER" ]]; then 47 - echo "FAIL: CORS server missing Access-Control-Allow-Origin header" 48 - exit 1 49 - fi 50 - echo " CORS header: $CORS_HEADER" 51 - 52 - echo "" 53 - echo "=== Running Playwright test ===" 54 - NODE_PATH="$MONO/x-ocaml/test/node_modules" node "$SCRIPT_DIR/test_crossorigin.js"
-114
odoc-interactive-extension/test_multiverse.js
··· 1 - // test_multiverse.js — Playwright test for multiverse demo (per-package layout) 2 - const { chromium } = require('playwright'); 3 - 4 - (async () => { 5 - const browser = await chromium.launch(); 6 - const page = await browser.newPage(); 7 - 8 - const consoleMessages = []; 9 - page.on('console', msg => consoleMessages.push(msg.text())); 10 - 11 - const errors = []; 12 - page.on('pageerror', err => errors.push(err.message)); 13 - 14 - console.log('Navigating to demo5_multiverse.html...'); 15 - await page.goto('http://localhost:8080/odoc-interactive-extension/demo5_multiverse.html'); 16 - 17 - // Wait for cells to render and execute (worker init + universe linking + eval) 18 - console.log('Waiting for cells to execute...'); 19 - await page.waitForTimeout(15000); 20 - 21 - // Check multiverse-specific behavior in console 22 - const hasWorkerFrom9090 = consoleMessages.some(m => 23 - m.includes('9090')); 24 - const hasStdlibFallback = consoleMessages.some(m => 25 - m.includes('stdlib_dcs not found') && m.includes('findlib discovery')); 26 - const hasStdlibDiscovered = consoleMessages.some(m => 27 - m.includes('Found stdlib dcs via findlib')); 28 - const hasDcsRewrite = consoleMessages.some(m => 29 - m.includes('Rewriting dcs_url')); 30 - const hasUniverseLinking = consoleMessages.some(m => 31 - m.includes('universes')); 32 - const hasInitFinished = consoleMessages.some(m => 33 - m.includes('init() finished')); 34 - const hasSetupFinished = consoleMessages.some(m => 35 - m.includes('setup() finished')); 36 - const hasInitError = consoleMessages.some(m => 37 - m.includes('init_error')); 38 - 39 - // Check for correct outputs 40 - const hasInt7 = consoleMessages.some(m => 41 - m.includes('int = 7')); 42 - const hasGreeting = consoleMessages.some(m => 43 - m.includes('Hello, Multiverse World')); 44 - const hasYojsonLoaded = consoleMessages.some(m => 45 - m.includes('Custom #require: yojson loaded')); 46 - const hasJsonOutput = consoleMessages.some(m => 47 - m.includes('"source": "multiverse"') || m.includes('multiverse')); 48 - 49 - console.log(''); 50 - console.log('=== Results ==='); 51 - console.log(` Worker loaded from port 9090: ${hasWorkerFrom9090 ? 'YES' : 'NO'}`); 52 - console.log(` stdlib_dcs fallback triggered: ${hasStdlibFallback ? 'YES' : 'NO'}`); 53 - console.log(` Stdlib discovered via findlib: ${hasStdlibDiscovered ? 'YES' : 'NO'}`); 54 - console.log(` dcs_url rewritten to absolute: ${hasDcsRewrite ? 'YES' : 'NO'}`); 55 - console.log(` Universe linking used: ${hasUniverseLinking ? 'YES' : 'NO'}`); 56 - console.log(` Init completed: ${hasInitFinished ? 'YES' : 'NO'}`); 57 - console.log(` Setup completed: ${hasSetupFinished ? 'YES' : 'NO'}`); 58 - console.log(` Init error: ${hasInitError ? 'YES (BAD)' : 'NO (good)'}`); 59 - console.log(` Output "int = 7": ${hasInt7 ? 'YES' : 'NO'}`); 60 - console.log(` Output "Hello, Multiverse": ${hasGreeting ? 'YES' : 'NO'}`); 61 - console.log(` Yojson loaded via #require: ${hasYojsonLoaded ? 'YES' : 'NO'}`); 62 - console.log(` JSON output with "multiverse": ${hasJsonOutput ? 'YES' : 'NO'}`); 63 - 64 - let passed = true; 65 - 66 - if (!hasStdlibFallback) { 67 - console.log('\nFAIL: stdlib_dcs fallback was not triggered (multiverse needs it)'); 68 - passed = false; 69 - } 70 - if (!hasStdlibDiscovered) { 71 - console.log('\nFAIL: Stdlib was not discovered via findlib universe linking'); 72 - passed = false; 73 - } 74 - if (!hasDcsRewrite) { 75 - console.log('\nFAIL: dcs_url was not rewritten to absolute URL'); 76 - passed = false; 77 - } 78 - if (!hasInitFinished) { 79 - console.log('\nFAIL: Worker init did not complete'); 80 - passed = false; 81 - } 82 - if (!hasSetupFinished) { 83 - console.log('\nFAIL: Worker setup did not complete'); 84 - passed = false; 85 - } 86 - if (hasInitError) { 87 - console.log('\nFAIL: Worker reported init_error'); 88 - const errMsg = consoleMessages.find(m => m.includes('init_error')); 89 - console.log(` ${errMsg}`); 90 - passed = false; 91 - } 92 - if (!hasInt7) { 93 - console.log('\nFAIL: Expected output "int = 7" not found'); 94 - passed = false; 95 - } 96 - if (!hasGreeting) { 97 - console.log('\nFAIL: Expected output "Hello, Multiverse World" not found'); 98 - passed = false; 99 - } 100 - if (!hasYojsonLoaded) { 101 - console.log('\nFAIL: Yojson was not loaded via #require'); 102 - passed = false; 103 - } 104 - 105 - if (passed) { 106 - console.log('\nPASS: Multiverse demo working correctly'); 107 - } else { 108 - console.log('\n--- Console messages (last 50) ---'); 109 - consoleMessages.slice(-50).forEach(m => console.log(` ${m.substring(0, 200)}`)); 110 - } 111 - 112 - await browser.close(); 113 - process.exit(passed ? 0 : 1); 114 - })();