···11+Embed OCaml notebooks in any web page thanks to WebComponents! Just copy and paste the following script in your html page source to load the integration:
22+33+```html
44+<script async
55+ src="https://cdn.jsdelivr.net/gh/art-w/x-ocaml.js@1/x-ocaml.js"
66+ src-worker="https://cdn.jsdelivr.net/gh/art-w/x-ocaml.js@1/x-ocaml.worker+effects.js"
77+></script>
88+```
99+1010+This will introduce a new html tag `<x-ocaml>` to present OCaml code, for example:
1111+1212+```html
1313+<x-ocaml>let x = 42</x-ocaml>
1414+```
1515+1616+The script will initialize a CodeMirror editor integrated with the OCaml interpreter, Merlin and OCamlformat (all running in a web worker). [**Check out the online demo**](https://art-w.github.io/x-ocaml/) for more details, including how to load additional OCaml libraries and ppx in your page.
1717+1818+## Acknowledgments
1919+2020+This project was heavily inspired by the amazing [`sketch.sh`](https://sketch.sh), [@jonludlam's notebooks in Odoc](https://jon.recoil.org/notebooks/foundations/foundations1.html#a-first-session-with-ocaml), [`blogaml` by @panglesd](https://github.com/panglesd/blogaml), and all the wonderful people who made [Try OCaml](https://try.ocamlpro.com/) and other online playgrounds! It was made possible thanks to the invaluable [`js_of_ocaml-toplevel`](https://github.com/ocsigen/js_of_ocaml) library, the magical [`merlin-js` by @voodoos](https://github.com/voodoos/merlin-js), the excellent [CodeMirror bindings by @patricoferris](https://github.com/patricoferris/jsoo-code-mirror/), the guidance of @Julow on `ocamlformat` and the javascript expertise of @xvw.
···11+let mapper _argv =
22+ let module Current_ast = Ppxlib_ast.Selected_ast in
33+ let structure s =
44+ match s with [] -> [] | _ -> Ppxlib.Driver.map_structure s
55+ in
66+ let structure _ st =
77+ Current_ast.of_ocaml Structure st
88+ |> structure
99+ |> Current_ast.to_ocaml Structure
1010+ in
1111+ let signature _ si =
1212+ Current_ast.of_ocaml Signature si
1313+ |> Ppxlib.Driver.map_signature
1414+ |> Current_ast.to_ocaml Signature
1515+ in
1616+ { Ast_mapper.default_mapper with structure; signature }
1717+1818+let () = Ast_mapper.register "ppxlib" mapper
···11+module Merlin_protocol = Protocol
22+33+type id = int
44+55+type request =
66+ | Merlin of id * Merlin_protocol.action
77+ | Eval of id * string
88+ | Format of id * string
99+ | Setup
1010+1111+type output = Stdout of string | Stderr of string | Meta of string
1212+1313+type response =
1414+ | Merlin_response of id * Merlin_protocol.answer
1515+ | Top_response of id * output list
1616+ | Top_response_at of id * int * output list
1717+ | Formatted_source of id * string
1818+1919+let req_to_bytes (req : request) = Marshal.to_bytes req []
2020+let resp_to_bytes (req : response) = Marshal.to_bytes req []
2121+let req_of_bytes req : request = Marshal.from_bytes req 0
2222+let resp_of_string resp : response = Marshal.from_string resp 0
+154
src/cell.ml
···11+open Brr
22+33+type status = Not_run | Running | Run_ok | Request_run
44+55+type t = {
66+ id : int;
77+ mutable prev : t option;
88+ mutable next : t option;
99+ mutable status : status;
1010+ cm : Editor.t;
1111+ worker : Client.t;
1212+ merlin_worker : Merlin_ext.Client.worker;
1313+}
1414+1515+let id t = t.id
1616+1717+let pre_source t =
1818+ let rec go acc t =
1919+ match t.prev with
2020+ | None -> String.concat "\n" (List.rev acc)
2121+ | Some e -> go (Editor.source e.cm :: acc) e
2222+ in
2323+ let s = go [] t in
2424+ if s = "" then s else s ^ " ;;\n"
2525+2626+let rec invalidate_from ~editor =
2727+ editor.status <- Not_run;
2828+ Editor.clear editor.cm;
2929+ let count = Editor.nb_lines editor.cm in
3030+ match editor.next with
3131+ | None -> ()
3232+ | Some editor ->
3333+ Editor.set_previous_lines editor.cm count;
3434+ invalidate_from ~editor
3535+3636+let invalidate_after ~editor =
3737+ editor.status <- Not_run;
3838+ let count = Editor.nb_lines editor.cm in
3939+ match editor.next with
4040+ | None -> ()
4141+ | Some editor ->
4242+ Editor.set_previous_lines editor.cm count;
4343+ invalidate_from ~editor
4444+4545+let rec refresh_lines_from ~editor =
4646+ let count = Editor.nb_lines editor.cm in
4747+ match editor.next with
4848+ | None -> ()
4949+ | Some editor ->
5050+ Editor.set_previous_lines editor.cm count;
5151+ refresh_lines_from ~editor
5252+5353+let rec run editor =
5454+ if editor.status = Running then ()
5555+ else (
5656+ editor.status <- Request_run;
5757+ Editor.clear_messages editor.cm;
5858+ match editor.prev with
5959+ | Some e when e.status <> Run_ok -> run e
6060+ | _ ->
6161+ editor.status <- Running;
6262+ let code_txt = Editor.source editor.cm in
6363+ Client.eval ~id:editor.id editor.worker code_txt)
6464+6565+let set_prev ~prev t =
6666+ let () = match t.prev with None -> () | Some prev -> prev.next <- None in
6767+ t.prev <- prev;
6868+ match prev with
6969+ | None ->
7070+ Editor.set_previous_lines t.cm 0;
7171+ refresh_lines_from ~editor:t;
7272+ run t
7373+ | Some p ->
7474+ assert (p.next = None);
7575+ p.next <- Some t;
7676+ refresh_lines_from ~editor:p;
7777+ run t
7878+7979+let set_source_from_html editor this =
8080+ let doc = Webcomponent.text_content this in
8181+ let doc = String.trim doc in
8282+ Editor.set_source editor.cm doc;
8383+ invalidate_from ~editor;
8484+ Client.fmt ~id:editor.id editor.worker doc
8585+8686+let init ~id worker this =
8787+ let shadow = Webcomponent.attach_shadow this in
8888+8989+ El.append_children shadow
9090+ [ El.style [ El.txt @@ Jstr.of_string [%blob "style.css"] ] ];
9191+ let run_btn = El.button [ El.txt (Jstr.of_string "Run") ] in
9292+ El.append_children shadow
9393+ [ El.div ~at:[ At.class' (Jstr.of_string "run_btn") ] [ run_btn ] ];
9494+9595+ let cm = Editor.make shadow in
9696+9797+ let merlin = Merlin_ext.make ~id worker in
9898+ let merlin_worker = Merlin_ext.Client.make_worker merlin in
9999+ let editor =
100100+ {
101101+ id;
102102+ status = Not_run;
103103+ cm;
104104+ prev = None;
105105+ next = None;
106106+ worker;
107107+ merlin_worker;
108108+ }
109109+ in
110110+ Editor.on_change cm (fun () -> invalidate_after ~editor);
111111+ set_source_from_html editor this;
112112+113113+ Merlin_ext.set_context merlin (fun () -> pre_source editor);
114114+ Editor.configure_merlin cm (Merlin_ext.extensions merlin_worker);
115115+116116+ let () =
117117+ Mutation_observer.observe ~target:(Webcomponent.as_target this)
118118+ @@ Mutation_observer.create (fun _ _ -> set_source_from_html editor this)
119119+ in
120120+121121+ let _ : Ev.listener =
122122+ Ev.listen Ev.click (fun _ev -> run editor) (El.as_target run_btn)
123123+ in
124124+125125+ editor
126126+127127+let set_source editor doc =
128128+ Editor.set_source editor.cm doc;
129129+ refresh_lines_from ~editor
130130+131131+let render_message msg =
132132+ let kind, text =
133133+ match msg with
134134+ | X_protocol.Stdout str -> ("stdout", str)
135135+ | Stderr str -> ("stderr", str)
136136+ | Meta str -> ("meta", str)
137137+ in
138138+ El.pre
139139+ ~at:[ At.class' (Jstr.of_string ("caml_" ^ kind)) ]
140140+ [ El.txt (Jstr.of_string text) ]
141141+142142+let add_message t loc msg =
143143+ Editor.add_message t.cm loc (List.map render_message msg)
144144+145145+let completed_run ed msg =
146146+ (if msg <> [] then
147147+ let loc = String.length (Editor.source ed.cm) in
148148+ add_message ed loc msg);
149149+ ed.status <- Run_ok;
150150+ match ed.next with Some e when e.status = Request_run -> run e | _ -> ()
151151+152152+let receive_merlin t msg =
153153+ Merlin_ext.Client.on_message t.merlin_worker
154154+ (Merlin_ext.fix_answer (pre_source t) msg)
+9
src/cell.mli
···11+type t
22+33+val init : id:int -> Client.t -> Webcomponent.t -> t
44+val id : t -> int
55+val set_source : t -> string -> unit
66+val add_message : t -> int -> X_protocol.output list -> unit
77+val completed_run : t -> X_protocol.output list -> unit
88+val set_prev : prev:t option -> t -> unit
99+val receive_merlin : t -> Protocol.answer -> unit
+60
src/client.ml
···11+module Worker = Brr_webworkers.Worker
22+open Brr
33+44+type t = Worker.t
55+66+let current_url =
77+ let url = Window.location G.window in
88+ let path = Jstr.to_string (Uri.path url) in
99+ let url =
1010+ match List.rev (String.split_on_char '/' path) with
1111+ | [] | "" :: _ -> url
1212+ | _ :: rev_path -> (
1313+ let path = Jstr.of_string @@ String.concat "/" @@ List.rev rev_path in
1414+ match Uri.with_uri ~path ~query:Jstr.empty ~fragment:Jstr.empty url with
1515+ | Ok url -> url
1616+ | Error _ -> url)
1717+ in
1818+ Jstr.to_string (Uri.to_jstr url)
1919+2020+let absolute_url url =
2121+ if
2222+ not
2323+ (String.starts_with ~prefix:"http:" url
2424+ || String.starts_with ~prefix:"https:" url)
2525+ then current_url ^ url
2626+ else url
2727+2828+let wrap_url ?extra_load url =
2929+ let url = absolute_url url in
3030+ let extra =
3131+ match extra_load with
3232+ | None -> ""
3333+ | Some extra -> "','" ^ absolute_url extra
3434+ in
3535+ let script = "importScripts('" ^ url ^ extra ^ "');" in
3636+ let script = Jstr.of_string script in
3737+ let url =
3838+ match Base64.(encode (data_of_binary_jstr script)) with
3939+ | Ok data -> Jstr.to_string data
4040+ | Error _ -> assert false
4141+ in
4242+ "data:text/javascript;base64," ^ url
4343+4444+let make ?extra_load url =
4545+ Worker.create @@ Jstr.of_string @@ wrap_url ?extra_load url
4646+4747+let on_message t fn =
4848+ let fn m =
4949+ let m = Ev.as_type m in
5050+ let msg = Bytes.to_string @@ Brr_io.Message.Ev.data m in
5151+ fn (X_protocol.resp_of_string msg)
5252+ in
5353+ let _listener =
5454+ Ev.listen Brr_io.Message.Ev.message fn @@ Worker.as_target t
5555+ in
5656+ ()
5757+5858+let post worker msg = Worker.post worker (X_protocol.req_to_bytes msg)
5959+let eval ~id worker code = post worker (Eval (id, code))
6060+let fmt ~id worker code = post worker (Format (id, code))
+7
src/client.mli
···11+type t
22+33+val make : ?extra_load:string -> string -> t
44+val on_message : t -> (X_protocol.response -> unit) -> unit
55+val post : t -> X_protocol.request -> unit
66+val eval : id:int -> t -> string -> unit
77+val fmt : id:int -> t -> string -> unit
···11+type t = {
22+ view : Code_mirror.Editor.View.t;
33+ messages_comp : Code_mirror.Compartment.t;
44+ lines_comp : Code_mirror.Compartment.t;
55+ merlin_comp : Code_mirror.Compartment.t;
66+ changes : Code_mirror.Compartment.t;
77+ mutable previous_lines : int;
88+ mutable current_doc : string;
99+ mutable messages : (int * Brr.El.t list) list;
1010+}
1111+1212+let find_line_ends at doc =
1313+ let rec go i =
1414+ if i >= String.length doc || doc.[i] = '\n' then i else go (i + 1)
1515+ in
1616+ go at
1717+1818+let render_messages cm =
1919+ let open Code_mirror.Editor in
2020+ let open Code_mirror.Decoration in
2121+ let (State.Facet ((module F), it)) = View.decorations () in
2222+ let doc = cm.current_doc in
2323+ let ranges =
2424+ Array.of_list
2525+ @@ List.map (fun (at, msg) ->
2626+ let at = find_line_ends at doc in
2727+ range ~from:at ~to_:at
2828+ @@ widget ~block:true ~side:99
2929+ @@ Widget.make (fun () -> msg))
3030+ @@ List.concat
3131+ @@ List.map (fun (loc, lst) -> List.map (fun m -> (loc, m)) lst)
3232+ @@ List.sort (fun (a, _) (b, _) -> Int.compare a b) cm.messages
3333+ in
3434+ F.of_ it (Range_set.of' ranges)
3535+3636+let refresh_messages ed =
3737+ Code_mirror.Editor.View.dispatch ed.view
3838+ (Code_mirror.Compartment.reconfigure ed.messages_comp
3939+ [ render_messages ed ]);
4040+ Code_mirror.Editor.View.request_measure ed.view
4141+4242+let custom_ln editor =
4343+ Code_mirror.Editor.View.line_numbers (fun x ->
4444+ string_of_int (editor.previous_lines + x))
4545+4646+let refresh_lines ed =
4747+ Code_mirror.Editor.View.dispatch ed.view
4848+ @@ Code_mirror.Compartment.reconfigure ed.lines_comp [ custom_ln ed ]
4949+5050+let configure_merlin ed extensions =
5151+ Code_mirror.Editor.View.dispatch ed.view
5252+ @@ Code_mirror.Compartment.reconfigure ed.merlin_comp extensions
5353+5454+let clear x =
5555+ x.messages <- [];
5656+ refresh_lines x;
5757+ refresh_messages x
5858+5959+let source_of_state s =
6060+ String.concat "\n" @@ Array.to_list @@ Array.map Jstr.to_string
6161+ @@ Code_mirror.Text.to_jstr_array
6262+ @@ Code_mirror.Editor.State.doc s
6363+6464+let source t = source_of_state @@ Code_mirror.Editor.View.state t.view
6565+6666+let prefix_length a b =
6767+ let rec go i =
6868+ if i >= String.length a || i >= String.length b || a.[i] <> b.[i] then i
6969+ else go (i + 1)
7070+ in
7171+ go 0
7272+7373+let basic_setup =
7474+ Jv.get Jv.global "__CM__basic_setup" |> Code_mirror.Extension.of_jv
7575+7676+let make parent =
7777+ let open Code_mirror.Editor in
7878+ let changes = Code_mirror.Compartment.make () in
7979+ let messages = Code_mirror.Compartment.make () in
8080+ let lines = Code_mirror.Compartment.make () in
8181+ let merlin = Code_mirror.Compartment.make () in
8282+ let extensions =
8383+ [|
8484+ basic_setup;
8585+ Code_mirror.Editor.View.line_wrapping ();
8686+ Code_mirror.Compartment.of' lines [];
8787+ Code_mirror.Compartment.of' messages [];
8888+ Code_mirror.Compartment.of' changes [];
8989+ Code_mirror.Compartment.of' merlin [];
9090+ |]
9191+ in
9292+ let config = State.Config.create ~doc:Jstr.empty ~extensions () in
9393+ let state = State.create ~config () in
9494+ let opts = View.opts ~state ~parent () in
9595+ let view = View.create ~opts () in
9696+ {
9797+ previous_lines = 0;
9898+ current_doc = "";
9999+ messages = [];
100100+ view;
101101+ messages_comp = messages;
102102+ lines_comp = lines;
103103+ merlin_comp = merlin;
104104+ changes;
105105+ }
106106+107107+let set_current_doc t new_doc =
108108+ let at = prefix_length t.current_doc new_doc in
109109+ t.current_doc <- new_doc;
110110+ t.messages <- List.filter (fun (loc, _) -> loc < at) t.messages;
111111+ refresh_messages t
112112+113113+let on_change cm fn =
114114+ let has_changed =
115115+ let open Code_mirror.Editor in
116116+ let (State.Facet ((module F), it)) = View.update_listener () in
117117+ F.of_ it (fun ev ->
118118+ if View.Update.doc_changed ev then
119119+ let new_doc = source_of_state (View.Update.state ev) in
120120+ if not (String.equal cm.current_doc new_doc) then (
121121+ set_current_doc cm new_doc;
122122+ fn ()))
123123+ in
124124+ Code_mirror.Editor.View.dispatch cm.view
125125+ @@ Code_mirror.Compartment.reconfigure cm.changes [ has_changed ]
126126+127127+let count_lines str =
128128+ if str = "" then 0
129129+ else
130130+ let nb = ref 1 in
131131+ for i = 0 to String.length str - 1 do
132132+ if str.[i] = '\n' then incr nb
133133+ done;
134134+ !nb
135135+136136+let nb_lines t = t.previous_lines + count_lines t.current_doc
137137+138138+let set_previous_lines t nb =
139139+ t.previous_lines <- nb;
140140+ refresh_lines t
141141+142142+let set_messages t msg =
143143+ t.messages <- msg;
144144+ refresh_messages t
145145+146146+let clear_messages t = set_messages t []
147147+let add_message t loc msg = set_messages t ((loc, msg) :: t.messages)
148148+149149+let set_source t doc =
150150+ set_current_doc t doc;
151151+ Code_mirror.Editor.View.set_doc t.view (Jstr.of_string doc)
+12
src/editor.mli
···11+type t
22+33+val make : Brr.El.t -> t
44+val source : t -> string
55+val set_source : t -> string -> unit
66+val clear : t -> unit
77+val nb_lines : t -> int
88+val set_previous_lines : t -> int -> unit
99+val clear_messages : t -> unit
1010+val add_message : t -> int -> Brr.El.t list -> unit
1111+val on_change : t -> (unit -> unit) -> unit
1212+val configure_merlin : t -> Code_mirror.Extension.t list -> unit
+72
src/merlin_ext.ml
···11+module Worker = Brr_webworkers.Worker
22+33+type t = { id : int; mutable context : unit -> string; client : Client.t }
44+55+let set_context t fn = t.context <- fn
66+77+let make ~id client =
88+ { id; context = (fun () -> failwith "Merlin_ext.context"); client }
99+1010+let fix_position pre_len = function
1111+ | `Offset at -> `Offset (at + pre_len)
1212+ | other -> other
1313+1414+let fix_loc pre_len ({ loc_start; loc_end; _ } as loc : Protocol.Location.t) =
1515+ {
1616+ loc with
1717+ loc_start = { loc_start with pos_cnum = loc_start.pos_cnum - pre_len };
1818+ loc_end = { loc_end with pos_cnum = loc_end.pos_cnum - pre_len };
1919+ }
2020+2121+let fix_request t msg =
2222+ let pre = t.context () in
2323+ let pre_len = String.length pre in
2424+ match msg with
2525+ | Protocol.Complete_prefix (src, position) ->
2626+ let position = fix_position pre_len position in
2727+ Protocol.Complete_prefix (pre ^ src, position)
2828+ | Protocol.Type_enclosing (src, position) ->
2929+ let position = fix_position pre_len position in
3030+ Protocol.Type_enclosing (pre ^ src, position)
3131+ | Protocol.All_errors src -> Protocol.All_errors (pre ^ src)
3232+ | Protocol.Add_cmis _ as other -> other
3333+3434+let fix_answer pre msg =
3535+ let pre_len = String.length pre in
3636+ match (msg : Protocol.answer) with
3737+ | Protocol.Errors errors ->
3838+ Protocol.Errors
3939+ (List.filter_map
4040+ (fun (e : Protocol.error) ->
4141+ let loc = fix_loc pre_len e.loc in
4242+ let from = loc.loc_start.pos_cnum in
4343+ let to_ = loc.loc_end.pos_cnum in
4444+ if from < 0 || to_ < 0 then None else Some { e with loc })
4545+ errors)
4646+ | Protocol.Completions completions ->
4747+ Completions
4848+ {
4949+ completions with
5050+ from = completions.from - pre_len;
5151+ to_ = completions.to_ - pre_len;
5252+ }
5353+ | Protocol.Typed_enclosings typed_enclosings ->
5454+ Typed_enclosings
5555+ (List.map
5656+ (fun (loc, a, b) -> (fix_loc pre_len loc, a, b))
5757+ typed_enclosings)
5858+ | Protocol.Added_cmis -> msg
5959+6060+module Merlin_send = struct
6161+ type nonrec t = t
6262+6363+ let post t msg =
6464+ let msg = fix_request t msg in
6565+ Client.post t.client (Merlin (t.id, msg))
6666+end
6767+6868+module Client = Merlin_client.Make (Merlin_send)
6969+module Ed = Merlin_codemirror.Extensions (Merlin_send)
7070+7171+let extensions t =
7272+ Merlin_codemirror.ocaml :: Array.to_list (Ed.all_extensions t)
+21
src/mutation_observer.ml
···11+open Brr
22+33+type t = Jv.t
44+55+let mutation_observer = Jv.get Jv.global "MutationObserver"
66+77+let create callback =
88+ let callback = Jv.callback ~arity:2 callback in
99+ Jv.new' mutation_observer [| callback |]
1010+1111+let disconnect t =
1212+ let _ : Jv.t = Jv.call t "disconnect" [||] in
1313+ ()
1414+1515+let observe t ~target =
1616+ let config =
1717+ Jv.obj
1818+ Jv.[| ("attributes", true'); ("childList", true'); ("subtree", true') |]
1919+ in
2020+ let _ : Jv.t = Jv.call t "observe" [| El.to_jv target; config |] in
2121+ ()
+7
src/mutation_observer.mli
···11+open Brr
22+33+type t
44+55+val create : (Jv.t -> Jv.t -> unit) -> t
66+val observe : t -> target:El.t -> unit
77+val disconnect : t -> unit