diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..8ca91cd6b --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Automatizes the source code formatting with ocamlformat. +af1c1d546b59029c4790238da3d4a7f7104040ed diff --git a/.ocamlformat b/.ocamlformat new file mode 100644 index 000000000..d10381227 --- /dev/null +++ b/.ocamlformat @@ -0,0 +1,3 @@ +profile = default +sequence-style = terminator +field-space = loose diff --git a/Makefile b/Makefile index 09e30c4df..7c94dc287 100644 --- a/Makefile +++ b/Makefile @@ -3,13 +3,19 @@ all: static build DUNE = dune DUNE_ARGS = --profile=release +FORMATER = ocamlformat +SRC = src/**/*.ml + build-deps: opam install . --deps-only --locked -.PHONY: build -build: +.PHONY: build format +build: format @${DUNE} build ${DUNE_ARGS} +format: + @${FORMATER} -i $(SRC) $(SRC:.ml=.mli) + .PHONY: static static: @${MAKE} -C static diff --git a/learn-ocaml-client.opam b/learn-ocaml-client.opam index e824aa73e..4bfdeb6d6 100644 --- a/learn-ocaml-client.opam +++ b/learn-ocaml-client.opam @@ -30,15 +30,17 @@ depends: [ "lwt" {>= "4.0.0"} "lwt_ssl" "ocaml" {= "4.05.0"} + "ocamlformat" {= "0.8"} "ocamlfind" {build} + "ocamlformat" {= "0.8"} "ocp-indent-nlfork" "ocp-ocamlres" {>= "0.4"} "ocplib-json-typed" {= "0.6"} "ipaddr" {= "2.8.0" } "cstruct" {>= "3.3.0"} "ppx_tools" - "ppx_sexp_conv" {= "v0.9.0"} - "ppx_fields_conv" {= "v0.9.0"} + "ppx_sexp_conv" {= "v0.11.1"} + "ppx_fields_conv" {= "v0.11.0"} ] build: [ ["dune" "build" "@install" "-p" name "-j" jobs] diff --git a/learn-ocaml.opam b/learn-ocaml.opam index a89a43dea..50422d4be 100644 --- a/learn-ocaml.opam +++ b/learn-ocaml.opam @@ -42,6 +42,7 @@ depends: [ "markup-lwt" "ocaml" {= "4.05.0"} "ocamlfind" {build} + "ocamlformat" {= "0.8"} "ocp-indent-nlfork" "ocp-ocamlres" {>= "0.4"} "ocplib-json-typed" {= "0.6"} diff --git a/learn-ocaml.opam.locked b/learn-ocaml.opam.locked index ad7b2abf8..c728b0c1f 100644 --- a/learn-ocaml.opam.locked +++ b/learn-ocaml.opam.locked @@ -14,9 +14,9 @@ homepage: "https://github.com/ocaml-sf/learn-ocaml" bug-reports: "https://github.com/ocaml-sf/learn-ocaml/issues" dev-repo: "git+https://github.com/ocaml-sf/learn-ocaml" depends: [ - "asak" {= "0.2"} - "astring" {= "0.8.4"} - "base" {= "v0.9.4"} + "asak" {= "0.3"} + "astring" {= "0.8.5"} + "base" {= "v0.11.1"} "base-bigarray" {= "base"} "base-bytes" {= "base"} "base-num" {= "base"} @@ -27,92 +27,91 @@ depends: [ "biniou" {= "1.2.1"} "checkseum" {= "0.1.0"} "cmdliner" {= "1.0.4"} - "cohttp" {= "1.1.1"} - "cohttp-lwt" {= "1.1.1"} - "cohttp-lwt-unix" {= "1.1.1"} + "cohttp" {= "1.2.0"} + "cohttp-lwt" {= "1.2.0"} + "cohttp-lwt-unix" {= "1.2.0"} "conduit" {= "1.3.0"} "conduit-lwt" {= "1.3.0"} "conduit-lwt-unix" {= "1.3.0"} - "conf-git" {= "1.0"} - "conf-libssl" {= "1"} + "conf-git" {= "1.1"} + "conf-libssl" {= "3"} "conf-m4" {= "1"} - "conf-pkg-config" {= "1.2"} + "conf-pkg-config" {= "2"} "conf-which" {= "1"} - "cppo" {= "1.6.6"} + "cppo" {= "1.6.7"} "cstruct" {= "5.0.0"} "decompress" {= "0.8.1"} "digestif" {= "0.8.0-1"} "dune" {= "2.0.1"} "easy-format" {= "1.3.2"} "eqaf" {= "0.7"} - "ezjsonm" {= "1.1.0"} - "fieldslib" {= "v0.9.0"} - "fmt" {= "0.8.8"} - "fpath" {= "0.7.2"} + "ezjsonm" {= "1.2.0"} + "fieldslib" {= "v0.11.0"} + "fmt" {= "0.8.9"} + "fpath" {= "0.7.3"} + "gg" {= "0.9.3"} "hex" {= "1.4.0"} "ipaddr" {= "2.8.0"} "jbuilder" {= "1.0+beta20.2"} "js_of_ocaml" {= "3.3.0"} "js_of_ocaml-compiler" {= "3.3.0"} "js_of_ocaml-lwt" {= "3.3.0"} + "js_of_ocaml-ocamlbuild" {= "3.5.2"} "js_of_ocaml-ppx" {= "3.3.0"} "js_of_ocaml-toplevel" {= "3.3.0"} "js_of_ocaml-tyxml" {= "3.3.0"} "jsonm" {= "1.0.1"} - "markup-lwt" {= "0.5.0"} "logs" {= "0.7.0"} - "lwt" {= "4.2.1"} - "lwt_react" {= "1.1.3"} + "lwt" {= "4.2.1-1"} + "lwt_react" {= "1.1.4"} "lwt_ssl" {= "1.1.3"} - "magic-mime" {= "1.1.2"} + "magic-mime" {= "1.2.0"} "markup" {= "0.8.2"} + "markup-lwt" {= "0.5.0"} "mmap" {= "1.1.0"} "num" {= "0"} "ocaml" {= "4.05.0"} - "ocaml-compiler-libs" {= "v0.9.0"} - "ocaml-migrate-parsetree" {= "1.7.3"} + "ocaml-compiler-libs" {= "v0.12.3"} + "ocaml-migrate-parsetree" {= "1.8.0"} "ocaml-secondary-compiler" {= "4.08.1-1"} "ocamlbuild" {= "0.14.0"} "ocamlfind" {= "1.8.1"} "ocamlfind-secondary" {= "1.8.1"} + "ocamlformat" {= "0.8"} "ocp-indent-nlfork" {= "1.5.4"} "ocp-ocamlres" {= "0.4"} "ocplib-json-typed" {= "0.6"} - "odoc" {= "1.5.1"} + "odoc" {= "1.5.3"} "omd" {= "1.3.1"} "optint" {= "0.0.2"} + "parsexp" {= "v0.11.0"} "pprint" {= "20200410"} - "ppx_ast" {= "v0.9.1"} - "ppx_core" {= "v0.9.3"} "ppx_cstruct" {= "5.0.0"} "ppx_derivers" {= "1.2.1"} - "ppx_driver" {= "v0.9.2"} - "ppx_fields_conv" {= "v0.9.0"} - "ppx_metaquot" {= "v0.9.0"} - "ppx_optcomp" {= "v0.9.0"} - "ppx_sexp_conv" {= "v0.9.0"} + "ppx_fields_conv" {= "v0.11.0"} + "ppx_sexp_conv" {= "v0.11.1"} "ppx_tools" {= "5.0+4.05.0"} "ppx_tools_versioned" {= "5.4.0"} - "ppx_traverse_builtins" {= "v0.9.0"} - "ppx_type_conv" {= "v0.9.1"} + "ppxlib" {= "0.2.2"} "re" {= "1.9.0"} "react" {= "1.2.1"} "reactiveData" {= "0.2.1"} "result" {= "1.5"} "rresult" {= "0.6.0"} "seq" {= "0.2.2"} - "sexplib" {= "v0.9.3"} + "sexplib" {= "v0.11.0"} + "sexplib0" {= "v0.11.0"} "ssl" {= "0.5.5"} - "stdio" {= "v0.9.1"} - "stdlib-shims" {= "0.1.0"} + "stdio" {= "v0.11.0"} + "stdlib-shims" {= "0.3.0"} "stringext" {= "1.6.0"} - "topkg" {= "1.0.1"} - "tyxml" {= "4.4.0"} + "topkg" {= "1.0.3"} + "tyxml" {= "4.5.0"} "uchar" {= "0.0.2"} - "uri" {= "1.9.7"} + "uri" {= "2.2.1"} "uutf" {= "1.0.2"} - "yojson" {= "1.7.0"} "vg" {= "0.9.3"} + "yojson" {= "1.7.0"} ] build: [ [make "static"] diff --git a/src/ace-lib/ace.ml b/src/ace-lib/ace.ml index 58f55feca..1a2cbe9f7 100644 --- a/src/ace-lib/ace.ml +++ b/src/ace-lib/ace.ml @@ -9,31 +9,28 @@ open Js_of_ocaml open Ace_types -let iter_option f = function - | None -> () - | Some x -> f x +let iter_option f = function None -> () | Some x -> f x (** Editor *) -type 'a editor = { - editor_div: Dom_html.divElement Js.t; - editor: ('a editor * 'a option) Ace_types.editor Js.t; - mutable marks: int list; - mutable keybinding_menu: bool; -} +type 'a editor = + { editor_div : Dom_html.divElement Js.t + ; editor : ('a editor * 'a option) Ace_types.editor Js.t + ; mutable marks : int list + ; mutable keybinding_menu : bool } let ace : Ace_types.ace Js.t = Js.Unsafe.variable "ace" -let edit el = ace##(edit el) + +let edit el = ace ## (edit el) let create_position r c = let pos : position Js.t = Js.Unsafe.obj [||] in pos##.row := r; pos##.column := c; pos -let greater_position p1 p2 = - p1##.row > p2##.row || - (p1##.row = p2##.row && p1##.column > p2##.column) +let greater_position p1 p2 = + p1##.row > p2##.row || (p1##.row = p2##.row && p1##.column > p2##.column) let create_range s e = let range : range Js.t = Js.Unsafe.obj [||] in @@ -44,18 +41,16 @@ let create_range s e = let read_position pos = (pos##.row, pos##.column) let read_range range = - ((range##.start##.row, range##.start##.column), - (range##.end_##.row, range##.end_##.column)) + ( (range##.start##.row, range##.start##.column) + , (range##.end_##.row, range##.end_##.column) ) let get_contents ?range {editor} = - let document = (editor##getSession)##getDocument in + let document = editor##getSession##getDocument in match range with - | None -> - Js.to_string @@ document##getValue - | Some r -> - Js.to_string @@ document##(getTextRange r) + | None -> Js.to_string @@ document##getValue + | Some r -> Js.to_string @@ (document ## (getTextRange r)) -let set_contents ?(reset_undo=false) {editor} code = +let set_contents ?(reset_undo = false) {editor} code = let session = editor##getSession in session##getDocument##setValue (Js.string code); if reset_undo then session##getUndoManager##reset @@ -63,143 +58,141 @@ let set_contents ?(reset_undo=false) {editor} code = let get_selection_range {editor} = editor##getSelectionRange let get_selection {editor} = - let document = (editor##getSession)##getDocument in + let document = editor##getSession##getDocument in let range = editor##getSelectionRange in - Js.to_string @@ document##(getTextRange range) + Js.to_string @@ (document ## (getTextRange range)) let get_line {editor} line = - let document = (editor##getSession)##getDocument in - Js.to_string @@ document##(getLine line) + let document = editor##getSession##getDocument in + Js.to_string @@ (document ## (getLine line)) let create_editor editor_div = let editor = edit editor_div in Js.Unsafe.set editor "$blockScrolling" (Js.Unsafe.variable "Infinity"); - let data = - { editor; editor_div; marks = []; keybinding_menu = false; } in + let data = {editor; editor_div; marks = []; keybinding_menu = false} in editor##.customData := (data, None); editor##setOption (Js.string "displayIndentGuides") (Js.bool false); data -let get_custom_data { editor } = - match snd editor##.customData with - | None -> raise Not_found - | Some x -> x +let get_custom_data {editor} = + match snd editor##.customData with None -> raise Not_found | Some x -> x -let set_custom_data { editor } data = +let set_custom_data {editor} data = let ed = fst editor##.customData in editor##.customData := (ed, Some data) -let set_mode {editor} name = - editor##getSession##(setMode (Js.string name)) +let set_mode {editor} name = editor ## getSession ## (setMode (Js.string name)) type mark_type = Error | Warning | Message -let string_of_make_type: mark_type -> string = function +let string_of_make_type : mark_type -> string = function | Error -> "error" | Warning -> "warning" | Message -> "message" -let require s = (Js.Unsafe.variable "ace")##(require (Js.string s)) +let require s = (Js.Unsafe.variable "ace") ## (require (Js.string s)) type range = Ace_types.range Js.t -let range_cstr = (require "ace/range")##._Range + +let range_cstr = (require "ace/range")##._Range + let range sr sc er ec : range = Js.Unsafe.new_obj range_cstr - [| Js.Unsafe.inject sr ; Js.Unsafe.inject sc ; - Js.Unsafe.inject er ; Js.Unsafe.inject ec |] + [| Js.Unsafe.inject sr + ; Js.Unsafe.inject sc + ; Js.Unsafe.inject er + ; Js.Unsafe.inject ec |] -type loc = { - loc_start: int * int; - loc_end: int * int; -} +type loc = {loc_start : int * int; loc_end : int * int} let set_mark editor ?loc ?(type_ = Message) msg = let session = (editor.editor)##getSession in let type_ = string_of_make_type type_ in let sr, sc, range = match loc with - | None -> 0, 0, None - | Some { loc_start = (sr, sc) ; loc_end = (er, ec) } -> + | None -> (0, 0, None) + | Some {loc_start = sr, sc; loc_end = er, ec} -> let sr = sr - 1 in let er = er - 1 in - sr, sc, Some (range sr sc er ec) in + (sr, sc, Some (range sr sc er ec)) + in let annot : annotation Js.t = Js.Unsafe.obj [||] in annot##.row := sr; annot##.column := sc; annot##.text := Js.string msg; annot##.type_ := Js.string type_; let annotations = - Array.concat [[| annot |]; Js.to_array (session##getAnnotations)] in - session##(setAnnotations (Js.array @@ annotations)); + Array.concat [[|annot|]; Js.to_array session##getAnnotations] + in + session ## (setAnnotations (Js.array @@ annotations)); match range with | None -> () | Some range -> - editor.marks <- - session##(addMarker range (Js.string type_) (Js.string "text") (Js._false)) :: editor.marks + <- session + ## (addMarker range (Js.string type_) (Js.string "text") Js._false) + :: editor.marks let set_background_color editor color = - editor.editor_div##.style##.backgroundColor := Js.string color + (editor.editor_div)##.style##.backgroundColor := Js.string color + +let add_class {editor_div} name = + editor_div ##. classList ## (add (Js.string name)) -let add_class { editor_div } name = - editor_div##.classList##(add (Js.string name)) -let remove_class { editor_div } name = - editor_div##.classList##(remove (Js.string name)) +let remove_class {editor_div} name = + editor_div ##. classList ## (remove (Js.string name)) let clear_marks editor = let session = (editor.editor)##getSession in - List.iter (fun i -> session##(removeMarker i)) editor.marks; + List.iter (fun i -> session ## (removeMarker i)) editor.marks; session##clearAnnotations; editor.marks <- [] let record_event_handler editor event handler = - editor.editor##(on (Js.string event) handler) + (editor.editor) ## (on (Js.string event) handler) -let focus { editor } = editor##focus -let resize { editor } force = editor##(resize (Js.bool force)) +let focus {editor} = editor##focus + +let resize {editor} force = editor ## (resize (Js.bool force)) let get_keybinding_menu e = - if e.keybinding_menu then - Some (Obj.magic e.editor : keybinding_menu Js.t) + if e.keybinding_menu then Some (Obj.magic e.editor : keybinding_menu Js.t) else let ext = require "ace/ext/keybinding_menu" in - Js.Optdef.case - ext + Js.Optdef.case ext (fun () -> None) (fun _ext -> - e.keybinding_menu <- true; - Some (Obj.magic e.editor : keybinding_menu Js.t)) + e.keybinding_menu <- true; + Some (Obj.magic e.editor : keybinding_menu Js.t) ) let show_keybindings e = match get_keybinding_menu e with | None -> - Firebug.console##(log - (Js.string "You should load: 'ext-keybinding_menu.js'")) - | Some o -> - o##showKeyboardShortcuts - -let add_keybinding { editor } - ?ro ?scrollIntoView ?multiSelectAction - name key exec = + Firebug.console + ## (log (Js.string "You should load: 'ext-keybinding_menu.js'")) + | Some o -> o##showKeyboardShortcuts + +let add_keybinding {editor} ?ro ?scrollIntoView ?multiSelectAction name key + exec = let command : _ command Js.t = Js.Unsafe.obj [||] in let binding : binding Js.t = Js.Unsafe.obj [||] in command##.name := Js.string name; - command##.exec := Js.wrap_callback (fun ed _args -> exec (fst ed##.customData)); + command##.exec := + Js.wrap_callback (fun ed _args -> exec (fst ed##.customData)); iter_option (fun ro -> command##.readOnly := Js.bool ro) ro; - iter_option - (fun s -> command##.scrollIntoView := Js.string s) - scrollIntoView; + iter_option (fun s -> command##.scrollIntoView := Js.string s) scrollIntoView; iter_option (fun s -> command##.multiSelectAction := Js.string s) multiSelectAction; binding##.win := Js.string key; binding##.mac := Js.string key; command##.bindKey := binding; - editor##.commands##(addCommand command) + editor ##. commands ## (addCommand command) (** Mode *) type token = Ace_types.token Js.t + let token ~type_ value = let obj : Ace_types.token Js.t = Js.Unsafe.obj [||] in obj##.value := Js.string value; @@ -208,13 +201,12 @@ let token ~type_ value = type doc = Ace_types.document Js.t -type 'state helpers = { - initial_state: unit -> 'state; - get_next_line_indent: 'state -> line:string -> tab:string -> string; - get_line_tokens: string -> 'state -> int -> doc -> ('state * token list); - check_outdent: ('state -> string -> string -> bool) option; - auto_outdent: ('state -> document Js.t -> int -> unit) option; -} +type 'state helpers = + { initial_state : unit -> 'state + ; get_next_line_indent : 'state -> line:string -> tab:string -> string + ; get_line_tokens : string -> 'state -> int -> doc -> 'state * token list + ; check_outdent : ('state -> string -> string -> bool) option + ; auto_outdent : ('state -> document Js.t -> int -> unit) option } let create_js_line_tokens (st, tokens) = let obj : _ Ace_types.line_tokens Js.t = Js.Unsafe.obj [||] in @@ -225,58 +217,51 @@ let create_js_line_tokens (st, tokens) = let define_mode name helpers = let js_helpers : _ ace_mode_helpers Js.t = Js.Unsafe.obj [||] in js_helpers##.initialState := Js.wrap_callback helpers.initial_state; - js_helpers##.getNextLineIndent := - (Js.wrap_callback @@ fun st line tab -> - Js.string @@ - helpers.get_next_line_indent - st ~line:(Js.to_string line) ~tab:(Js.to_string tab)); - js_helpers##.getLineTokens := - (Js.wrap_callback @@ fun line st row doc -> - create_js_line_tokens @@ - helpers.get_line_tokens (Js.to_string line) st row doc); - begin match helpers.check_outdent with - | None -> () - | Some check_outdent -> - js_helpers##.checkOutdent := - (Js.wrap_callback @@ fun st line input -> - Js.bool @@ - check_outdent st (Js.to_string line) (Js.to_string input)) - end; - begin match helpers.auto_outdent with - | None -> () - | Some auto_outdent -> - js_helpers##.autoOutdent := Js.wrap_callback auto_outdent - end; + ( js_helpers##.getNextLineIndent + := Js.wrap_callback + @@ fun st line tab -> + Js.string + @@ helpers.get_next_line_indent st ~line:(Js.to_string line) + ~tab:(Js.to_string tab) ); + (js_helpers##.getLineTokens := + Js.wrap_callback + @@ fun line st row doc -> + create_js_line_tokens + @@ helpers.get_line_tokens (Js.to_string line) st row doc); + ( match helpers.check_outdent with + | None -> () + | Some check_outdent -> + js_helpers##.checkOutdent := + Js.wrap_callback + @@ fun st line input -> + Js.bool @@ check_outdent st (Js.to_string line) (Js.to_string input) ); + ( match helpers.auto_outdent with + | None -> () + | Some auto_outdent -> + js_helpers##.autoOutdent := Js.wrap_callback auto_outdent ); Js.Unsafe.fun_call (Js.Unsafe.variable "define_ocaml_mode") - [| Js.Unsafe.inject (Js.string ("ace/mode/" ^ name)) ; - Js.Unsafe.inject js_helpers |] + [| Js.Unsafe.inject (Js.string ("ace/mode/" ^ name)) + ; Js.Unsafe.inject js_helpers |] + +let set_font_size {editor} sz = editor ## (setFontSize sz) -let set_font_size {editor} sz = - editor##(setFontSize sz) -let set_tab_size {editor} sz = - editor##getSession##(setTabSize sz) +let set_tab_size {editor} sz = editor ## getSession ## (setTabSize sz) -let set_readonly {editor} t = - editor##setReadOnly (Js.bool t) +let set_readonly {editor} t = editor##setReadOnly (Js.bool t) -let get_state { editor } row = - editor##getSession##(getState row) +let get_state {editor} row = editor ## getSession ## (getState row) -let get_last { editor } = - let doc = (editor##getSession)##getDocument in +let get_last {editor} = + let doc = editor##getSession##getDocument in let lines = doc##getLength in - let last = doc##(getLine (lines - 1)) in + let last = doc ## (getLine (lines - 1)) in create_position (lines - 1) last##.length -let document { editor } = - (editor##getSession)##getDocument +let document {editor} = editor##getSession##getDocument -let replace doc range text = - doc##(replace range (Js.string text)) +let replace doc range text = doc ## (replace range (Js.string text)) -let delete doc range = - doc##(replace range (Js.string "")) +let delete doc range = doc ## (replace range (Js.string "")) -let remove { editor } dir = - editor##(remove (Js.string dir)) +let remove {editor} dir = editor ## (remove (Js.string dir)) diff --git a/src/ace-lib/ace.mli b/src/ace-lib/ace.mli index 21be73d09..8d6c25ad5 100644 --- a/src/ace-lib/ace.mli +++ b/src/ace-lib/ace.mli @@ -12,81 +12,101 @@ open Js_of_ocaml type 'a editor -type loc = { - loc_start: int * int; - loc_end: int * int; -} +type loc = {loc_start : int * int; loc_end : int * int} -val create_editor: Dom_html.divElement Js.t -> 'a editor +val create_editor : Dom_html.divElement Js.t -> 'a editor -val set_mode: 'a editor -> string -> unit +val set_mode : 'a editor -> string -> unit -val read_range: Ace_types.range Js.t -> (int * int) * (int * int) -val create_range: - Ace_types.position Js.t -> Ace_types.position Js.t -> Ace_types.range Js.t -val create_position: int -> int -> Ace_types.position Js.t -val read_position: Ace_types.position Js.t -> int * int -val greater_position: +val read_range : Ace_types.range Js.t -> (int * int) * (int * int) + +val create_range : + Ace_types.position Js.t -> Ace_types.position Js.t -> Ace_types.range Js.t + +val create_position : int -> int -> Ace_types.position Js.t + +val read_position : Ace_types.position Js.t -> int * int + +val greater_position : Ace_types.position Js.t -> Ace_types.position Js.t -> bool -val get_contents: ?range:Ace_types.range Js.t -> 'a editor -> string -val get_line: 'a editor -> int -> string -val set_contents: ?reset_undo:bool -> 'a editor -> string -> unit +val get_contents : ?range:Ace_types.range Js.t -> 'a editor -> string + +val get_line : 'a editor -> int -> string + +val set_contents : ?reset_undo:bool -> 'a editor -> string -> unit -val get_selection_range: 'a editor -> Ace_types.range Js.t -val get_selection: 'a editor -> string +val get_selection_range : 'a editor -> Ace_types.range Js.t + +val get_selection : 'a editor -> string type mark_type = Error | Warning | Message -val set_mark: - 'a editor -> ?loc:loc -> ?type_:mark_type -> string -> unit -val clear_marks: 'a editor -> unit -val record_event_handler: 'a editor -> string -> (unit -> unit) -> unit -val set_background_color: 'a editor -> string -> unit -val add_class: 'a editor -> string -> unit -val remove_class: 'a editor -> string -> unit +val set_mark : 'a editor -> ?loc:loc -> ?type_:mark_type -> string -> unit + +val clear_marks : 'a editor -> unit + +val record_event_handler : 'a editor -> string -> (unit -> unit) -> unit + +val set_background_color : 'a editor -> string -> unit + +val add_class : 'a editor -> string -> unit -val focus: 'a editor -> unit -val resize: 'a editor -> bool -> unit +val remove_class : 'a editor -> string -> unit -val require: string -> unit +val focus : 'a editor -> unit -val show_keybindings: 'a editor -> unit -val add_keybinding: - 'a editor -> - ?ro:bool -> - ?scrollIntoView:string -> - ?multiSelectAction:string -> - string -> string -> ('a editor -> unit) -> unit +val resize : 'a editor -> bool -> unit -val set_font_size: 'a editor -> int -> unit -val set_tab_size: 'a editor -> int -> unit -val set_readonly: 'a editor -> bool -> unit -val get_state: 'a editor -> int -> < .. > Js.t +val require : string -> unit -val get_last: 'a editor -> Ace_types.position Js.t +val show_keybindings : 'a editor -> unit + +val add_keybinding : + 'a editor + -> ?ro:bool + -> ?scrollIntoView:string + -> ?multiSelectAction:string + -> string + -> string + -> ('a editor -> unit) + -> unit + +val set_font_size : 'a editor -> int -> unit + +val set_tab_size : 'a editor -> int -> unit + +val set_readonly : 'a editor -> bool -> unit + +val get_state : 'a editor -> int -> < .. > Js.t + +val get_last : 'a editor -> Ace_types.position Js.t type doc -val document: 'a editor -> doc -val replace: doc -> Ace_types.range Js.t -> string -> unit -val delete: doc -> Ace_types.range Js.t -> unit -val remove: 'a editor -> string -> unit +val document : 'a editor -> doc -val get_custom_data: 'a editor -> 'a -val set_custom_data: 'a editor -> 'a -> unit +val replace : doc -> Ace_types.range Js.t -> string -> unit + +val delete : doc -> Ace_types.range Js.t -> unit + +val remove : 'a editor -> string -> unit + +val get_custom_data : 'a editor -> 'a + +val set_custom_data : 'a editor -> 'a -> unit (** Mode *) type token -val token: type_:string -> string -> token -type 'state helpers = { - initial_state: unit -> 'state; - get_next_line_indent: 'state -> line:string -> tab:string -> string; - get_line_tokens: string -> 'state -> int -> doc -> ('state * token list); - check_outdent: ('state -> string -> string -> bool) option; - auto_outdent: ('state -> Ace_types.document Js.t -> int -> unit) option; -} +val token : type_:string -> string -> token + +type 'state helpers = + { initial_state : unit -> 'state + ; get_next_line_indent : 'state -> line:string -> tab:string -> string + ; get_line_tokens : string -> 'state -> int -> doc -> 'state * token list + ; check_outdent : ('state -> string -> string -> bool) option + ; auto_outdent : ('state -> Ace_types.document Js.t -> int -> unit) option } -val define_mode: string -> 'state helpers -> unit +val define_mode : string -> 'state helpers -> unit diff --git a/src/ace-lib/ace_types.mli b/src/ace-lib/ace_types.mli index 1867a3128..3dd7c97e6 100644 --- a/src/ace-lib/ace_types.mli +++ b/src/ace-lib/ace_types.mli @@ -8,148 +8,235 @@ open Js_of_ocaml -class type token = object - method value : Js.js_string Js.t Js.prop - method _type : Js.js_string Js.t Js.prop -end +class type token = + object + method value : Js.js_string Js.t Js.prop + + method _type : Js.js_string Js.t Js.prop + end (** Editor *) -class type position = object - method row : int Js.prop - method column : int Js.prop -end - -class type range = object - method start : position Js.t Js.prop - method end_ : position Js.t Js.prop -end - -class type binding = object - method win : Js.js_string Js.t Js.prop - method mac : Js.js_string Js.t Js.prop -end - -class type annotation = object - method row : int Js.prop - method column : int Js.prop - method text : Js.js_string Js.t Js.prop - method type_ : Js.js_string Js.t Js.prop -end - -class type document = object - method getLine : int -> Js.js_string Js.t Js.meth - method getLines : int -> int -> Js.string_array Js.t Js.meth - method getTextRange : range Js.t -> Js.js_string Js.t Js.meth - method getValue : Js.js_string Js.t Js.meth - method replace : range Js.t -> Js.js_string Js.t -> unit Js.meth - method setValue : Js.js_string Js.t -> unit Js.meth - method getLength : int Js.meth -end - -class type undoManager = object - method undo : bool Js.t -> range Js.t Js.meth - method redo : bool Js.t -> unit Js.meth - method reset : unit Js.meth -end - -class type editSession = object - method getDocument : document Js.t Js.meth - method getTabSize : int Js.meth - method setTabSize : int -> unit Js.meth - method getTokenAt : int -> int -> token Js.t Js.meth - method replace : range Js.t -> Js.js_string Js.t -> unit Js.meth - method setMode : Js.js_string Js.t -> unit Js.meth - method setAnnotations : annotation Js.t Js.js_array Js.t -> unit Js.meth - method getAnnotations : annotation Js.t Js.js_array Js.t Js.meth - method clearAnnotations : unit Js.meth - method addMarker : - range Js.t -> Js.js_string Js.t -> Js.js_string Js.t -> bool Js.t -> - int Js.meth - method getMarkers : - bool Js.t -> int Js.js_array Js.t Js.meth - method removeMarker : int -> unit Js.meth - method getState : 'a. int -> (< .. > as 'a) Js.t Js.meth - method getUndoManager : undoManager Js.t Js.meth -end - -class type selection = object - method selectLine : unit Js.meth - method selectTo : int -> int -> unit Js.meth - method setSelectionRange : range Js.t -> bool Js.t -> unit Js.meth -end - -class type ['a] editor = object - method on : Js.js_string Js.t -> (unit -> unit) -> unit Js.meth - method commands : 'a commandManager Js.t Js.prop - method destroy : unit Js.meth - method getCursorPosition : position Js.t Js.meth - method getSelection : selection Js.t Js.meth - method getSelectionRange : range Js.t Js.meth - method getSession : editSession Js.t Js.meth - method getValue : Js.js_string Js.t Js.meth - method moveCursorTo : int -> int -> unit Js.meth - method remove : Js.js_string Js.t -> unit Js.meth - method remove_range : range Js.t -> unit Js.meth - method removeLines : unit Js.meth - method resize : bool Js.t -> unit Js.meth - method selectAll : unit Js.meth - method setReadOnly : bool Js.t -> unit Js.meth - method setSession : editSession Js.t -> unit Js.meth - method setTheme : Js.js_string Js.t -> unit Js.meth - method setValue : Js.js_string Js.t -> unit Js.meth - method setOption : Js.js_string Js.t -> 'b Js.t -> unit Js.meth - method toggleCommentLines : unit Js.meth - method focus : unit Js.meth - method setFontSize : int -> unit Js.meth - method customData : 'a Js.prop -end - -and ['a] command = object - method name : Js.js_string Js.t Js.prop - method bindKey : binding Js.t Js.prop - method exec : ('a editor Js.t -> unit -> unit) Js.callback Js.prop - method readOnly : bool Js.t Js.prop - method multiSelectAction : Js.js_string Js.t Js.prop - method scrollIntoView : Js.js_string Js.t Js.prop -end - -and ['a] commandManager = object - method addCommand : 'a command Js.t -> unit Js.meth -end - -class type ace = object - method edit: 'a. Dom_html.element Js.t -> 'a editor Js.t Js.meth -end - -class type keybinding_menu = object - method showKeyboardShortcuts: unit Js.meth -end +class type position = + object + method row : int Js.prop + + method column : int Js.prop + end + +class type range = + object + method start : position Js.t Js.prop + + method end_ : position Js.t Js.prop + end + +class type binding = + object + method win : Js.js_string Js.t Js.prop + + method mac : Js.js_string Js.t Js.prop + end + +class type annotation = + object + method row : int Js.prop + + method column : int Js.prop + + method text : Js.js_string Js.t Js.prop + + method type_ : Js.js_string Js.t Js.prop + end + +class type document = + object + method getLine : int -> Js.js_string Js.t Js.meth + + method getLines : int -> int -> Js.string_array Js.t Js.meth + + method getTextRange : range Js.t -> Js.js_string Js.t Js.meth + + method getValue : Js.js_string Js.t Js.meth + + method replace : range Js.t -> Js.js_string Js.t -> unit Js.meth + + method setValue : Js.js_string Js.t -> unit Js.meth + + method getLength : int Js.meth + end + +class type undoManager = + object + method undo : bool Js.t -> range Js.t Js.meth + + method redo : bool Js.t -> unit Js.meth + + method reset : unit Js.meth + end + +class type editSession = + object + method getDocument : document Js.t Js.meth + + method getTabSize : int Js.meth + + method setTabSize : int -> unit Js.meth + + method getTokenAt : int -> int -> token Js.t Js.meth + + method replace : range Js.t -> Js.js_string Js.t -> unit Js.meth + + method setMode : Js.js_string Js.t -> unit Js.meth + + method setAnnotations : annotation Js.t Js.js_array Js.t -> unit Js.meth + + method getAnnotations : annotation Js.t Js.js_array Js.t Js.meth + + method clearAnnotations : unit Js.meth + + method addMarker : + range Js.t + -> Js.js_string Js.t + -> Js.js_string Js.t + -> bool Js.t + -> int Js.meth + + method getMarkers : bool Js.t -> int Js.js_array Js.t Js.meth + + method removeMarker : int -> unit Js.meth + + method getState : 'a. int -> (< .. > as 'a) Js.t Js.meth + + method getUndoManager : undoManager Js.t Js.meth + end + +class type selection = + object + method selectLine : unit Js.meth + + method selectTo : int -> int -> unit Js.meth + + method setSelectionRange : range Js.t -> bool Js.t -> unit Js.meth + end + +class type ['a] editor = + object + method on : Js.js_string Js.t -> (unit -> unit) -> unit Js.meth + + method commands : 'a commandManager Js.t Js.prop + + method destroy : unit Js.meth + + method getCursorPosition : position Js.t Js.meth + + method getSelection : selection Js.t Js.meth + + method getSelectionRange : range Js.t Js.meth + + method getSession : editSession Js.t Js.meth + + method getValue : Js.js_string Js.t Js.meth + + method moveCursorTo : int -> int -> unit Js.meth + + method remove : Js.js_string Js.t -> unit Js.meth + + method remove_range : range Js.t -> unit Js.meth + + method removeLines : unit Js.meth + + method resize : bool Js.t -> unit Js.meth + + method selectAll : unit Js.meth + + method setReadOnly : bool Js.t -> unit Js.meth + + method setSession : editSession Js.t -> unit Js.meth + + method setTheme : Js.js_string Js.t -> unit Js.meth + + method setValue : Js.js_string Js.t -> unit Js.meth + + method setOption : Js.js_string Js.t -> 'b Js.t -> unit Js.meth + + method toggleCommentLines : unit Js.meth + + method focus : unit Js.meth + + method setFontSize : int -> unit Js.meth + + method customData : 'a Js.prop + end + +and ['a] command = + object + method name : Js.js_string Js.t Js.prop + + method bindKey : binding Js.t Js.prop + + method exec : ('a editor Js.t -> unit -> unit) Js.callback Js.prop + + method readOnly : bool Js.t Js.prop + + method multiSelectAction : Js.js_string Js.t Js.prop + + method scrollIntoView : Js.js_string Js.t Js.prop + end + +and ['a] commandManager = + object + method addCommand : 'a command Js.t -> unit Js.meth + end + +class type ace = + object + method edit : 'a. Dom_html.element Js.t -> 'a editor Js.t Js.meth + end + +class type keybinding_menu = + object + method showKeyboardShortcuts : unit Js.meth + end (** Mode *) -class type ['a] line_tokens = object - method state : 'a Js.prop - method tokens : token Js.t Js.js_array Js.t Js.prop -end - -class type ['state] ace_mode_helpers = object - method initialState : - (unit -> 'state) Js.callback Js.writeonly_prop - method getNextLineIndent : - ('state -> Js.js_string Js.t -> Js.js_string Js.t -> Js.js_string Js.t) - Js.callback Js.writeonly_prop - method getLineTokens : - (Js.js_string Js.t -> 'state -> int -> document Js.t -> - 'state line_tokens Js.t) Js.callback Js.writeonly_prop - method checkOutdent : - ('state -> Js.js_string Js.t -> Js.js_string Js.t -> bool Js.t) - Js.callback Js.writeonly_prop - method autoOutdent : - ('state -> document Js.t -> int -> unit) Js.callback Js.writeonly_prop -end - -class type ace_mode = object - method define : - Js.js_string Js.t -> 'state ace_mode_helpers -> unit Js.meth -end +class type ['a] line_tokens = + object + method state : 'a Js.prop + + method tokens : token Js.t Js.js_array Js.t Js.prop + end + +class type ['state] ace_mode_helpers = + object + method initialState : (unit -> 'state) Js.callback Js.writeonly_prop + + method getNextLineIndent : + ('state -> Js.js_string Js.t -> Js.js_string Js.t -> Js.js_string Js.t) + Js.callback + Js.writeonly_prop + + method getLineTokens : + ( Js.js_string Js.t + -> 'state + -> int + -> document Js.t + -> 'state line_tokens Js.t) + Js.callback + Js.writeonly_prop + + method checkOutdent : + ('state -> Js.js_string Js.t -> Js.js_string Js.t -> bool Js.t) + Js.callback + Js.writeonly_prop + + method autoOutdent : + ('state -> document Js.t -> int -> unit) Js.callback Js.writeonly_prop + end + +class type ace_mode = + object + method define : + Js.js_string Js.t -> 'state ace_mode_helpers -> unit Js.meth + end diff --git a/src/ace-lib/ocaml_mode.ml b/src/ace-lib/ocaml_mode.ml index e1af420cd..5dc9302c3 100644 --- a/src/ace-lib/ocaml_mode.ml +++ b/src/ace-lib/ocaml_mode.ml @@ -11,6 +11,7 @@ open Js_of_ocaml open Lwt.Infix let debug_indent = ref 0 + (* <= 0: nothing *) (* 1: fun call *) (* 2: ocp-indent stacks *) @@ -18,208 +19,79 @@ let debug_indent = ref 0 let token_type = let open Approx_tokens in function - - | COMMENT_OPEN_EOL - | COMMENT_OPEN_CLOSE - | COMMENT_OPEN - | COMMENT_VERB_OPEN - | COMMENT_CODE_OPEN - | COMMENT_CONTENT - | COMMENT_CLOSE - | COMMENT_VERB_CLOSE - | COMMENT_CODE_CLOSE -> "comment" - - | AND - | AS - | ASSERT - | BEGIN - | CLASS - | CONSTRAINT - | DO - | DONE - | DOWNTO - | ELSE - | END - | EXCEPTION - | EXTERNAL - | FOR - | FUN - | FUNCTION - | FUNCTOR - | IF - | IN - | INCLUDE - | INHERIT - | INITIALIZER - | LAZY - | LET - | MATCH - | METHOD - | MODULE - | MUTABLE - | NEW - | OBJECT - | OF - | OPEN - | OR - | PRIVATE - | REC - | SIG - | STRUCT - | THEN - | TO - | TRY - | TYPE - | VAL - | VIRTUAL - | WHEN - | WHILE - | WITH - - | INFIXOP3 "mod" - | INFIXOP3 "land" - | INFIXOP3 "lor" - | INFIXOP3 "lxor" - | INFIXOP4 "lsl" - | INFIXOP4 "lsr" - | INFIXOP4 "asr" -> "keyword" - - | FLOAT _ - | INT _ - | INT32 _ - | INT64 _ - | NATIVEINT _ -> "constant" - - | INFIXOP0 _ - | INFIXOP1 _ - | INFIXOP2 _ - | INFIXOP3 _ - | INFIXOP4 _ - | PREFIXOP _ -> "function" - - | LABEL _ - | OPTLABEL _ -> "type" (* Hack *) - - | AMPERAMPER - | AMPERSAND - | BACKQUOTE - | BANG - | BAR - | BARBAR - | BARRBRACKET - | COLON - | COLONCOLON - | COLONEQUAL - | COLONGREATER - | COMMA - | DOT - | DOTDOT - | EQUAL - | GREATER - | GREATERRBRACE - | GREATERRBRACKET - | LBRACE - | LBRACELESS - | LBRACKET - | LBRACKETAT - | LBRACKETATAT - | LBRACKETATATAT - | LBRACKETBAR - | LBRACKETGREATER - | LBRACKETLESS - | LBRACKETPERCENT - | LBRACKETPERCENTPERCENT - | LESS - | LESSMINUS - | LPAREN - | MINUS - | MINUSDOT - | MINUSGREATER - | PLUS - | PLUSDOT - | QUESTION - | QUESTIONQUESTION - | QUOTE - | RBRACE - | RBRACKET - | RPAREN - | SEMI - | SEMISEMI - | SHARP - | STAR - | TILDE - | UNDERSCORE -> "operator" - + | COMMENT_OPEN_EOL | COMMENT_OPEN_CLOSE | COMMENT_OPEN | COMMENT_VERB_OPEN + |COMMENT_CODE_OPEN | COMMENT_CONTENT | COMMENT_CLOSE | COMMENT_VERB_CLOSE + |COMMENT_CODE_CLOSE -> + "comment" + | AND | AS | ASSERT | BEGIN | CLASS | CONSTRAINT | DO | DONE | DOWNTO + |ELSE | END | EXCEPTION | EXTERNAL | FOR | FUN | FUNCTION | FUNCTOR | IF + |IN | INCLUDE | INHERIT | INITIALIZER | LAZY | LET | MATCH | METHOD + |MODULE | MUTABLE | NEW | OBJECT | OF | OPEN | OR | PRIVATE | REC | SIG + |STRUCT | THEN | TO | TRY | TYPE | VAL | VIRTUAL | WHEN | WHILE | WITH + |INFIXOP3 "mod" + |INFIXOP3 "land" + |INFIXOP3 "lor" + |INFIXOP3 "lxor" + |INFIXOP4 "lsl" + |INFIXOP4 "lsr" + |INFIXOP4 "asr" -> + "keyword" + | FLOAT _ | INT _ | INT32 _ | INT64 _ | NATIVEINT _ -> "constant" + | INFIXOP0 _ | INFIXOP1 _ | INFIXOP2 _ | INFIXOP3 _ | INFIXOP4 _ | PREFIXOP _ + -> + "function" + | LABEL _ | OPTLABEL _ -> "type" (* Hack *) + | AMPERAMPER | AMPERSAND | BACKQUOTE | BANG | BAR | BARBAR | BARRBRACKET + |COLON | COLONCOLON | COLONEQUAL | COLONGREATER | COMMA | DOT | DOTDOT + |EQUAL | GREATER | GREATERRBRACE | GREATERRBRACKET | LBRACE | LBRACELESS + |LBRACKET | LBRACKETAT | LBRACKETATAT | LBRACKETATATAT | LBRACKETBAR + |LBRACKETGREATER | LBRACKETLESS | LBRACKETPERCENT | LBRACKETPERCENTPERCENT + |LESS | LESSMINUS | LPAREN | MINUS | MINUSDOT | MINUSGREATER | PLUS + |PLUSDOT | QUESTION | QUESTIONQUESTION | QUOTE | RBRACE | RBRACKET + |RPAREN | SEMI | SEMISEMI | SHARP | STAR | TILDE | UNDERSCORE -> + "operator" | LINE_DIRECTIVE _ -> "meta" - - | FALSE - | TRUE - | LIDENT _ - | UIDENT _ - | TYPEVAR -> "variable" - - | EOL - | SPACES -> "text" + | FALSE | TRUE | LIDENT _ | UIDENT _ | TYPEVAR -> "variable" + | EOL | SPACES -> "text" | ILLEGAL_CHAR _ -> "error" - | EOF -> assert false + | CHAR _ | STRING_OPEN | STRING_CONTENT | ESCAPED_EOL | STRING_CLOSE + |PPX_QUOTATION_OPEN | PPX_QUOTATION_CONTENT | PPX_QUOTATION_CLOSE -> + "string" + | P4_QUOTATION_OPEN | P4_QUOTATION_CONTENT | P4_QUOTATION_CLOSE -> "meta" - | CHAR _ +type state = {block : IndentBlock.t; lex_ctxt : Nstream.snapshot} - | STRING_OPEN - | STRING_CONTENT - | ESCAPED_EOL - | STRING_CLOSE - - | PPX_QUOTATION_OPEN - | PPX_QUOTATION_CONTENT - | PPX_QUOTATION_CLOSE -> "string" - - | P4_QUOTATION_OPEN - | P4_QUOTATION_CONTENT - | P4_QUOTATION_CLOSE -> "meta" - -type state = { - block: IndentBlock.t; - lex_ctxt: Nstream.snapshot; -} - -let initial_state = { - block = IndentBlock.empty ; - lex_ctxt = - { Approx_lexer.initial_state with Approx_lexer.eof_closing = false }, - Nstream.Region.zero; -} +let initial_state = + { block = IndentBlock.empty + ; lex_ctxt = + ( {Approx_lexer.initial_state with Approx_lexer.eof_closing = false} + , Nstream.Region.zero ) } let in_comment ctxt = let open Approx_lexer in let rec loop = function | [] | Code :: _ -> false | Comment :: _ -> true - | _ :: stack -> loop stack in + | _ :: stack -> loop stack + in loop ctxt.stack - (* List.mem Comment ctxt.stack *) + +(* List.mem Comment ctxt.stack *) let wrap_token state token = let open Nstream in let value = token.between ^ token.substr in let type_ = - if in_comment (fst state.lex_ctxt) then - "comment" - else - token_type token.token in + if in_comment (fst state.lex_ctxt) then "comment" + else token_type token.token + in Ace.token ~type_ value -type config = { - indent: IndentConfig.t; - forced: bool; -} +type config = {indent : IndentConfig.t; forced : bool} let config = - ref { - indent = IndentConfig.({default with i_match_clause = 4}); - forced = true; - } + ref {indent = IndentConfig.{default with i_match_clause = 4}; forced = true} let get_next_line_indent state ~line ~tab = let indent = IndentBlock.guess_indent state.block in @@ -232,8 +104,7 @@ let comment_open between = Ace.token ~type_:"comment" (between ^ "(*") let phrases : (Ace.doc * Ace_types.position Js.t list ref) list ref = ref [] let get_phrases doc = - try List.assq doc !phrases - with Not_found -> + try List.assq doc !phrases with Not_found -> let ph = ref [] in phrases := (doc, ph) :: !phrases; ph @@ -242,20 +113,20 @@ let remove_phrases doc row = let rec remove row = function | [] -> [] | pos' :: _ as phrases when Ace.greater_position row pos' -> phrases - | _ :: phrases -> remove row phrases in + | _ :: phrases -> remove row phrases + in let phrases = get_phrases doc in phrases := remove (Ace.create_position row 0) !phrases let mark_phrase doc pos = let phrases = get_phrases doc in phrases := pos :: !phrases; - if !debug_indent > 0 then begin + if !debug_indent > 0 then ( debug "Phrases:"; - List.iter js_debug (List.rev !phrases) - end + List.iter js_debug (List.rev !phrases) ) let all_spaces s = - let rec loop s l i = i >= l || s.[i] = ' ' && loop s l (i+1) in + let rec loop s l i = i >= l || (s.[i] = ' ' && loop s l (i + 1)) in loop s (String.length s) 0 let get_line_tokens line st row doc = @@ -266,17 +137,17 @@ let get_line_tokens line st row doc = let open Approx_tokens in let open Nstream in match Nstream.next_full stream with - | None | Some ({token = EOF ; _}, _, _) -> st, List.rev tokens - | Some (tok, lex_ctxt, stream) -> - let block = IndentBlock.update !config.indent st.block stream tok; in + | None | Some ({token = EOF; _}, _, _) -> (st, List.rev tokens) + | Some (tok, lex_ctxt, stream) -> ( + let block = IndentBlock.update !config.indent st.block stream tok in let tok, block, offset = - if not first || all_spaces line || IndentBlock.is_in_comment block then - tok, block, offset + if (not first) || all_spaces line || IndentBlock.is_in_comment block + then (tok, block, offset) else if not !config.forced then (* Update ocp-indent context with current indentation. *) - tok, IndentBlock.reverse block, 0 + (tok, IndentBlock.reverse block, 0) else if IndentBlock.indent block = String.length tok.between then - tok, block, 0 + (tok, block, 0) else (* Update line to the 'forced' indentation. *) let old_indent = String.length tok.between in @@ -284,65 +155,54 @@ let get_line_tokens line st row doc = if !debug_indent > 0 then debug "Reindent: new indent %d (old: %d)" indent old_indent; let spaces = String.make indent ' ' in - Ace.replace - doc + Ace.replace doc (Ace.create_range (Ace.create_position row 0) (Ace.create_position row old_indent)) spaces; - { tok with between = spaces }, block, (indent - old_indent) in + ({tok with between = spaces}, block, indent - old_indent) + in let col = Nstream.(Region.start_column tok.region) in if IndentBlock.is_at_top block then mark_phrase doc (Ace.create_position row (col + offset)); - if !debug_indent > 1 && tok.token <> EOL && tok.token <> ESCAPED_EOL then - IndentBlock.dump block; - let st = { block; lex_ctxt; } in + if !debug_indent > 1 && tok.token <> EOL && tok.token <> ESCAPED_EOL + then IndentBlock.dump block; + let st = {block; lex_ctxt} in match tok.token with - | EOL | ESCAPED_EOL -> - (* FIXME some spaces ??? *) - (st, List.rev tokens) + | EOL | ESCAPED_EOL -> (* FIXME some spaces ??? *) + (st, List.rev tokens) | COMMENT_OPEN_EOL -> (st, List.rev (comment_open tok.between :: tokens)) - | _ -> - iter st offset stream (wrap_token st tok :: tokens) + | _ -> iter st offset stream (wrap_token st tok :: tokens) ) in iter ~first:true st 0 stream [] let () = let open Ace in let initial_state () = initial_state in - define_mode "ocaml.ocp" { - initial_state; - get_next_line_indent; - get_line_tokens; - check_outdent = None; - auto_outdent = None; - } - -type loc = Ace.loc = { - loc_start: int * int; - loc_end: int * int; -} - -type error = { - locs: loc list; - msg: string; -} - -type warning = { - loc: loc; - msg: string; -} - -type editor = { - ace: editor Ace.editor; - mutable current_error: error option; - mutable current_warnings: warning list; -} - -let get_editor { ace; _ } = ace -let get_current_error { current_error; _ } = current_error -let get_current_warnings { current_warnings; _ } = current_warnings + define_mode "ocaml.ocp" + { initial_state + ; get_next_line_indent + ; get_line_tokens + ; check_outdent = None + ; auto_outdent = None } + +type loc = Ace.loc = {loc_start : int * int; loc_end : int * int} + +type error = {locs : loc list; msg : string} + +type warning = {loc : loc; msg : string} + +type editor = + { ace : editor Ace.editor + ; mutable current_error : error option + ; mutable current_warnings : warning list } + +let get_editor {ace; _} = ace + +let get_current_error {current_error; _} = current_error + +let get_current_warnings {current_warnings; _} = current_warnings let reset_error editor = editor.current_error <- None; @@ -350,170 +210,155 @@ let reset_error editor = Ace.clear_marks editor.ace; Ace.remove_class editor.ace "ocaml-check-error"; Ace.remove_class editor.ace "ocaml-check-warn"; - Ace.remove_class editor.ace "ocaml-check-success" ; + Ace.remove_class editor.ace "ocaml-check-success"; Lwt_js.sleep 0.1 let report_error editor ?(set_class = true) err warnings = - reset_error editor >>= fun () -> - Lwt_js.yield () >|= fun () -> - let add_warning editor { loc; msg } = - Ace.set_mark editor ~loc ~type_:Ace.Warning msg in + reset_error editor + >>= fun () -> + Lwt_js.yield () + >|= fun () -> + let add_warning editor {loc; msg} = + Ace.set_mark editor ~loc ~type_:Ace.Warning msg + in editor.current_error <- err; editor.current_warnings <- warnings; - match err, warnings with + match (err, warnings) with | None, [] -> - if set_class then - Ace.add_class editor.ace "ocaml-check-success"; + if set_class then Ace.add_class editor.ace "ocaml-check-success" | None, warnings -> - if set_class then - Ace.add_class editor.ace "ocaml-check-warn"; + if set_class then Ace.add_class editor.ace "ocaml-check-warn"; List.iter (add_warning editor.ace) warnings - | Some { locs; msg }, warnings -> - if set_class then - Ace.add_class editor.ace "ocaml-check-error"; + | Some {locs; msg}, warnings -> ( + if set_class then Ace.add_class editor.ace "ocaml-check-error"; List.iter (add_warning editor.ace) warnings; match locs with - | [] -> - Ace.set_mark editor.ace ~type_:Ace.Error msg + | [] -> Ace.set_mark editor.ace ~type_:Ace.Error msg | locs -> List.iter (fun loc -> Ace.set_mark editor.ace ~loc ~type_:Ace.Error msg) - locs + locs ) let report_current_error editor ?set_class () = report_error editor ?set_class editor.current_error editor.current_warnings let get_state editor row = let s = Ace.get_state editor row in - if Js.to_string (Js.typeof s) = "string" then - initial_state - else - (Obj.magic s : state) + if Js.to_string (Js.typeof s) = "string" then initial_state + else (Obj.magic s : state) let get_old_indent line = let rec loop line len i = - if i < len && line.[i] = ' ' then loop line len (i+1) else i in + if i < len && line.[i] = ' ' then loop line len (i + 1) else i + in loop line (String.length line) 0 let get_indent state line = debug "Indent!"; IndentBlock.dump state.block; match Nstream.(next (of_string ~st:state.lex_ctxt line)) with - | None | Some ({ Nstream.token = Approx_tokens.EOF; _ } , _) -> + | None | Some ({Nstream.token = Approx_tokens.EOF; _}, _) -> IndentBlock.guess_indent state.block | Some _ when IndentBlock.is_in_comment state.block -> IndentBlock.guess_indent state.block | Some (token, stream) -> if !debug_indent > 1 then IndentBlock.dump state.block; - let block = - IndentBlock.update !config.indent state.block stream token in + let block = IndentBlock.update !config.indent state.block stream token in if !debug_indent > 1 then IndentBlock.dump block; IndentBlock.indent block let do_indent ace_editor = - let ((row, _col), _) = + let (row, _col), _ = (* TODO when multiple line are selected... *) - Ace.read_range (Ace.get_selection_range ace_editor) in + Ace.read_range (Ace.get_selection_range ace_editor) + in let state = get_state ace_editor (row - 1) in - if IndentBlock.is_in_comment state.block || not !config.forced then begin + if IndentBlock.is_in_comment state.block || not !config.forced then ( if !debug_indent > 0 then - debug "Tab-indent: line %d (%a)" - row Approx_lexer.print_context (fst state.lex_ctxt); + debug "Tab-indent: line %d (%a)" row Approx_lexer.print_context + (fst state.lex_ctxt); let line = Ace.get_line ace_editor row in let old_indent = get_old_indent line in let indent = get_indent state line in if !debug_indent > 0 then debug "Tab-indent: new indent %d (old: %d)" indent old_indent; if old_indent <> indent && indent >= 0 then - Ace.replace - (Ace.document ace_editor) + Ace.replace (Ace.document ace_editor) (Ace.create_range (Ace.create_position row 0) (Ace.create_position row old_indent)) - (String.make indent ' ') - end + (String.make indent ' ') ) let rec all_spaces line i max = - i >= max || - ((line.[i] = ' ' || line.[i] = '\n') && all_spaces line (succ i) max) + i >= max + || ((line.[i] = ' ' || line.[i] = '\n') && all_spaces line (succ i) max) let rec all_trailing_spaces line i = - if i <= 0 || line.[i] <> ' ' then - i - else - all_trailing_spaces line (i-1) + if i <= 0 || line.[i] <> ' ' then i else all_trailing_spaces line (i - 1) let remove_trailing_spaces line = let last = String.length line - 1 in let last_non_space = all_trailing_spaces line last in - if last != last_non_space then - String.sub line 0 (last_non_space + 1) - else - line + if last != last_non_space then String.sub line 0 (last_non_space + 1) + else line let may_reset_indent ace_editor = - let (_, (row, _col)) = - Ace.read_range (Ace.get_selection_range ace_editor) in + let _, (row, _col) = Ace.read_range (Ace.get_selection_range ace_editor) in let line = Ace.get_line ace_editor row in - if all_spaces line 0 (String.length line) then begin - let state = get_state ace_editor (row-1) in - if not (IndentBlock.is_in_comment state.block) then begin + if all_spaces line 0 (String.length line) then + let state = get_state ace_editor (row - 1) in + if not (IndentBlock.is_in_comment state.block) then let indent = IndentBlock.guess_indent state.block in let old_indent = get_old_indent line in - Ace.replace - (Ace.document ace_editor) + Ace.replace (Ace.document ace_editor) (Ace.create_range (Ace.create_position row 0) (Ace.create_position row old_indent)) (String.make indent ' ') - end - end let do_delete ace_editor = - if !config.forced then begin - let ((row, col), (row2, _col2)) = - Ace.read_range (Ace.get_selection_range ace_editor) in + if !config.forced then ( + let (row, col), (row2, _col2) = + Ace.read_range (Ace.get_selection_range ace_editor) + in if !debug_indent > 0 then debug "Delete: line %d col %d -> line %d col %d" row col row2 _col2; let selected = Ace.get_selection ace_editor in let line = Ace.get_line ace_editor row in - let state = get_state ace_editor (row2-1) in - if IndentBlock.is_in_comment state.block then - Ace.remove ace_editor "left" - else if not (all_spaces line 0 (min col (String.length line)) && - all_spaces selected 0 (String.length selected)) then begin + let state = get_state ace_editor (row2 - 1) in + if IndentBlock.is_in_comment state.block then Ace.remove ace_editor "left" + else if + not + ( all_spaces line 0 (min col (String.length line)) + && all_spaces selected 0 (String.length selected) ) + then ( Ace.remove ace_editor "left"; - may_reset_indent ace_editor - end else if row > 0 then begin - let raw_prev_line = Ace.get_line ace_editor (row-1) in + may_reset_indent ace_editor ) + else if row > 0 then + let raw_prev_line = Ace.get_line ace_editor (row - 1) in let prev_line = remove_trailing_spaces raw_prev_line in let old_indent = get_old_indent (Ace.get_line ace_editor row2) in if String.length prev_line = 0 then - Ace.delete - (Ace.document ace_editor) + Ace.delete (Ace.document ace_editor) (Ace.create_range - (Ace.create_position (row-1) (String.length raw_prev_line)) + (Ace.create_position (row - 1) (String.length raw_prev_line)) (Ace.create_position row2 old_indent)) else - Ace.replace - (Ace.document ace_editor) + Ace.replace (Ace.document ace_editor) (Ace.create_range - (Ace.create_position (row-1) (String.length prev_line)) + (Ace.create_position (row - 1) (String.length prev_line)) (Ace.create_position row2 old_indent)) - " " - end - end else begin - Ace.remove ace_editor "left" - end + " " ) + else Ace.remove ace_editor "left" let create_ocaml_editor div = let ace = Ace.create_editor div in Ace.set_mode ace "ace/mode/ocaml.ocp"; Ace.set_tab_size ace !config.indent.IndentConfig.i_base; - let editor = { ace; current_error = None; current_warnings = [] } in + let editor = {ace; current_error = None; current_warnings = []} in Ace.set_custom_data editor.ace editor; - Ace.record_event_handler editor.ace "change" - (fun () -> Lwt.async (fun () -> reset_error editor)); + Ace.record_event_handler editor.ace "change" (fun () -> + Lwt.async (fun () -> reset_error editor) ); Ace.add_keybinding editor.ace "backspace" "Shift-Backspace|Backspace" do_delete; Ace.add_keybinding editor.ace "indent" "Tab" do_indent; diff --git a/src/ace-lib/ocaml_mode.mli b/src/ace-lib/ocaml_mode.mli index 231360851..b58e5ec6c 100644 --- a/src/ace-lib/ocaml_mode.mli +++ b/src/ace-lib/ocaml_mode.mli @@ -10,28 +10,23 @@ open Js_of_ocaml type editor -type loc = Ace.loc = { - loc_start: int * int; - loc_end: int * int; -} +type loc = Ace.loc = {loc_start : int * int; loc_end : int * int} -type error = { - locs: loc list; - msg: string; -} +type error = {locs : loc list; msg : string} -type warning = { - loc: loc; - msg: string; -} +type warning = {loc : loc; msg : string} -val create_ocaml_editor: Dom_html.divElement Js.t -> editor -val get_editor: editor -> editor Ace.editor +val create_ocaml_editor : Dom_html.divElement Js.t -> editor -val report_error: editor -> ?set_class: bool -> error option -> warning list -> unit Lwt.t -val report_current_error: editor -> ?set_class: bool -> unit -> unit Lwt.t +val get_editor : editor -> editor Ace.editor -val get_current_error: editor -> error option -val get_current_warnings: editor -> warning list +val report_error : + editor -> ?set_class:bool -> error option -> warning list -> unit Lwt.t + +val report_current_error : editor -> ?set_class:bool -> unit -> unit Lwt.t + +val get_current_error : editor -> error option + +val get_current_warnings : editor -> warning list val token_type : Approx_tokens.token -> string diff --git a/src/app/learnocaml_common.ml b/src/app/learnocaml_common.ml index bd9c10aff..cca1f815e 100644 --- a/src/app/learnocaml_common.ml +++ b/src/app/learnocaml_common.ml @@ -11,14 +11,13 @@ open Js_utils open Lwt.Infix open Learnocaml_data open Learnocaml_config - module H = Tyxml_js.Html let find_div_or_append_to_body id = match Manip.by_id id with | Some div -> div | None -> - let div = H.(div ~a:[ a_id id ]) [] in + let div = H.(div ~a:[a_id id]) [] in Manip.(appendChild Elt.body) div; div @@ -30,251 +29,248 @@ let find_component id = let fake_download ~name ~contents = (* TODO: add some primitives to jsoo and clean this up *) let blob : (Js.js_string Js.t Js.js_array Js.t -> File.blob Js.t) Js.constr = - Js.Unsafe.global ##. _Blob in - let blob = new%js blob (Js.array [| contents |]) in + Js.Unsafe.global##._Blob + in + let blob = new%js blob (Js.array [|contents|]) in let url = - Js.Unsafe.meth_call (Js.Unsafe.global##._URL) "createObjectURL" [| Js.Unsafe.inject blob |] in + Js.Unsafe.meth_call Js.Unsafe.global##._URL "createObjectURL" + [|Js.Unsafe.inject blob|] + in let link = Dom_html.createA Dom_html.document in - link##.href := url ; - Js.Unsafe.set link (Js.string "download") (Js.string name) ; - ignore (Dom_html.document##.body##(appendChild ((link :> Dom.node Js.t)))) ; - ignore (Js.Unsafe.meth_call link "click" [||]) ; - ignore (Dom_html.document##.body##(removeChild ((link :> Dom.node Js.t)))) + link##.href := url; + Js.Unsafe.set link (Js.string "download") (Js.string name); + ignore Dom_html.document ##. body ## (appendChild (link :> Dom.node Js.t)); + ignore (Js.Unsafe.meth_call link "click" [||]); + ignore Dom_html.document ##. body ## (removeChild (link :> Dom.node Js.t)) let fake_upload () = let input_files_load = - Dom_html.createInput ~_type: (Js.string "file") Dom_html.document in + Dom_html.createInput ~_type:(Js.string "file") Dom_html.document + in let result_t, result_wakener = Lwt.wait () in let fail () = Lwt.wakeup_exn result_wakener - (Failure "file loading not implemented for this browser") ; - Js._true in - input_files_load##.onchange := Dom.handler (fun ev -> - Js.Opt.case (ev##.target) fail @@ fun target -> - Js.Opt.case (Dom_html.CoerceTo.input target) fail @@ fun input -> - Js.Optdef.case (input##.files) fail @@ fun files -> - Js.Opt.case (files##(item (0))) fail @@ fun file -> - let name = Js.to_string file##.name in - let fileReader = new%js File.fileReader in - fileReader##.onload := Dom.handler (fun ev -> - Js.Opt.case (ev##.target) fail @@ fun target -> - Js.Opt.case (File.CoerceTo.string (target##.result)) fail @@ fun result -> - Lwt.wakeup result_wakener (name, result) ; - Js._true) ; - fileReader##(readAsText file) ; - Js._true) ; - ignore (Js.Unsafe.meth_call input_files_load "click" [||]) ; + (Failure "file loading not implemented for this browser"); + Js._true + in + input_files_load##.onchange + := Dom.handler (fun ev -> + Js.Opt.case ev##.target fail + @@ fun target -> + Js.Opt.case (Dom_html.CoerceTo.input target) fail + @@ fun input -> + Js.Optdef.case input##.files fail + @@ fun files -> + Js.Opt.case files ## (item 0) fail + @@ fun file -> + let name = Js.to_string file##.name in + let fileReader = new%js File.fileReader in + fileReader##.onload := + Dom.handler (fun ev -> + Js.Opt.case ev##.target fail + @@ fun target -> + Js.Opt.case (File.CoerceTo.string target##.result) fail + @@ fun result -> + Lwt.wakeup result_wakener (name, result); + Js._true ); + fileReader ## (readAsText file); + Js._true ); + ignore (Js.Unsafe.meth_call input_files_load "click" [||]); result_t -let fatal ?(title=[%i"INTERNAL ERROR"]) message = +let fatal ?(title = [%i "INTERNAL ERROR"]) message = let titletext = title in let id = "ocp-fatal-layer" in - let div = match Manip.by_id id with + let div = + match Manip.by_id id with | Some div -> div | None -> let div = - H.div ~a:[ H.a_id id ; - H.a_class ["learnocaml-dialog-overlay"] - ] - [] + H.div ~a:[H.a_id id; H.a_class ["learnocaml-dialog-overlay"]] [] in Manip.(appendChild Elt.body) div; - div in - Manip.replaceChildren div [ - H.div [ - H.h3 [ H.txt titletext ]; - H.div [ H.p [ H.txt (String.trim message) ] ]; - ] - ] + div + in + Manip.replaceChildren div + [H.div [H.h3 [H.txt titletext]; H.div [H.p [H.txt (String.trim message)]]]] let dialog_layer_id = "ocp-dialog-layer" let box_button txt f = - H.button ~a: [ - H.a_onclick (fun _ -> - f (); - match Manip.by_id dialog_layer_id with - | Some div -> Manip.removeChild Manip.Elt.body div; false - | None -> (); false) - ] [ H.txt txt ] - -let close_button txt = - box_button txt @@ fun () -> () - -let ext_alert ~title ?(buttons = [close_button [%i"OK"]]) message = - let div = match Manip.by_id dialog_layer_id with + H.button + ~a: + [ H.a_onclick (fun _ -> + f (); + match Manip.by_id dialog_layer_id with + | Some div -> + Manip.removeChild Manip.Elt.body div; + false + | None -> (); false ) ] + [H.txt txt] + +let close_button txt = box_button txt @@ fun () -> () + +let ext_alert ~title ?(buttons = [close_button [%i "OK"]]) message = + let div = + match Manip.by_id dialog_layer_id with | Some div -> div | None -> let div = - H.div ~a:[ H.a_id dialog_layer_id ; - H.a_class ["learnocaml-dialog-overlay"] ] + H.div + ~a:[H.a_id dialog_layer_id; H.a_class ["learnocaml-dialog-overlay"]] [] in Manip.(appendChild Elt.body) div; - div in - Manip.replaceChildren div [ - H.div [ - H.h3 [ H.txt title ]; - H.div message; - H.div ~a:[ H.a_class ["buttons"] ] buttons; - ] - ] + div + in + Manip.replaceChildren div + [ H.div + [ H.h3 [H.txt title] + ; H.div message + ; H.div ~a:[H.a_class ["buttons"]] buttons ] ] let lwt_alert ~title ~buttons message = let waiter, wakener = Lwt.task () in let buttons = - List.map (fun (txt, f) -> + List.map + (fun (txt, f) -> box_button txt (fun () -> - Lwt.async @@ fun () -> - f () >|= Lwt.wakeup_later wakener)) + Lwt.async @@ fun () -> f () >|= Lwt.wakeup_later wakener ) ) buttons in ext_alert ~title message ~buttons; waiter -let alert ?(title=[%i"ERROR"]) ?buttons message = - ext_alert ~title ?buttons [ H.p [H.txt (String.trim message)] ] +let alert ?(title = [%i "ERROR"]) ?buttons message = + ext_alert ~title ?buttons [H.p [H.txt (String.trim message)]] -let confirm ~title ?(ok_label=[%i"OK"]) ?(cancel_label=[%i"Cancel"]) contents f = - ext_alert ~title contents ~buttons:[ - box_button ok_label f; - close_button cancel_label; - ] +let confirm ~title ?(ok_label = [%i "OK"]) ?(cancel_label = [%i "Cancel"]) + contents f = + ext_alert ~title contents + ~buttons:[box_button ok_label f; close_button cancel_label] -let ask_string ~title ?(ok_label=[%i"OK"]) contents = - let input_field = - H.input ~a:[ - H.a_input_type `Text; - ] () - in +let ask_string ~title ?(ok_label = [%i "OK"]) contents = + let input_field = H.input ~a:[H.a_input_type `Text] () in let result_t, up = Lwt.wait () in - ext_alert ~title (contents @ [input_field]) ~buttons:[ - box_button ok_label (fun () -> Lwt.wakeup up @@ Manip.value input_field) - ]; + ext_alert ~title + (contents @ [input_field]) + ~buttons: + [box_button ok_label (fun () -> Lwt.wakeup up @@ Manip.value input_field)]; result_t let default_exn_printer = function | Failure msg -> msg | e -> Printexc.to_string e -let catch_with_alert ?(printer=default_exn_printer) f = - Lwt.catch f @@ fun exn -> alert (printer exn); Lwt.return_unit +let catch_with_alert ?(printer = default_exn_printer) f = + Lwt.catch f + @@ fun exn -> + alert (printer exn); + Lwt.return_unit let hide_loading ?(id = "ocp-loading-layer") () = let elt = find_div_or_append_to_body id in - Manip.(removeClass elt "initial") ; - Manip.(removeClass elt "loading") ; + Manip.(removeClass elt "initial"); + Manip.(removeClass elt "loading"); Manip.(addClass elt "loaded") let show_loading ?(id = "ocp-loading-layer") contents f = let show () = let elt = find_div_or_append_to_body id in - Manip.(addClass elt "loading-layer") ; - Manip.(removeClass elt "loaded") ; - Manip.(addClass elt "loading") ; + Manip.(addClass elt "loading-layer"); + Manip.(removeClass elt "loaded"); + Manip.(addClass elt "loading"); let chamo_src = - api_server ^ "/icons/tryocaml_loading_" ^ string_of_int (Random.int 9 + 1) ^ ".gif" in + api_server ^ "/icons/tryocaml_loading_" + ^ string_of_int (Random.int 9 + 1) + ^ ".gif" + in Manip.replaceChildren elt - H.[ - div ~a: [ a_id "chamo" ] [ img ~alt: "loading" ~src: chamo_src () ] ; - div ~a: [ a_class [ "messages" ] ] contents - ] + H. + [ div ~a:[a_id "chamo"] [img ~alt:"loading" ~src:chamo_src ()] + ; div ~a:[a_class ["messages"]] contents ] in let hide () = let elt = find_div_or_append_to_body id in - Manip.(removeClass elt "initial") ; - Manip.(removeClass elt "loading") ; + Manip.(removeClass elt "initial"); + Manip.(removeClass elt "loading"); Manip.(addClass elt "loaded") in - Lwt.finalize - (fun () -> show (); f ()) - (fun () -> hide (); Lwt.return_unit) + Lwt.finalize (fun () -> show (); f ()) (fun () -> hide (); Lwt.return_unit) let set_assoc name value = let rec set acc = function | [] -> List.rev ((name, value) :: acc) | (n, _) :: args when n = name -> List.rev_append ((name, value) :: acc) args - | arg :: args -> set (arg :: acc) args in + | arg :: args -> set (arg :: acc) args + in set [] -let delete_assoc name = - List.filter (fun (n, _) -> n <> name) +let delete_assoc name = List.filter (fun (n, _) -> n <> name) let arg, set_arg, delete_arg = let args = ref (Js_utils.parse_fragment ()) in let delete_arg name = - args := delete_assoc name !args ; - Js_utils.set_fragment !args in + args := delete_assoc name !args; + Js_utils.set_fragment !args + in let set_arg name value = - args := set_assoc name value !args ; - Js_utils.set_fragment !args in - let arg name = - List.assoc name !args in - arg, set_arg, delete_arg + args := set_assoc name value !args; + Js_utils.set_fragment !args + in + let arg name = List.assoc name !args in + (arg, set_arg, delete_arg) type button_group = (< disabled : bool Js.t Js.prop > Js.t * bool ref) list ref * Lwt_mutex.t * int ref -let button_group () : button_group = - (ref [], Lwt_mutex.create (), ref 0) +let button_group () : button_group = (ref [], Lwt_mutex.create (), ref 0) type button_state = - bool ref - * (button_group * < disabled : bool Js.t Js.prop > Js.t) option ref + bool ref * (button_group * < disabled : bool Js.t Js.prop > Js.t) option ref -let button_state () : button_state = - (ref false, ref None) +let button_state () : button_state = (ref false, ref None) let disable_button_group (buttons, _, cpt) = - incr cpt ; + incr cpt; if !cpt = 1 then - List.iter - (fun (button, _) -> - button##.disabled := Js.bool true) - !buttons + List.iter (fun (button, _) -> button##.disabled := Js.bool true) !buttons let enable_button_group (buttons, _, cpt) = - decr cpt ; + decr cpt; if !cpt = 0 then List.iter (fun (button, state) -> - if not !state then - button##.disabled := Js.bool false) + if not !state then button##.disabled := Js.bool false ) !buttons let disable_button (disabled, self) = match !self with - | None -> - disabled := true + | None -> disabled := true | Some (_, button) -> - disabled := true ; + disabled := true; button##.disabled := Js.bool true let enable_button (disabled, self) = match !self with - | None -> - disabled := false + | None -> disabled := false | Some ((_, _, cpt), button) -> - disabled := false ; - if !cpt = 0 then - button##.disabled := Js.bool false + disabled := false; + if !cpt = 0 then button##.disabled := Js.bool false -let button_group_disabled (_, _, cpt) = - !cpt > 0 +let button_group_disabled (_, _, cpt) = !cpt > 0 let disabling_button_group group cb = - disable_button_group group ; - Lwt_js.yield () >>= fun () -> - Lwt.catch cb - (function - | Lwt.Canceled -> Lwt.return () - | exn -> Lwt.fail exn) >>= fun res -> - enable_button_group group ; - Lwt_js.yield () >>= fun () -> - Lwt.return res + disable_button_group group; + Lwt_js.yield () + >>= fun () -> + Lwt.catch cb (function Lwt.Canceled -> Lwt.return () | exn -> Lwt.fail exn) + >>= fun res -> + enable_button_group group; + Lwt_js.yield () >>= fun () -> Lwt.return res let disable_with_button_group component (buttons, _, _) = buttons := @@ -282,106 +278,91 @@ let disable_with_button_group component (buttons, _, _) = :: !buttons let button ~container ~theme ?group ?state ~icon lbl cb = - let (others, mutex, cnt) as group = - match group with - | None -> button_group () - | Some group -> group in + let ((others, mutex, cnt) as group) = + match group with None -> button_group () | Some group -> group + in let button = - H.(button [ - img ~alt:"" ~src:(api_server ^ "/icons/icon_" ^ icon ^ "_" ^ theme ^ ".svg") () ; - txt " " ; - span ~a:[ a_class [ "label" ] ] [ txt lbl ] - ]) in - Manip.Ev.onclick button - (fun _ -> - begin Lwt.async @@ fun () -> - Lwt_mutex.with_lock mutex @@ fun () -> - disabling_button_group group cb - end ; - true) ; + H.( + button + [ img ~alt:"" + ~src:(api_server ^ "/icons/icon_" ^ icon ^ "_" ^ theme ^ ".svg") + () + ; txt " " + ; span ~a:[a_class ["label"]] [txt lbl] ]) + in + Manip.Ev.onclick button (fun _ -> + ( Lwt.async + @@ fun () -> + Lwt_mutex.with_lock mutex @@ fun () -> disabling_button_group group cb ); + true ); let dom_button = - (Tyxml_js.To_dom.of_button button - :> < disabled : bool Js.t Js.prop > Js.t) in + (Tyxml_js.To_dom.of_button button :> < disabled : bool Js.t Js.prop > Js.t) + in let self_disabled = match state with | None -> ref false | Some (disabled, self) -> - self := Some (group, dom_button) ; - disabled in - others := (dom_button, self_disabled) :: !others ; - if !self_disabled || !cnt > 0 then - dom_button##.disabled := Js.bool true ; + self := Some (group, dom_button); + disabled + in + others := (dom_button, self_disabled) :: !others; + if !self_disabled || !cnt > 0 then dom_button##.disabled := Js.bool true; Manip.appendChild container button let dropdown ~id ~title items = - let toggle _ = - let menu = find_component id in - let disp = - match Manip.Css.display menu with - | "block" -> "none" - | _ -> - Lwt_js_events.async (fun () -> - Lwt_js_events.click window >|= fun _ -> - Manip.SetCss.display menu "none" - ); - "block" - in - Manip.SetCss.display menu disp; - false + let toggle _ = + let menu = find_component id in + let disp = + match Manip.Css.display menu with + | "block" -> "none" + | _ -> + Lwt_js_events.async (fun () -> + Lwt_js_events.click window + >|= fun _ -> Manip.SetCss.display menu "none" ); + "block" in - H.div ~a: [H.a_class ["dropdown_btn"]] [ - H.button ~a: [H.a_onclick toggle] - (title @ [H.txt " \xe2\x96\xb4" (* U+25B4 *)]); - H.div ~a: [H.a_id id; H.a_class ["dropdown_content"]] items - ] + Manip.SetCss.display menu disp; + false + in + H.div + ~a:[H.a_class ["dropdown_btn"]] + [ H.button ~a:[H.a_onclick toggle] + (title @ [H.txt " \xe2\x96\xb4" (* U+25B4 *)]) + ; H.div ~a:[H.a_id id; H.a_class ["dropdown_content"]] items ] -let gettimeofday () = - (new%js Js.date_now)##getTime /. 1000. +let gettimeofday () = (new%js Js.date_now)##getTime /. 1000. let render_rich_text ?on_runnable_clicked text = let open Learnocaml_data.Tutorial in let rec render acc text = match text with | [] -> List.rev acc - | Text text :: rest -> - render - (H.txt text :: acc) - rest - | Code { code ; runnable } :: rest -> - let elt = H.code [ H.txt code ] in - (match runnable, on_runnable_clicked with - | true, Some cb -> - Manip.addClass elt "runnable" ; - Manip.Ev.onclick elt (fun _ -> cb code ; true) - | _ -> ()) ; - render (elt :: acc) rest ; - | Emph text :: rest -> - render - (H.em (render [] text) :: acc) - rest + | Text text :: rest -> render (H.txt text :: acc) rest + | Code {code; runnable} :: rest -> + let elt = H.code [H.txt code] in + ( match (runnable, on_runnable_clicked) with + | true, Some cb -> + Manip.addClass elt "runnable"; + Manip.Ev.onclick elt (fun _ -> cb code; true) + | _ -> () ); + render (elt :: acc) rest + | Emph text :: rest -> render (H.em (render [] text) :: acc) rest | Image _ :: _ -> assert false - | Math code :: rest -> - render - (H.txt ("`" ^ code ^ "`") :: acc) - rest in - (render [] text - :> [< Html_types.phrasing > `Code `Em `PCDATA ] H.elt list) + | Math code :: rest -> render (H.txt ("`" ^ code ^ "`") :: acc) rest + in + (render [] text :> [< Html_types.phrasing > `Code `Em `PCDATA] H.elt list) let extract_text_from_rich_text text = let open Learnocaml_data.Tutorial in let rec render acc text = match text with | [] -> String.concat " " (List.rev acc) - | Text text :: rest -> - render (text :: acc) rest - | Code { code ; _ } :: rest -> - render (("[" ^ code ^ "]") :: acc) rest - | Emph text :: rest -> - render (("*" ^ render [] text ^ "*") :: acc) rest - | Image { alt ; _ } :: rest -> - render (("(" ^ alt ^ ")") :: acc) rest - | Math code :: rest -> - render (("$" ^ code ^ "$") :: acc) rest in + | Text text :: rest -> render (text :: acc) rest + | Code {code; _} :: rest -> render (("[" ^ code ^ "]") :: acc) rest + | Emph text :: rest -> render (("*" ^ render [] text ^ "*") :: acc) rest + | Image {alt; _} :: rest -> render (("(" ^ alt ^ ")") :: acc) rest + | Math code :: rest -> render (("$" ^ code ^ "$") :: acc) rest + in render [] text let set_state_from_save_file ?token save = @@ -390,68 +371,69 @@ let set_state_from_save_file ?token save = (match token with None -> () | Some t -> store sync_token t); store nickname save.nickname; store all_exercise_states - (SMap.merge (fun _ ans edi -> - match ans, edi with + (SMap.merge + (fun _ ans edi -> + match (ans, edi) with | Some ans, Some (mtime, solution) -> Some {ans with Answer.solution; mtime} | None, Some (mtime, solution) -> Some Answer.{grade = None; report = None; solution; mtime} - | ans, _ -> ans) - save.all_exercise_states save.all_exercise_editors); + | ans, _ -> ans ) + save.all_exercise_states save.all_exercise_editors); store all_toplevel_histories save.all_toplevel_histories; store all_exercise_toplevel_histories save.all_exercise_toplevel_histories let rec retrieve ?ignore req = - Server_caller.request req >>= function + Server_caller.request req + >>= function | Ok x -> Lwt.return x | Error e -> - lwt_alert ~title:[%i"REQUEST ERROR"] [ - H.p [H.txt [%i"Could not retrieve data from server"]]; - H.code [H.txt (Server_caller.string_of_error e)]; - ] ~buttons:( - ([%i"Retry"], (fun () -> retrieve req)) :: - (match ignore with - | None -> [] - | Some v -> [[%i"Ignore"], fun () -> Lwt.return v]) @ - [[%i"Cancel"], (fun () -> Lwt.fail Lwt.Canceled)] - ) + lwt_alert ~title:[%i "REQUEST ERROR"] + [ H.p [H.txt [%i "Could not retrieve data from server"]] + ; H.code [H.txt (Server_caller.string_of_error e)] ] + ~buttons: + ( ([%i "Retry"], fun () -> retrieve req) + :: + ( match ignore with + | None -> [] + | Some v -> [([%i "Ignore"], fun () -> Lwt.return v)] ) + @ [([%i "Cancel"], fun () -> Lwt.fail Lwt.Canceled)] ) let get_state_as_save_file ?(include_reports = false) () = let open Learnocaml_data.Save in let open Learnocaml_local_storage in let answers = retrieve all_exercise_states in - { - nickname = retrieve nickname; - all_exercise_editors = - if include_reports then SMap.empty - else SMap.map (fun a -> a.Answer.mtime, a.Answer.solution) answers; - all_exercise_states = - if include_reports then answers - else SMap.empty; - all_toplevel_histories = retrieve all_toplevel_histories; - all_exercise_toplevel_histories = retrieve all_exercise_toplevel_histories; + { nickname = retrieve nickname + ; all_exercise_editors = + ( if include_reports then SMap.empty + else SMap.map (fun a -> (a.Answer.mtime, a.Answer.solution)) answers ) + ; all_exercise_states = (if include_reports then answers else SMap.empty) + ; all_toplevel_histories = retrieve all_toplevel_histories + ; all_exercise_toplevel_histories = retrieve all_exercise_toplevel_histories } let rec sync_save token save_file = Server_caller.request (Learnocaml_api.Update_save (token, save_file)) >>= function - | Ok save -> set_state_from_save_file ~token save; Lwt.return save + | Ok save -> + set_state_from_save_file ~token save; + Lwt.return save | Error (`Not_found _) -> Server_caller.request_exn - (Learnocaml_api.Create_token ("", Some token, None)) >>= fun _token -> + (Learnocaml_api.Create_token ("", Some token, None)) + >>= fun _token -> assert (_token = token); - Server_caller.request_exn - (Learnocaml_api.Update_save (token, save_file)) >>= fun save -> + Server_caller.request_exn (Learnocaml_api.Update_save (token, save_file)) + >>= fun save -> set_state_from_save_file ~token save; Lwt.return save | Error e -> - lwt_alert ~title:[%i"SYNC FAILED"] [ - H.p [H.txt [%i"Could not synchronise save with the server"]]; - H.code [H.txt (Server_caller.string_of_error e)]; - ] ~buttons:[ - [%i"Retry"], (fun () -> sync_save token save_file); - [%i"Ignore"], (fun () -> Lwt.return save_file); - ] + lwt_alert ~title:[%i "SYNC FAILED"] + [ H.p [H.txt [%i "Could not synchronise save with the server"]] + ; H.code [H.txt (Server_caller.string_of_error e)] ] + ~buttons: + [ ([%i "Retry"], fun () -> sync_save token save_file) + ; ([%i "Ignore"], fun () -> Lwt.return save_file) ] let sync token = sync_save token (get_state_as_save_file ()) @@ -459,19 +441,18 @@ let sync_exercise token ?answer ?editor id = let handle_serverless () = (* save the text at least locally (but not the report & grade, that could be misleading) *) - let txt = match editor, answer with + let txt = + match (editor, answer) with | Some t, _ -> Some t | _, Some a -> Some a.Answer.solution | _ -> None in match txt with | Some txt -> - let key = Learnocaml_local_storage.exercise_state id in - let a0 = Learnocaml_local_storage.retrieve key in - Learnocaml_local_storage.store key - {a0 with Answer. - solution = txt; - mtime = gettimeofday () } + let key = Learnocaml_local_storage.exercise_state id in + let a0 = Learnocaml_local_storage.retrieve key in + Learnocaml_local_storage.store key + {a0 with Answer.solution = txt; mtime = gettimeofday ()} | None -> () in let nickname = Learnocaml_local_storage.(retrieve nickname) in @@ -483,31 +464,32 @@ let sync_exercise token ?answer ?editor id = | Some i -> SMap.singleton id i | None -> SMap.empty in - let save_file = Save.{ - nickname; - all_exercise_editors = opt_to_map txt; - all_exercise_states = opt_to_map answer; - all_toplevel_histories = SMap.empty; - all_exercise_toplevel_histories = opt_to_map toplevel_history; - } in + let save_file = + Save. + { nickname + ; all_exercise_editors = opt_to_map txt + ; all_exercise_states = opt_to_map answer + ; all_toplevel_histories = SMap.empty + ; all_exercise_toplevel_histories = opt_to_map toplevel_history } + in match token with | Some token -> - Lwt.catch (fun () -> sync_save token save_file) - (fun e -> - handle_serverless (); - raise e) - | None -> set_state_from_save_file save_file; - handle_serverless (); - Lwt.return save_file + Lwt.catch + (fun () -> sync_save token save_file) + (fun e -> handle_serverless (); raise e) + | None -> + set_state_from_save_file save_file; + handle_serverless (); + Lwt.return save_file let string_of_seconds seconds = let days = seconds / 24 / 60 / 60 in let hours = seconds / 60 / 60 mod 24 in let minutes = seconds / 60 mod 60 in let seconds = seconds mod 60 in - if days >= 1 then Printf.sprintf [%if"%dd %02dh"] days hours else - if hours >= 1 then Printf.sprintf [%if"%02d:%02d"] hours minutes else - Printf.sprintf [%if"0:%02d:%02d"] minutes seconds + if days >= 1 then Printf.sprintf [%if "%dd %02dh"] days hours + else if hours >= 1 then Printf.sprintf [%if "%02d:%02d"] hours minutes + else Printf.sprintf [%if "0:%02d:%02d"] minutes seconds let countdown ?(ontimeout = fun () -> ()) container t = let deadline = gettimeofday () +. t in @@ -521,27 +503,27 @@ let countdown ?(ontimeout = fun () -> ()) container t = in let rec callback () = let remaining = int_of_float (deadline -. gettimeofday ()) in - if remaining <= 0 then - (update 0; - ontimeout ()) - else - (update remaining; - ignore (window##setTimeout - (Js.wrap_callback callback) - (update_interval remaining))) + if remaining <= 0 then ( update 0; ontimeout () ) + else ( + update remaining; + ignore + (window##setTimeout + (Js.wrap_callback callback) + (update_interval remaining)) ) in callback () -let flog fmt = Printf.ksprintf (fun s -> Firebug.console##log(Js.string s)) fmt +let flog fmt = + Printf.ksprintf (fun s -> Firebug.console##log (Js.string s)) fmt let stars_div stars = - H.div ~a:[ H.a_class [ "stars" ] ] [ - let num = 5 * int_of_float (stars *. 2.) in - let num = max (min num 40) 0 in - let alt = Format.asprintf [%if"difficulty: %d / 40"] num in - let src = Format.asprintf "%s/icons/stars_%02d.svg" api_server num in - H.img ~alt ~src () - ] + H.div + ~a:[H.a_class ["stars"]] + [ (let num = 5 * int_of_float (stars *. 2.) in + let num = max (min num 40) 0 in + let alt = Format.asprintf [%if "difficulty: %d / 40"] num in + let src = Format.asprintf "%s/icons/stars_%02d.svg" api_server num in + H.img ~alt ~src ()) ] let exercise_text ex_meta exo = let mathjax_url = @@ -555,8 +537,8 @@ let exercise_text ex_meta exo = \ showMathMenuMSIE: false,\n\ \ \"HTML-CSS\": {\n\ \ imageFont: null\n\ - \ } - });" + \ }\n\ + \ });" (* the following would allow comma instead of dot for the decimal separator, but should depend on the language the exercise is in, not the language of the app @@ -568,37 +550,25 @@ let exercise_text ex_meta exo = (* Looking for the description in the correct language. *) let descr = let lang = "" in - try - List.assoc lang (Learnocaml_exercise.(access File.descr exo)) - with - Not_found -> - try List.assoc "" (Learnocaml_exercise.(access File.descr exo)) - with Not_found -> [%i "No description available for this exercise." ] + try List.assoc lang Learnocaml_exercise.(access File.descr exo) + with Not_found -> ( + try List.assoc "" Learnocaml_exercise.(access File.descr exo) + with Not_found -> [%i "No description available for this exercise."] ) in Format.asprintf - "\ - \ - %s - exercise text\ - \ - \ - - \ - \ - \ - %s\ - \ - \ - " - ex_meta.Exercise.Meta.title - api_server - mathjax_config - mathjax_url - descr + "%s - exercise text\n\ + \ %s" + ex_meta.Exercise.Meta.title api_server mathjax_config mathjax_url descr let string_of_exercise_kind = function - | Exercise.Meta.Project -> [%i"project"] - | Exercise.Meta.Problem -> [%i"problem"] - | Exercise.Meta.Exercise -> [%i"exercise"] + | Exercise.Meta.Project -> [%i "project"] + | Exercise.Meta.Problem -> [%i "problem"] + | Exercise.Meta.Exercise -> [%i "exercise"] let grade_color = function | None -> "#808080" @@ -608,90 +578,93 @@ let grade_color = function let get_assignments tokens exos_status = let module ES = Exercise.Status in - let module ATM = Map.Make(struct - type t = (float * float) * Token.Set.t * bool - let compare (d1, ts1, dft1) (d2, ts2, dft2) = - match compare d1 d2 with - | 0 -> (match Token.Set.compare ts1 ts2 with - | 0 -> compare dft1 dft2 - | n -> n) - | n -> n - end) - in + let module ATM = Map.Make (struct + type t = (float * float) * Token.Set.t * bool + + let compare (d1, ts1, dft1) (d2, ts2, dft2) = + match compare d1 d2 with + | 0 -> ( + match Token.Set.compare ts1 ts2 with 0 -> compare dft1 dft2 | n -> n ) + | n -> n + end) in let atm_add atm key id = match ATM.find_opt key atm with | None -> ATM.add key (SSet.singleton id) atm | Some set -> ATM.add key (SSet.add id set) atm in let atm = - SMap.fold (fun id st atm -> + SMap.fold + (fun id st atm -> let assg = st.ES.assignments in let default = ES.default_assignment assg in let stl = ES.by_status tokens assg in - let atm = match default with + let atm = + match default with | ES.Assigned {start; stop} -> let explicit_tokens = - Token.Map.fold (fun tok _ -> Token.Set.add tok) + Token.Map.fold + (fun tok _ -> Token.Set.add tok) assg.ES.token_map Token.Set.empty in - let implicit_tokens = - Token.Set.diff tokens explicit_tokens - in + let implicit_tokens = Token.Set.diff tokens explicit_tokens in atm_add atm ((start, stop), implicit_tokens, true) id | _ -> atm in - List.fold_left (fun atm (status, tokens) -> + List.fold_left + (fun atm (status, tokens) -> match status with | ES.Open | ES.Closed -> atm - | ES.Assigned {start; stop} -> - let key = (start, stop), tokens, (status = default) in + | ES.Assigned {start; stop} -> ( + let key = ((start, stop), tokens, status = default) in match ATM.find_opt key atm with - | None -> - ATM.add key (SSet.singleton id) atm - | Some ids -> - ATM.add key (SSet.add id ids) atm) - atm - stl) - exos_status - ATM.empty + | None -> ATM.add key (SSet.singleton id) atm + | Some ids -> ATM.add key (SSet.add id ids) atm ) ) + atm stl ) + exos_status ATM.empty in - ATM.fold (fun (assg, tokens, dft) exos l -> - (assg, tokens, dft, exos) :: l) + ATM.fold + (fun (assg, tokens, dft) exos l -> (assg, tokens, dft, exos) :: l) atm [] |> List.rev -let string_of_date ?(time=false) t = +let string_of_date ?(time = false) t = let date = new%js Js.date_fromTimeValue (t *. 1000.) in if time then - Printf.sprintf "%04d-%02d-%02d %02d:%02d" - date##getFullYear (date##getMonth + 1) date##getDate - date##getHours date##getMinutes + Printf.sprintf "%04d-%02d-%02d %02d:%02d" date##getFullYear + (date##getMonth + 1) + date##getDate date##getHours date##getMinutes else - Printf.sprintf "%04d-%02d-%02d" - date##getFullYear (date##getMonth + 1) date##getDate + Printf.sprintf "%04d-%02d-%02d" date##getFullYear + (date##getMonth + 1) + date##getDate -let date ?(time=false) t = +let date ?(time = false) t = let date = new%js Js.date_fromTimeValue (t *. 1000.) in - H.time ~a:[ H.a_datetime (Js.to_string date##toISOString) ] [ - H.txt - (Js.to_string (if time then date##toLocaleString - else date##toLocaleDateString)) - ] + H.time + ~a:[H.a_datetime (Js.to_string date##toISOString)] + [ H.txt + (Js.to_string + (if time then date##toLocaleString else date##toLocaleDateString)) + ] let tag_span tag = let color = - Printf.sprintf "#%06x" ((Hashtbl.hash tag lor 0x808080) land 0xffffff) + Printf.sprintf "#%06x" (Hashtbl.hash tag lor 0x808080 land 0xffffff) in - H.span ~a:[H.a_class ["tag"]; - H.a_style ("background-color: "^color)] + H.span + ~a:[H.a_class ["tag"]; H.a_style ("background-color: " ^ color)] [H.txt tag] let get_worker_code name = let worker_url = ref None in - fun () -> match !worker_url with + fun () -> + match !worker_url with | None -> - retrieve (Learnocaml_api.Static ["js"; name]) >|= fun js -> - let url = js_code_url js in worker_url := Some url; url + retrieve (Learnocaml_api.Static ["js"; name]) + >|= fun js -> + let url = js_code_url js in + worker_url := Some url; + url | Some url -> Lwt.return url let mouseover_toggle_signal elt sigvalue setter = @@ -699,101 +672,99 @@ let mouseover_toggle_signal elt sigvalue setter = Manip.Ev.onmouseout elt (fun _ -> setter None; Manip.Ev.onmouseover elt hdl; - true - ); + true ); setter (Some sigvalue); true in Manip.Ev.onmouseover elt hdl let ace_display tab = - let ace = lazy ( - let answer = - Ocaml_mode.create_ocaml_editor - (Tyxml_js.To_dom.of_div tab) - in - let ace = Ocaml_mode.get_editor answer in - Ace.set_font_size ace 16; - Ace.set_readonly ace true; - ace - ) in - (fun ans -> - Ace.set_contents (Lazy.force ace) ~reset_undo:true ans), - (fun () -> - Ace.set_contents (Lazy.force ace) ~reset_undo:true "") - -let toplevel_launch ?display_welcome ?after_init ?(on_disable=fun () -> ()) ?(on_enable=fun () -> ()) - container history on_show toplevel_buttons_group id = - let timeout_prompt = - Learnocaml_toplevel.make_timeout_popup ~on_show () in - let flood_prompt = - Learnocaml_toplevel.make_flood_popup ~on_show () in + let ace = + lazy + (let answer = + Ocaml_mode.create_ocaml_editor (Tyxml_js.To_dom.of_div tab) + in + let ace = Ocaml_mode.get_editor answer in + Ace.set_font_size ace 16; Ace.set_readonly ace true; ace) + in + ( (fun ans -> Ace.set_contents (Lazy.force ace) ~reset_undo:true ans) + , fun () -> Ace.set_contents (Lazy.force ace) ~reset_undo:true "" ) + +let toplevel_launch ?display_welcome ?after_init ?(on_disable = fun () -> ()) + ?(on_enable = fun () -> ()) container history on_show + toplevel_buttons_group id = + let timeout_prompt = Learnocaml_toplevel.make_timeout_popup ~on_show () in + let flood_prompt = Learnocaml_toplevel.make_flood_popup ~on_show () in let history = let storage_key = history id in let on_update self = Learnocaml_local_storage.store storage_key - (Learnocaml_toplevel_history.snapshot self) in - let snapshot = - Learnocaml_local_storage.retrieve storage_key in - Learnocaml_toplevel_history.create - ~gettimeofday - ~on_update - ~max_size: 99 - ~snapshot () in - get_worker_code "learnocaml-toplevel-worker.js" () >>= fun worker_js_file -> - Learnocaml_toplevel.create ~worker_js_file - ?display_welcome ?after_init ~timeout_prompt ~flood_prompt - ~on_disable_input: (fun _ -> on_disable (); disable_button_group toplevel_buttons_group) - ~on_enable_input: (fun _ -> on_enable (); enable_button_group toplevel_buttons_group) - ~container - ~history () - -let init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button = - begin toplevel_button - ~icon: "cleanup" [%i"Clear"] @@ fun () -> - Learnocaml_toplevel.clear top ; - Lwt.return () - end ; - begin toplevel_button - ~icon: "reload" [%i"Reset"] @@ fun () -> - toplevel_launch >>= fun top -> - disabling_button_group toplevel_buttons_group (fun () -> Learnocaml_toplevel.reset top) - end ; - begin toplevel_button - ~icon: "run" [%i"Eval phrase"] @@ fun () -> - Learnocaml_toplevel.execute top ; - Lwt.return () - end + (Learnocaml_toplevel_history.snapshot self) + in + let snapshot = Learnocaml_local_storage.retrieve storage_key in + Learnocaml_toplevel_history.create ~gettimeofday ~on_update ~max_size:99 + ~snapshot () + in + get_worker_code "learnocaml-toplevel-worker.js" () + >>= fun worker_js_file -> + Learnocaml_toplevel.create ~worker_js_file ?display_welcome ?after_init + ~timeout_prompt ~flood_prompt + ~on_disable_input:(fun _ -> + on_disable (); + disable_button_group toplevel_buttons_group ) + ~on_enable_input:(fun _ -> + on_enable (); + enable_button_group toplevel_buttons_group ) + ~container ~history () + +let init_toplevel_pane toplevel_launch top toplevel_buttons_group + toplevel_button = + ( toplevel_button ~icon:"cleanup" [%i "Clear"] + @@ fun () -> + Learnocaml_toplevel.clear top; + Lwt.return () ); + ( toplevel_button ~icon:"reload" [%i "Reset"] + @@ fun () -> + toplevel_launch + >>= fun top -> + disabling_button_group toplevel_buttons_group (fun () -> + Learnocaml_toplevel.reset top ) ); + toplevel_button ~icon:"run" [%i "Eval phrase"] + @@ fun () -> + Learnocaml_toplevel.execute top; + Lwt.return () let set_inner_list lst = let aux (id, text) = match Js_utils.Manip.by_id id with | None -> () - | Some component -> - Manip.setInnerHtml component text in + | Some component -> Manip.setInnerHtml component text + in List.iter aux lst let set_string_translations_exercises () = - let translations = [ - "txt_preparing", [%i"Preparing the environment"]; - "learnocaml-exo-button-editor", [%i"Editor"]; - "learnocaml-exo-button-toplevel", [%i"Toplevel"]; - "learnocaml-exo-button-report", [%i"Report"]; - "learnocaml-exo-button-text", [%i"Exercise"]; - "learnocaml-exo-button-meta", [%i"Details"]; - "learnocaml-exo-editor-pane", [%i"Editor"]; - "txt_grade_report", [%i"Click the Grade button to get your report"]; - ] in set_inner_list translations + let translations = + [ ("txt_preparing", [%i "Preparing the environment"]) + ; ("learnocaml-exo-button-editor", [%i "Editor"]) + ; ("learnocaml-exo-button-toplevel", [%i "Toplevel"]) + ; ("learnocaml-exo-button-report", [%i "Report"]) + ; ("learnocaml-exo-button-text", [%i "Exercise"]) + ; ("learnocaml-exo-button-meta", [%i "Details"]) + ; ("learnocaml-exo-editor-pane", [%i "Editor"]) + ; ("txt_grade_report", [%i "Click the Grade button to get your report"]) ] + in + set_inner_list translations let set_string_translations_view () = - let translations = [ - "txt_loading", [%i"Loading student data"]; - "learnocaml-exo-button-stats", [%i"Stats"]; - "learnocaml-exo-button-list", [%i"Exercises"]; - "learnocaml-exo-button-report", [%i"Report"]; - "learnocaml-exo-button-text", [%i"Subject"]; - "learnocaml-exo-button-editor", [%i"Answer"]; - ] in set_inner_list translations + let translations = + [ ("txt_loading", [%i "Loading student data"]) + ; ("learnocaml-exo-button-stats", [%i "Stats"]) + ; ("learnocaml-exo-button-list", [%i "Exercises"]) + ; ("learnocaml-exo-button-report", [%i "Report"]) + ; ("learnocaml-exo-button-text", [%i "Subject"]) + ; ("learnocaml-exo-button-editor", [%i "Answer"]) ] + in + set_inner_list translations let local_save ace id = let key = Learnocaml_local_storage.exercise_state id in @@ -802,138 +773,144 @@ let local_save ace id = Answer.{solution = ""; mtime = 0.; report = None; grade = None} in Learnocaml_local_storage.store key - { ans with Answer.solution = Ace.get_contents ace; - mtime = gettimeofday () } + {ans with Answer.solution = Ace.get_contents ace; mtime = gettimeofday ()} let run_async_with_log f = - Lwt.async_exception_hook := begin fun e -> - Firebug.console##log (Js.string - (Printexc.to_string e ^ - if Printexc.backtrace_status () then - Printexc.get_backtrace () - else "")); - match e with - | Failure message -> fatal message - | Server_caller.Cannot_fetch message -> fatal message - | exn -> fatal (Printexc.to_string exn) - end ; - (match Js_utils.get_lang() with Some l -> Ocplib_i18n.set_lang l | None -> ()); + (Lwt.async_exception_hook := + fun e -> + Firebug.console##log + (Js.string + ( Printexc.to_string e + ^ + if Printexc.backtrace_status () then Printexc.get_backtrace () + else "" )); + match e with + | Failure message -> fatal message + | Server_caller.Cannot_fetch message -> fatal message + | exn -> fatal (Printexc.to_string exn)); + ( match Js_utils.get_lang () with + | Some l -> Ocplib_i18n.set_lang l + | None -> () ); Lwt.async f let mk_tab_handlers default_tab other_tabs = - let names = default_tab::other_tabs in + let names = default_tab :: other_tabs in let current = ref default_tab in let select_tab name = - set_arg "tab" name ; + set_arg "tab" name; Manip.removeClass (find_component ("learnocaml-exo-button-" ^ !current)) - "front-tab" ; + "front-tab"; Manip.removeClass (find_component ("learnocaml-exo-tab-" ^ !current)) - "front-tab" ; - Manip.enable - (find_component ("learnocaml-exo-button-" ^ !current)) ; + "front-tab"; + Manip.enable (find_component ("learnocaml-exo-button-" ^ !current)); Manip.addClass (find_component ("learnocaml-exo-button-" ^ name)) - "front-tab" ; - Manip.addClass - (find_component ("learnocaml-exo-tab-" ^ name)) - "front-tab" ; - Manip.disable - (find_component ("learnocaml-exo-button-" ^ name)) ; - current := name in + "front-tab"; + Manip.addClass (find_component ("learnocaml-exo-tab-" ^ name)) "front-tab"; + Manip.disable (find_component ("learnocaml-exo-button-" ^ name)); + current := name + in let init_tabs () = - current := - begin - try - let requested = arg "tab" in - if List.mem requested names then requested else default_tab - with Not_found -> default_tab - end ; + (current := + try + let requested = arg "tab" in + if List.mem requested names then requested else default_tab + with Not_found -> default_tab); List.iter (fun name -> Manip.removeClass (find_component ("learnocaml-exo-button-" ^ name)) - "front-tab" ; + "front-tab"; Manip.removeClass (find_component ("learnocaml-exo-tab-" ^ name)) - "front-tab" ; + "front-tab"; Manip.Ev.onclick (find_component ("learnocaml-exo-button-" ^ name)) - (fun _ -> select_tab name ; true)) - names ; - select_tab !current in - init_tabs, select_tab + (fun _ -> select_tab name; true) ) + names; + select_tab !current + in + (init_tabs, select_tab) module type Editor_info = sig val ace : Ocaml_mode.editor Ace.editor + val buttons_container : 'a Tyxml_js.Html5.elt end module Editor_button (E : Editor_info) = struct - let editor_button = button ~container:E.buttons_container ~theme:"light" let cleanup template = - editor_button - ~icon: "cleanup" [%i"Reset"] @@ fun () -> - confirm ~title:[%i"START FROM SCRATCH"] - [H.txt [%i"This will discard all your edits. Are you sure?"]] - (fun () -> - Ace.set_contents E.ace template); + editor_button ~icon:"cleanup" [%i "Reset"] + @@ fun () -> + confirm ~title:[%i "START FROM SCRATCH"] + [H.txt [%i "This will discard all your edits. Are you sure?"]] + (fun () -> Ace.set_contents E.ace template); Lwt.return () let download id = - editor_button - ~icon: "download" [%i"Download"] @@ fun () -> - let name = id ^ ".ml" in - let contents = Js.string (Ace.get_contents E.ace) in - fake_download ~name ~contents ; - Lwt.return () + editor_button ~icon:"download" [%i "Download"] + @@ fun () -> + let name = id ^ ".ml" in + let contents = Js.string (Ace.get_contents E.ace) in + fake_download ~name ~contents; + Lwt.return () let eval top select_tab = - editor_button - ~icon: "run" [%i"Eval code"] @@ fun () -> - Learnocaml_toplevel.execute_phrase top (Ace.get_contents E.ace) >>= fun _ -> - select_tab "toplevel"; - Lwt.return_unit + editor_button ~icon:"run" [%i "Eval code"] + @@ fun () -> + Learnocaml_toplevel.execute_phrase top (Ace.get_contents E.ace) + >>= fun _ -> select_tab "toplevel"; Lwt.return_unit let sync token id = - editor_button - ~icon: "sync" [%i"Sync"] @@ fun () -> - token >>= fun token -> - sync_exercise token id ~editor:(Ace.get_contents E.ace) >|= fun _save -> () + editor_button ~icon:"sync" [%i "Sync"] + @@ fun () -> + token + >>= fun token -> + sync_exercise token id ~editor:(Ace.get_contents E.ace) >|= fun _save -> () end let setup_editor solution = let editor_pane = find_component "learnocaml-exo-editor-pane" in - let editor = Ocaml_mode.create_ocaml_editor (Tyxml_js.To_dom.of_div editor_pane) in + let editor = + Ocaml_mode.create_ocaml_editor (Tyxml_js.To_dom.of_div editor_pane) + in let ace = Ocaml_mode.get_editor editor in Ace.set_contents ace ~reset_undo:true solution; Ace.set_font_size ace 18; - editor, ace + (editor, ace) let typecheck top ace editor set_class = - Learnocaml_toplevel.check top (Ace.get_contents ace) >>= fun res -> + Learnocaml_toplevel.check top (Ace.get_contents ace) + >>= fun res -> let error, warnings = match res with - | Toploop_results.Ok ((), warnings) -> None, warnings - | Toploop_results.Error (err, warnings) -> Some err, warnings in - let transl_loc { Toploop_results.loc_start ; loc_end } = - { Ocaml_mode.loc_start ; loc_end } in - let error = match error with + | Toploop_results.Ok ((), warnings) -> (None, warnings) + | Toploop_results.Error (err, warnings) -> (Some err, warnings) + in + let transl_loc {Toploop_results.loc_start; loc_end} = + {Ocaml_mode.loc_start; loc_end} + in + let error = + match error with | None -> None - | Some { Toploop_results.locs ; msg ; if_highlight } -> - Some { Ocaml_mode.locs = List.map transl_loc locs ; - msg = (if if_highlight <> "" then if_highlight else msg) } in + | Some {Toploop_results.locs; msg; if_highlight} -> + Some + { Ocaml_mode.locs = List.map transl_loc locs + ; msg = (if if_highlight <> "" then if_highlight else msg) } + in let warnings = List.map - (fun { Toploop_results.locs ; msg ; if_highlight } -> - { Ocaml_mode.loc = transl_loc (List.hd locs) ; - msg = (if if_highlight <> "" then if_highlight else msg) }) - warnings in - Ocaml_mode.report_error ~set_class editor error warnings >|= fun () -> - Ace.focus ace + (fun {Toploop_results.locs; msg; if_highlight} -> + { Ocaml_mode.loc = transl_loc (List.hd locs) + ; msg = (if if_highlight <> "" then if_highlight else msg) } ) + warnings + in + Ocaml_mode.report_error ~set_class editor error warnings + >|= fun () -> Ace.focus ace let set_nickname_div () = let nickname_div = find_component "learnocaml-nickname" in @@ -942,291 +919,306 @@ let set_nickname_div () = | exception Not_found -> () let setup_prelude_pane ace prelude = - if prelude = "" then () else - let editor_pane = find_component "learnocaml-exo-editor-pane" in - let prelude_pane = find_component "learnocaml-exo-prelude" in - let open Tyxml_js.Html5 in - let state = - ref (match arg "prelude" with - | exception Not_found -> true - | "shown" -> true - | "hidden" -> false - | _ -> failwith "Bad format for argument prelude.") in - let prelude_btn = button [] in - let prelude_title = h1 [ txt [%i"OCaml prelude"] ; - prelude_btn ] in - let prelude_container = - pre ~a: [ a_class [ "toplevel-code" ] ] - (Learnocaml_toplevel_output.format_ocaml_code prelude) in - let update () = - if !state then begin - Manip.replaceChildren prelude_btn [ txt ("↳ "^[%i"Hide"]) ] ; - Manip.SetCss.display prelude_container "" ; - Manip.SetCss.top editor_pane "193px" ; (* 150 + 43 *) - Manip.SetCss.bottom editor_pane "40px" ; + if prelude = "" then () + else + let editor_pane = find_component "learnocaml-exo-editor-pane" in + let prelude_pane = find_component "learnocaml-exo-prelude" in + let open Tyxml_js.Html5 in + let state = + ref + ( match arg "prelude" with + | exception Not_found -> true + | "shown" -> true + | "hidden" -> false + | _ -> failwith "Bad format for argument prelude." ) + in + let prelude_btn = button [] in + let prelude_title = h1 [txt [%i "OCaml prelude"]; prelude_btn] in + let prelude_container = + pre + ~a:[a_class ["toplevel-code"]] + (Learnocaml_toplevel_output.format_ocaml_code prelude) + in + let update () = + if !state then ( + Manip.replaceChildren prelude_btn [txt ("↳ " ^ [%i "Hide"])]; + Manip.SetCss.display prelude_container ""; + Manip.SetCss.top editor_pane "193px"; + (* 150 + 43 *) + Manip.SetCss.bottom editor_pane "40px"; Ace.resize ace true; - set_arg "prelude" "shown" - end else begin - Manip.replaceChildren prelude_btn [ txt ("↰ "^[%i"Show"]) ] ; - Manip.SetCss.display prelude_container "none" ; - Manip.SetCss.top editor_pane "43px" ; - Manip.SetCss.bottom editor_pane "40px" ; + set_arg "prelude" "shown" ) + else ( + Manip.replaceChildren prelude_btn [txt ("↰ " ^ [%i "Show"])]; + Manip.SetCss.display prelude_container "none"; + Manip.SetCss.top editor_pane "43px"; + Manip.SetCss.bottom editor_pane "40px"; Ace.resize ace true; - set_arg "prelude" "hidden" - end in - update () ; - Manip.Ev.onclick prelude_btn - (fun _ -> state := not !state ; update () ; true) ; - Manip.appendChildren prelude_pane - [ prelude_title ; prelude_container ] - + set_arg "prelude" "hidden" ) + in + update (); + Manip.Ev.onclick prelude_btn (fun _ -> + state := not !state; + update (); + true ); + Manip.appendChildren prelude_pane [prelude_title; prelude_container] + let get_token ?(has_server = true) () = - if not has_server then - Lwt.return None + if not has_server then Lwt.return None else - try - Some Learnocaml_local_storage.(retrieve sync_token) |> - Lwt.return + try Some Learnocaml_local_storage.(retrieve sync_token) |> Lwt.return with Not_found -> retrieve (Learnocaml_api.Nonce ()) >>= fun nonce -> - ask_string ~title:"Secret" - [H.txt [%i"Enter the secret"]] + ask_string ~title:"Secret" [H.txt [%i "Enter the secret"]] >>= fun secret -> retrieve - (Learnocaml_api.Create_token (Sha.sha512 (nonce ^ Sha.sha512 secret), None, None)) + (Learnocaml_api.Create_token + (Sha.sha512 (nonce ^ Sha.sha512 secret), None, None)) >|= fun token -> Learnocaml_local_storage.(store sync_token) token; Some token - -module Display_exercise = - functor ( - Q: sig - val exercise_link: ?cl:string list -> - string -> 'a Tyxml_js.Html.elt list -> [> 'a Html_types.a ] Tyxml_js.Html.elt - end) -> - struct - open Q - let display_descr ex_meta = - let open Tyxml_js.Html5 in - let open Learnocaml_data.Exercise in - match ex_meta.Meta.short_description with - | None -> div ~a:[ a_class [ "descr" ] ] [] - | Some descr -> - div ~a:[ a_class [ "descr" ] ] [ - h2 ~a:[ a_class [ "learnocaml-exo-meta-category" ] ] - [ txt ex_meta.Meta.title ] ; - p [ txt descr ] - ] - - let display_stars ex_meta = - let open Tyxml_js.Html5 in - let open Learnocaml_data.Exercise in - let stars = - let num = 5 * int_of_float (ex_meta.Meta.stars *. 2.) in - let num = max (min num 40) 0 in - let alt = Format.asprintf [%if"difficulty: %d / 40"] num in - let src = Format.asprintf "%s/icons/stars_%02d.svg" api_server num in - img ~alt ~src () - in - div ~a:[ a_class [ "stars" ] ] [ - p [ - txt [%i "Difficulty:"] ; - txt " "; (* Put no whitespace in translation strings + +module Display_exercise (Q : sig + val exercise_link : + ?cl:string list + -> string + -> 'a Tyxml_js.Html.elt list + -> [> 'a Html_types.a] Tyxml_js.Html.elt +end) = +struct + open Q + + let display_descr ex_meta = + let open Tyxml_js.Html5 in + let open Learnocaml_data.Exercise in + match ex_meta.Meta.short_description with + | None -> div ~a:[a_class ["descr"]] [] + | Some descr -> + div + ~a:[a_class ["descr"]] + [ h2 + ~a:[a_class ["learnocaml-exo-meta-category"]] + [txt ex_meta.Meta.title] + ; p [txt descr] ] + + let display_stars ex_meta = + let open Tyxml_js.Html5 in + let open Learnocaml_data.Exercise in + let stars = + let num = 5 * int_of_float (ex_meta.Meta.stars *. 2.) in + let num = max (min num 40) 0 in + let alt = Format.asprintf [%if "difficulty: %d / 40"] num in + let src = Format.asprintf "%s/icons/stars_%02d.svg" api_server num in + img ~alt ~src () + in + div + ~a:[a_class ["stars"]] + [ p + [ txt [%i "Difficulty:"] + ; txt " " + ; (* Put no whitespace in translation strings (the colon is mandatory, though, given the conventions are different in English and French, for example). *) - stars - ] - ] - - let display_kind ex_meta = - let open Tyxml_js.Html5 in - let open Learnocaml_data.Exercise in - let kind_repr = string_of_exercise_kind ex_meta.Meta.kind in - div ~a:[ a_class [ "length" ] ] [ - p [ txt (Format.sprintf [%if "Kind: %s"] kind_repr) ] - ] - - let display_exercise_meta id meta content_id = - let content = find_component content_id in - let descr = - exercise_link ~cl:[ "exercise" ] id [ - display_descr meta ; - H.div ~a:[ ] [ - display_stars meta ; - display_kind meta ; - ] - ] - in - Manip.replaceChildren content [ descr ]; - Lwt.return () - - let display_list ?(sep=Tyxml_js.Html5.txt ", ") l = - let open Tyxml_js.Html5 in - let rec gen acc = function - | [] -> [ txt "" ] - | a :: [] -> a :: acc - | a :: ((_ :: _) as rem) -> - gen (sep :: (a :: acc)) rem - in - gen [] l |> List.rev - - let get_skill_index token = - let index = lazy ( - retrieve (Learnocaml_api.Exercise_index (Some token)) - >|= fun (index, _) -> - Exercise.Index.fold_exercises (fun (req, focus) id meta -> - let add sk id map = - SMap.add sk - (SSet.add id (try SMap.find sk map with Not_found -> SSet.empty)) - map - in - List.fold_left (fun acc sk -> add sk id acc) req - meta.Exercise.Meta.requirements, - List.fold_left (fun acc sk -> add sk id acc) focus - meta.Exercise.Meta.focus - ) (SMap.empty, SMap.empty) index - ) in - fun skill -> - Lazy.force index >|= fun (req, focus) -> - try match skill with - | `Requirements s -> SSet.elements (SMap.find s req) - | `Focus s -> SSet.elements (SMap.find s focus) + stars ] ] + + let display_kind ex_meta = + let open Tyxml_js.Html5 in + let open Learnocaml_data.Exercise in + let kind_repr = string_of_exercise_kind ex_meta.Meta.kind in + div + ~a:[a_class ["length"]] + [p [txt (Format.sprintf [%if "Kind: %s"] kind_repr)]] + + let display_exercise_meta id meta content_id = + let content = find_component content_id in + let descr = + exercise_link ~cl:["exercise"] id + [ display_descr meta + ; H.div ~a:[] [display_stars meta; display_kind meta] ] + in + Manip.replaceChildren content [descr]; + Lwt.return () + + let display_list ?(sep = Tyxml_js.Html5.txt ", ") l = + let open Tyxml_js.Html5 in + let rec gen acc = function + | [] -> [txt ""] + | [a] -> a :: acc + | a :: (_ :: _ as rem) -> gen (sep :: a :: acc) rem + in + gen [] l |> List.rev + + let get_skill_index token = + let index = + lazy + ( retrieve (Learnocaml_api.Exercise_index (Some token)) + >|= fun (index, _) -> + Exercise.Index.fold_exercises + (fun (req, focus) id meta -> + let add sk id map = + SMap.add sk + (SSet.add id + (try SMap.find sk map with Not_found -> SSet.empty)) + map + in + ( List.fold_left + (fun acc sk -> add sk id acc) + req meta.Exercise.Meta.requirements + , List.fold_left + (fun acc sk -> add sk id acc) + focus meta.Exercise.Meta.focus ) ) + (SMap.empty, SMap.empty) index ) + in + fun skill -> + Lazy.force index + >|= fun (req, focus) -> + try + match skill with + | `Requirements s -> SSet.elements (SMap.find s req) + | `Focus s -> SSet.elements (SMap.find s focus) with Not_found -> [] - let display_skill_meta _skill exs content_id = - let content = find_component content_id in - Manip.replaceChildren content - (display_list @@ - List.map (fun ex_id -> - exercise_link ex_id [Tyxml_js.Html5.txt ex_id]) exs); - Lwt.return () - - let display_link onclick content_id value = - let open Tyxml_js.Html5 in - let cid = Format.asprintf "%s-%s" content_id value in - let expand_id = Format.asprintf "%s-expand" cid in - let displayed = ref false in - let onclick _ = - let exp = find_component expand_id in - if !displayed then - Manip.removeChildren (find_component cid) - else - ignore (onclick cid); - Manip.removeChildren exp; - Manip.appendChild exp (txt (if !displayed then "[-]" else "[+]")); - displayed := not !displayed; - true - in - div [ p ~a:[ a_class [ "learnocaml-exo-expandable-link" ] ; - a_onclick onclick ] [ - span ~a:[ a_id expand_id ; - a_class ["expand-sign"] ] [ txt "[+]" ] ; - txt value ] ; - div ~a:[ a_id cid ; - a_class [ "learnocaml-exo-meta-category" ] ] - [] - ] - - let display_skill_link index content_id s = - let skill = match s with `Focus s | `Requirements s -> s in - display_link (display_skill_meta s index) content_id skill - - let display_exercise_link content_id meta e = - display_link (display_exercise_meta e meta) content_id e - - let display_authors caption_text authors = - let open Tyxml_js.Html5 in - let author (name, mail) = - span [ txt name ; - txt " <" ; - a ~a:[ a_href ("mailto:" ^ mail) ] [ txt mail ] ; - txt ">" ; - ] in - span [ txt caption_text; txt " " ] - :: (display_list @@ List.map author authors) - - let add_map_set sk id map = - SMap.add sk - (SSet.add id (try SMap.find sk map with Not_found -> SSet.empty)) - map - - let extract_maps_exo_index index = - Exercise.Index.fold_exercises - (fun (req, focus) id meta -> - (List.fold_left (fun acc sk -> add_map_set sk id acc) req - meta.Exercise.Meta.requirements, - List.fold_left (fun acc sk -> add_map_set sk id acc) focus - meta.Exercise.Meta.focus)) - (SMap.empty, SMap.empty) index - - let opt_display_skills caption map label fskill = function + let display_skill_meta _skill exs content_id = + let content = find_component content_id in + Manip.replaceChildren content + ( display_list + @@ List.map + (fun ex_id -> exercise_link ex_id [Tyxml_js.Html5.txt ex_id]) + exs ); + Lwt.return () + + let display_link onclick content_id value = + let open Tyxml_js.Html5 in + let cid = Format.asprintf "%s-%s" content_id value in + let expand_id = Format.asprintf "%s-expand" cid in + let displayed = ref false in + let onclick _ = + let exp = find_component expand_id in + if !displayed then Manip.removeChildren (find_component cid) + else ignore (onclick cid); + Manip.removeChildren exp; + Manip.appendChild exp (txt (if !displayed then "[-]" else "[+]")); + displayed := not !displayed; + true + in + div + [ p + ~a:[a_class ["learnocaml-exo-expandable-link"]; a_onclick onclick] + [ span ~a:[a_id expand_id; a_class ["expand-sign"]] [txt "[+]"] + ; txt value ] + ; div ~a:[a_id cid; a_class ["learnocaml-exo-meta-category"]] [] ] + + let display_skill_link index content_id s = + let skill = match s with `Focus s | `Requirements s -> s in + display_link (display_skill_meta s index) content_id skill + + let display_exercise_link content_id meta e = + display_link (display_exercise_meta e meta) content_id e + + let display_authors caption_text authors = + let open Tyxml_js.Html5 in + let author (name, mail) = + span + [ txt name + ; txt " <" + ; a ~a:[a_href ("mailto:" ^ mail)] [txt mail] + ; txt ">" ] + in + span [txt caption_text; txt " "] + :: (display_list @@ List.map author authors) + + let add_map_set sk id map = + SMap.add sk + (SSet.add id (try SMap.find sk map with Not_found -> SSet.empty)) + map + + let extract_maps_exo_index index = + Exercise.Index.fold_exercises + (fun (req, focus) id meta -> + ( List.fold_left + (fun acc sk -> add_map_set sk id acc) + req meta.Exercise.Meta.requirements + , List.fold_left + (fun acc sk -> add_map_set sk id acc) + focus meta.Exercise.Meta.focus ) ) + (SMap.empty, SMap.empty) index + + let opt_display_skills caption map label fskill = function + | [] -> None + | skills -> + Some + ( caption + , display_list ~sep:(H.txt "") + @@ List.map + (fun s -> + display_skill_link + ( try SSet.elements (SMap.find s map) with Not_found -> [] + ) + label (fskill s) ) + skills ) + + let opt_display_adjacent_exos index label exos caption = + List.fold_left + (fun acc id -> + match Exercise.Index.find_opt index id with + | Some meta -> display_exercise_link label meta id :: acc + | None -> acc ) + [] (List.rev exos) + |> function + | [] -> None | l -> Some (caption, display_list ~sep:(H.txt "") l) + + let display_meta token ex_meta id = + let open Learnocaml_data.Exercise in + let ident = Format.asprintf "%s %s" [%i "Identifier:"] id in + let authors = + match ex_meta.Meta.author with | [] -> None - | skills -> - Some (caption, - display_list ~sep:(H.txt "") @@ - List.map (fun s -> - display_skill_link - (try SSet.elements (SMap.find s map) with Not_found -> []) - label (fskill s)) - skills) - - let opt_display_adjacent_exos index label exos caption = - List.fold_left (fun acc id -> - match Exercise.Index.find_opt index id with - | Some meta -> - display_exercise_link label meta id :: acc - | None -> acc) - [] - (List.rev exos) - |> function - | [] -> None - | l -> Some (caption, display_list ~sep:(H.txt "") l) - - let display_meta token ex_meta id = - let open Learnocaml_data.Exercise in - let ident = Format.asprintf "%s %s" [%i "Identifier:" ] id in - let authors = - match ex_meta.Meta.author with - | [] -> None - | [author] -> Some (display_authors [%i "Author:"] [author]) - | authors -> Some (display_authors [%i "Authors:"] authors) in - retrieve (Learnocaml_api.Exercise_index token) - >|= fun (index, _) -> - let req_map, focus_map = extract_maps_exo_index index in - let focus = - opt_display_skills [%i "Skills trained:"] focus_map - "learnocaml-exo-focus-meta" (fun s -> `Focus s) - ex_meta.Meta.focus in - let requirements = - opt_display_skills [%i "Skills required:"] req_map - "learnocaml-exo-requirements-meta" (fun s -> `Requirements s) - ex_meta.Meta.requirements in - let backward = - opt_display_adjacent_exos index "learnocaml-exo-backward-meta" - ex_meta.Meta.backward [%i "Previous exercises:"] in - let forward = - opt_display_adjacent_exos index "learnocaml-exo-forward-meta" - ex_meta.Meta.forward [%i "Next exercises:"] in - let tab = find_div_or_append_to_body "learnocaml-exo-tab-meta" in - Manip.replaceChildren tab @@ - Tyxml_js.Html5.([ - h1 ~a:[ a_class [ "learnocaml-exo-meta-title" ] ] - [ txt [%i "Metadata" ] ] ; - div ~a:[ a_id "learnocaml-exo-content-meta" ] @@ - [ display_descr ex_meta ; - display_stars ex_meta ; - display_kind ex_meta ; - p [ txt ident ] ; - (match authors with Some a -> p a | None -> div []) - ] @ List.map - (function - | Some (title, values) -> - div (h2 ~a:[ a_class - [ "learnocaml-exo-meta-category-title" ] ] - [ txt title ] :: values) - | None -> div []) - [ focus ; requirements ; backward ; forward ] - ]) - end + | [author] -> Some (display_authors [%i "Author:"] [author]) + | authors -> Some (display_authors [%i "Authors:"] authors) + in + retrieve (Learnocaml_api.Exercise_index token) + >|= fun (index, _) -> + let req_map, focus_map = extract_maps_exo_index index in + let focus = + opt_display_skills [%i "Skills trained:"] focus_map + "learnocaml-exo-focus-meta" + (fun s -> `Focus s) + ex_meta.Meta.focus + in + let requirements = + opt_display_skills [%i "Skills required:"] req_map + "learnocaml-exo-requirements-meta" + (fun s -> `Requirements s) + ex_meta.Meta.requirements + in + let backward = + opt_display_adjacent_exos index "learnocaml-exo-backward-meta" + ex_meta.Meta.backward [%i "Previous exercises:"] + in + let forward = + opt_display_adjacent_exos index "learnocaml-exo-forward-meta" + ex_meta.Meta.forward [%i "Next exercises:"] + in + let tab = find_div_or_append_to_body "learnocaml-exo-tab-meta" in + Manip.replaceChildren tab + @@ Tyxml_js.Html5. + [ h1 ~a:[a_class ["learnocaml-exo-meta-title"]] [txt [%i "Metadata"]] + ; div ~a:[a_id "learnocaml-exo-content-meta"] + @@ [ display_descr ex_meta + ; display_stars ex_meta + ; display_kind ex_meta + ; p [txt ident] + ; (match authors with Some a -> p a | None -> div []) ] + @ List.map + (function + | Some (title, values) -> + div + ( h2 + ~a:[a_class ["learnocaml-exo-meta-category-title"]] + [txt title] + :: values ) + | None -> div []) + [focus; requirements; backward; forward] ] +end diff --git a/src/app/learnocaml_common.mli b/src/app/learnocaml_common.mli index 275f2c8cf..f2c75a209 100644 --- a/src/app/learnocaml_common.mli +++ b/src/app/learnocaml_common.mli @@ -9,51 +9,60 @@ open Js_of_ocaml open Learnocaml_data -val find_div_or_append_to_body : string -> [> Html_types.div ] Tyxml_js.Html.elt +val find_div_or_append_to_body : string -> [> Html_types.div] Tyxml_js.Html.elt val find_component : string -> 'a Tyxml_js.Html.elt val gettimeofday : unit -> float -val fake_download : name: string -> contents: Js.js_string Js.t -> unit +val fake_download : name:string -> contents:Js.js_string Js.t -> unit -val fake_upload : unit -> (string * Js.js_string Js.t ) Lwt.t +val fake_upload : unit -> (string * Js.js_string Js.t) Lwt.t -val fatal : ?title: string -> string -> unit +val fatal : ?title:string -> string -> unit -val alert : ?title: string -> ?buttons: Html_types.div_content Tyxml_js.Html.elt list -> string -> unit +val alert : + ?title:string + -> ?buttons:Html_types.div_content Tyxml_js.Html.elt list + -> string + -> unit val ext_alert : - title: string -> - ?buttons: Html_types.div_content_fun Tyxml_js.Html.elt list -> - [< Html_types.div_content ] Tyxml_js.Html.elt list -> - unit + title:string + -> ?buttons:Html_types.div_content_fun Tyxml_js.Html.elt list + -> [< Html_types.div_content] Tyxml_js.Html.elt list + -> unit val lwt_alert : - title: string -> - buttons: (string * (unit -> 'a Lwt.t)) list -> - [< Html_types.div_content ] Tyxml_js.Html.elt list -> - 'a Lwt.t + title:string + -> buttons:(string * (unit -> 'a Lwt.t)) list + -> [< Html_types.div_content] Tyxml_js.Html.elt list + -> 'a Lwt.t val confirm : - title: string -> - ?ok_label: string -> ?cancel_label: string -> - [< Html_types.div_content ] Tyxml_js.Html.elt list -> - (unit -> unit) -> unit + title:string + -> ?ok_label:string + -> ?cancel_label:string + -> [< Html_types.div_content] Tyxml_js.Html.elt list + -> (unit -> unit) + -> unit val ask_string : - title: string -> - ?ok_label: string -> - [< Html_types.div_content > `Input] Tyxml_js.Html.elt list -> - string Lwt.t + title:string + -> ?ok_label:string + -> [< Html_types.div_content > `Input] Tyxml_js.Html.elt list + -> string Lwt.t -val catch_with_alert : ?printer: (exn -> string) -> (unit -> unit Lwt.t) -> unit Lwt.t +val catch_with_alert : + ?printer:(exn -> string) -> (unit -> unit Lwt.t) -> unit Lwt.t -val hide_loading : ?id: string -> unit -> unit +val hide_loading : ?id:string -> unit -> unit val show_loading : - ?id: string -> [< Html_types.div_content_fun ] Tyxml_js.Html.elt list -> - (unit -> 'a Lwt.t) -> 'a Lwt.t + ?id:string + -> [< Html_types.div_content_fun] Tyxml_js.Html.elt list + -> (unit -> 'a Lwt.t) + -> 'a Lwt.t val set_assoc : string -> 'a -> (string * 'a) list -> (string * 'a) list @@ -86,194 +95,219 @@ val enable_button : button_state -> unit val disabling_button_group : button_group -> (unit -> unit Lwt.t) -> unit Lwt.t val disable_with_button_group : - < disabled : bool Js.t Js.prop ; .. > Js.t -> - button_group -> unit + < disabled : bool Js.t Js.prop ; .. > Js.t -> button_group -> unit val button : - container: 'a Tyxml_js.Html.elt -> - theme: string -> - ?group: button_group -> - ?state: button_state -> - icon:string -> - string -> (unit -> unit Lwt.t) -> - unit + container:'a Tyxml_js.Html.elt + -> theme:string + -> ?group:button_group + -> ?state:button_state + -> icon:string + -> string + -> (unit -> unit Lwt.t) + -> unit val dropdown : - id: string -> - title: [< Html_types.button_content_fun > `PCDATA] Tyxml_js.Html.elt list -> - [< Html_types.div_content_fun ] Tyxml_js.Html.elt list -> - [> Html_types.div ] Tyxml_js.Html.elt + id:string + -> title:[< Html_types.button_content_fun > `PCDATA] Tyxml_js.Html.elt list + -> [< Html_types.div_content_fun] Tyxml_js.Html.elt list + -> [> Html_types.div] Tyxml_js.Html.elt val render_rich_text : - ?on_runnable_clicked: (string -> unit) -> - Learnocaml_data.Tutorial.text -> - [< Html_types.phrasing > `Code `Em `PCDATA ] Tyxml_js.Html.elt list + ?on_runnable_clicked:(string -> unit) + -> Learnocaml_data.Tutorial.text + -> [< Html_types.phrasing > `Code `Em `PCDATA] Tyxml_js.Html.elt list val extract_text_from_rich_text : Learnocaml_data.Tutorial.text -> string +val set_state_from_save_file : ?token:Token.t -> Save.t -> unit (** Sets the local storage from the data in a save file *) -val set_state_from_save_file : - ?token:Token.t -> Save.t -> unit -(** Gets a save file containing the locally stored data *) val get_state_as_save_file : ?include_reports:bool -> unit -> Save.t +(** Gets a save file containing the locally stored data *) +val sync : Token.t -> Save.t Lwt.t (** Sync the local save state with the server state, and returns the merged save file. The save will be created on the server if it doesn't exist. This syncs student {b,content}, but never the reports which are only synched on "Grade" *) -val sync: Token.t -> Save.t Lwt.t +val sync_exercise : + Token.t option + -> ?answer:Learnocaml_data.Answer.t + -> ?editor:string + -> Learnocaml_data.Exercise.id + -> Save.t Lwt.t (** The same, but limiting the submission to the given exercise, using the given answer if any, and the given editor text, if any. *) -val sync_exercise: - Token.t option -> ?answer:Learnocaml_data.Answer.t -> ?editor:string -> - Learnocaml_data.Exercise.id -> - Save.t Lwt.t -val countdown: - ?ontimeout: (unit -> unit) -> 'a Tyxml_js.Html5.elt -> float -> unit +val countdown : + ?ontimeout:(unit -> unit) -> 'a Tyxml_js.Html5.elt -> float -> unit -val string_of_seconds: int -> string +val string_of_seconds : int -> string -val flog: ('a, unit, string, unit) format4 -> 'a +val flog : ('a, unit, string, unit) format4 -> 'a -val stars_div: float -> [> Html_types.div ] Tyxml_js.Html5.elt +val stars_div : float -> [> Html_types.div] Tyxml_js.Html5.elt +val exercise_text : Exercise.Meta.t -> Exercise.t -> string (** Returns an HTML string expected to be put in an iframe *) -val exercise_text: - Exercise.Meta.t -> Exercise.t -> string -val string_of_exercise_kind: Exercise.Meta.kind -> string +val string_of_exercise_kind : Exercise.Meta.kind -> string -val get_assignments: - Token.Set.t -> Exercise.Status.t SMap.t -> - ((float * float) * Token.Set.t * bool * SSet.t) list +val get_assignments : + Token.Set.t + -> Exercise.Status.t SMap.t + -> ((float * float) * Token.Set.t * bool * SSet.t) list +val grade_color : int option -> string (** Returns a CSS color from a grade (red for 0, green for 100, grey for None) *) -val grade_color: int option -> string -val string_of_date: ?time:bool -> float -> string +val string_of_date : ?time:bool -> float -> string -val date: ?time:bool -> float -> [> Html_types.time ] Tyxml_js.Html5.elt +val date : ?time:bool -> float -> [> Html_types.time] Tyxml_js.Html5.elt -val tag_span: string -> [> Html_types.span ] Tyxml_js.Html5.elt +val tag_span : string -> [> Html_types.span] Tyxml_js.Html5.elt +val retrieve : ?ignore:'a -> 'a Learnocaml_api.request -> 'a Lwt.t (** A protected call to Server_caller.request *) -val retrieve: ?ignore:'a -> 'a Learnocaml_api.request -> 'a Lwt.t -val get_worker_code: string -> (unit -> string Lwt.t) +val get_worker_code : string -> unit -> string Lwt.t val set_string_translations_exercises : unit -> unit + val set_string_translations_view : unit -> unit val local_save : 'a Ace.editor -> string -> unit val toplevel_launch : - ?display_welcome:bool -> - ?after_init:(Learnocaml_toplevel.t -> unit Lwt.t) -> - ?on_disable:(unit -> unit) -> - ?on_enable:(unit -> unit) -> - [ `Div ] Tyxml_js.Html5.elt -> - (string -> - Learnocaml_toplevel_history.snapshot - Learnocaml_local_storage.storage_key) -> - (unit -> unit) -> - button_group -> string -> Learnocaml_toplevel.t Lwt.t - -val mouseover_toggle_signal : 'a Tyxml_js.Html5.elt -> 'b -> ('b option -> unit) -> unit + ?display_welcome:bool + -> ?after_init:(Learnocaml_toplevel.t -> unit Lwt.t) + -> ?on_disable:(unit -> unit) + -> ?on_enable:(unit -> unit) + -> [`Div] Tyxml_js.Html5.elt + -> ( string + -> Learnocaml_toplevel_history.snapshot + Learnocaml_local_storage.storage_key) + -> (unit -> unit) + -> button_group + -> string + -> Learnocaml_toplevel.t Lwt.t + +val mouseover_toggle_signal : + 'a Tyxml_js.Html5.elt -> 'b -> ('b option -> unit) -> unit val ace_display : - [< Html_types.div ] Tyxml_js.To_dom.elt -> (string -> unit) * (unit -> unit) + [< Html_types.div] Tyxml_js.To_dom.elt -> (string -> unit) * (unit -> unit) val init_toplevel_pane : - Learnocaml_toplevel.t Lwt.t -> - Learnocaml_toplevel.t -> - button_group -> - (icon:string -> - string -> (unit -> unit Lwt.t) -> - unit) -> - unit + Learnocaml_toplevel.t Lwt.t + -> Learnocaml_toplevel.t + -> button_group + -> (icon:string -> string -> (unit -> unit Lwt.t) -> unit) + -> unit val run_async_with_log : (unit -> 'a Lwt.t) -> unit -val mk_tab_handlers : string -> string list -> (unit -> unit) * (string -> unit) +val mk_tab_handlers : + string -> string list -> (unit -> unit) * (string -> unit) module type Editor_info = sig val ace : Ocaml_mode.editor Ace.editor + val buttons_container : 'a Tyxml_js.Html5.elt end module Editor_button (E : Editor_info) : sig val cleanup : string -> unit + val download : string -> unit + val eval : Learnocaml_toplevel.t -> (string -> 'a) -> unit + val sync : Token.t option Lwt.t -> Learnocaml_data.SMap.key -> unit end val setup_editor : string -> Ocaml_mode.editor * Ocaml_mode.editor Ace.editor val typecheck : - Learnocaml_toplevel.t -> - 'a Ace.editor -> Ocaml_mode.editor -> bool -> unit Lwt.t + Learnocaml_toplevel.t + -> 'a Ace.editor + -> Ocaml_mode.editor + -> bool + -> unit Lwt.t val set_nickname_div : unit -> unit val setup_prelude_pane : 'a Ace.editor -> string -> unit -val get_token : ?has_server:bool -> unit -> Learnocaml_data.student Learnocaml_data.token option Lwt.t - -module Display_exercise :functor - (Q : sig - val exercise_link : - ?cl:string list -> - string -> - 'a Tyxml_js.Html.elt list -> - [> 'a Html_types.a ] Tyxml_js.Html.elt - end) -> - sig - val display_descr : - Learnocaml_data.Exercise.Meta.t -> - [> Html_types.div ] Tyxml_js.Html5.elt - val display_stars : - Learnocaml_data.Exercise.Meta.t -> - [> Html_types.div ] Tyxml_js.Html5.elt - val display_kind : - Learnocaml_data.Exercise.Meta.t -> - [> Html_types.div ] Tyxml_js.Html5.elt - val display_exercise_meta : - string -> Learnocaml_data.Exercise.Meta.t -> string -> unit Lwt.t - val display_list : - ?sep:([> Html_types.pcdata ] as 'a) Tyxml_js.Html5.elt -> - 'a Tyxml_js.Html5.elt list -> 'a Tyxml_js.Html5.elt list - val get_skill_index : - 'a Learnocaml_data.token -> - [< `Focus of Learnocaml_data.SMap.key - | `Requirements of Learnocaml_data.SMap.key ] -> - Learnocaml_data.SSet.elt list Lwt.t - val display_skill_meta : - 'a -> string Tyxml_js.Html5.wrap list -> string -> unit Lwt.t - val display_link : - (string -> 'a) -> - string -> - string Tyxml_js.Html5.wrap -> [> Html_types.div ] Tyxml_js.Html5.elt - val display_skill_link : - string Tyxml_js.Html5.wrap list -> - string -> - [< `Focus of string Tyxml_js.Html5.wrap - | `Requirements of string Tyxml_js.Html5.wrap ] -> - [> Html_types.div ] Tyxml_js.Html5.elt - val display_exercise_link : - string -> - Learnocaml_data.Exercise.Meta.t -> - string Tyxml_js.Html5.wrap -> [> Html_types.div ] Tyxml_js.Html5.elt - val display_authors : - string -> - (string Tyxml_js.Html5.wrap * string Tyxml_js.Html5.wrap) list -> - [> `PCDATA | `Span ] Tyxml_js.Html5.elt list - val display_meta : - 'a Learnocaml_data.token option -> - Learnocaml_data.Exercise.Meta.t -> string -> unit Lwt.t - end +val get_token : + ?has_server:bool + -> unit + -> Learnocaml_data.student Learnocaml_data.token option Lwt.t + +module Display_exercise (Q : sig + val exercise_link : + ?cl:string list + -> string + -> 'a Tyxml_js.Html.elt list + -> [> 'a Html_types.a] Tyxml_js.Html.elt +end) : sig + val display_descr : + Learnocaml_data.Exercise.Meta.t -> [> Html_types.div] Tyxml_js.Html5.elt + + val display_stars : + Learnocaml_data.Exercise.Meta.t -> [> Html_types.div] Tyxml_js.Html5.elt + + val display_kind : + Learnocaml_data.Exercise.Meta.t -> [> Html_types.div] Tyxml_js.Html5.elt + + val display_exercise_meta : + string -> Learnocaml_data.Exercise.Meta.t -> string -> unit Lwt.t + + val display_list : + ?sep:([> Html_types.pcdata] as 'a) Tyxml_js.Html5.elt + -> 'a Tyxml_js.Html5.elt list + -> 'a Tyxml_js.Html5.elt list + + val get_skill_index : + 'a Learnocaml_data.token + -> [< `Focus of Learnocaml_data.SMap.key + | `Requirements of Learnocaml_data.SMap.key ] + -> Learnocaml_data.SSet.elt list Lwt.t + + val display_skill_meta : + 'a -> string Tyxml_js.Html5.wrap list -> string -> unit Lwt.t + + val display_link : + (string -> 'a) + -> string + -> string Tyxml_js.Html5.wrap + -> [> Html_types.div] Tyxml_js.Html5.elt + + val display_skill_link : + string Tyxml_js.Html5.wrap list + -> string + -> [< `Focus of string Tyxml_js.Html5.wrap + | `Requirements of string Tyxml_js.Html5.wrap ] + -> [> Html_types.div] Tyxml_js.Html5.elt + + val display_exercise_link : + string + -> Learnocaml_data.Exercise.Meta.t + -> string Tyxml_js.Html5.wrap + -> [> Html_types.div] Tyxml_js.Html5.elt + + val display_authors : + string + -> (string Tyxml_js.Html5.wrap * string Tyxml_js.Html5.wrap) list + -> [> `PCDATA | `Span] Tyxml_js.Html5.elt list + + val display_meta : + 'a Learnocaml_data.token option + -> Learnocaml_data.Exercise.Meta.t + -> string + -> unit Lwt.t +end diff --git a/src/app/learnocaml_config.ml b/src/app/learnocaml_config.ml index f99afef34..dad95526e 100644 --- a/src/app/learnocaml_config.ml +++ b/src/app/learnocaml_config.ml @@ -5,16 +5,26 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -class type learnocaml_config = object - method enableTryocaml: bool Js.optdef_prop - method enableLessons: bool Js.optdef_prop - method enableExercises: bool Js.optdef_prop - method enableToplevel: bool Js.optdef_prop - method enablePlayground: bool Js.optdef_prop - method txtLoginWelcome: Js.js_string Js.t Js.optdef_prop - method txtNickname: Js.js_string Js.t Js.optdef_prop - method baseUrl: Js.js_string Js.t Js.optdef_prop -end +class type learnocaml_config = + object + method enableTryocaml : bool Js.optdef_prop + + method enableLessons : bool Js.optdef_prop + + method enableExercises : bool Js.optdef_prop + + method enableToplevel : bool Js.optdef_prop + + method enablePlayground : bool Js.optdef_prop + + method txtLoginWelcome : Js.js_string Js.t Js.optdef_prop + + method txtNickname : Js.js_string Js.t Js.optdef_prop + + method baseUrl : Js.js_string Js.t Js.optdef_prop + end let config : learnocaml_config Js.t = Js.Unsafe.js_expr "learnocaml_config" -let api_server = Js.(to_string (Optdef.get config##.baseUrl (fun () -> string ""))) + +let api_server = + Js.(to_string (Optdef.get config##.baseUrl (fun () -> string ""))) diff --git a/src/app/learnocaml_config.mli b/src/app/learnocaml_config.mli index ba20ae535..ea59f2838 100644 --- a/src/app/learnocaml_config.mli +++ b/src/app/learnocaml_config.mli @@ -9,16 +9,25 @@ to the values stored in this file. It is "statically linked" with learnocaml-common.ml. *) -class type learnocaml_config = object - method enableTryocaml: bool Js.optdef_prop - method enableLessons: bool Js.optdef_prop - method enableExercises: bool Js.optdef_prop - method enableToplevel: bool Js.optdef_prop - method enablePlayground: bool Js.optdef_prop - method txtLoginWelcome: Js.js_string Js.t Js.optdef_prop - method txtNickname: Js.js_string Js.t Js.optdef_prop - method baseUrl: Js.js_string Js.t Js.optdef_prop -end +class type learnocaml_config = + object + method enableTryocaml : bool Js.optdef_prop + + method enableLessons : bool Js.optdef_prop + + method enableExercises : bool Js.optdef_prop + + method enableToplevel : bool Js.optdef_prop + + method enablePlayground : bool Js.optdef_prop + + method txtLoginWelcome : Js.js_string Js.t Js.optdef_prop + + method txtNickname : Js.js_string Js.t Js.optdef_prop + + method baseUrl : Js.js_string Js.t Js.optdef_prop + end val config : learnocaml_config Js.t + val api_server : string diff --git a/src/app/learnocaml_description_main.ml b/src/app/learnocaml_description_main.ml index 8ee7bcbee..52ba998d7 100644 --- a/src/app/learnocaml_description_main.ml +++ b/src/app/learnocaml_description_main.ml @@ -4,52 +4,52 @@ open Lwt.Infix open Learnocaml_common open Learnocaml_data.Exercise.Meta -let init_tabs, select_tab = - mk_tab_handlers "text" ["text"; "meta"] +let init_tabs, select_tab = mk_tab_handlers "text" ["text"; "meta"] -module Exercise_link = - struct - let exercise_link ?(cl = []) id content = - let token = Learnocaml_data.Token.(to_string (parse (arg "token"))) in - Tyxml_js.Html5.(a ~a:[ a_href ("/description/"^id^"#token="^token); - a_class cl ] - content) - end - -module Display = Display_exercise(Exercise_link) +module Exercise_link = struct + let exercise_link ?(cl = []) id content = + let token = Learnocaml_data.Token.(to_string (parse (arg "token"))) in + Tyxml_js.Html5.( + a + ~a:[a_href ("/description/" ^ id ^ "#token=" ^ token); a_class cl] + content) +end + +module Display = Display_exercise (Exercise_link) open Display - + let () = - run_async_with_log @@ fun () -> - let id = match Url.Current.path with - | "" :: "description" :: p | "description" :: p -> - String.concat "/" (List.map Url.urldecode (List.filter ((<>) "") p)) - | _ -> arg "id" in - Learnocaml_local_storage.init () ; - let text_container = find_component "learnocaml-exo-tab-text" in - try begin - let token = Learnocaml_data.Token.parse (arg "token") in - let exercise_fetch = - retrieve (Learnocaml_api.Exercise (Some token, id)) - in - init_tabs (); - exercise_fetch >>= fun (ex_meta, exo, _deadline) -> - (* display exercise questions *) - let text_iframe = Dom_html.createIframe Dom_html.document in - Manip.replaceChildren text_container - Tyxml_js.Html5.[ h1 [ txt ex_meta.title ] ; - Tyxml_js.Of_dom.of_iFrame text_iframe ] ; - Js.Opt.case - (text_iframe##.contentDocument) - (fun () -> failwith "cannot edit iframe document") - (fun d -> - d##open_; - d##write (Js.string (exercise_text ex_meta exo)); - d##close) ; - (* display meta *) - display_meta (Some token) ex_meta id - end - with Not_found -> - Lwt.return @@ - Manip.replaceChildren text_container - Tyxml_js.Html5.[ h1 [ txt "Error: Missing token" ] ] + run_async_with_log + @@ fun () -> + let id = + match Url.Current.path with + | "" :: "description" :: p | "description" :: p -> + String.concat "/" (List.map Url.urldecode (List.filter (( <> ) "") p)) + | _ -> arg "id" + in + Learnocaml_local_storage.init (); + let text_container = find_component "learnocaml-exo-tab-text" in + try + let token = Learnocaml_data.Token.parse (arg "token") in + let exercise_fetch = retrieve (Learnocaml_api.Exercise (Some token, id)) in + init_tabs (); + exercise_fetch + >>= fun (ex_meta, exo, _deadline) -> + (* display exercise questions *) + let text_iframe = Dom_html.createIframe Dom_html.document in + Manip.replaceChildren text_container + Tyxml_js.Html5. + [h1 [txt ex_meta.title]; Tyxml_js.Of_dom.of_iFrame text_iframe]; + Js.Opt.case + text_iframe##.contentDocument + (fun () -> failwith "cannot edit iframe document") + (fun d -> + d##open_; + d##write (Js.string (exercise_text ex_meta exo)); + d##close ); + (* display meta *) + display_meta (Some token) ex_meta id + with Not_found -> + Lwt.return + @@ Manip.replaceChildren text_container + Tyxml_js.Html5.[h1 [txt "Error: Missing token"]] diff --git a/src/app/learnocaml_exercise_main.ml b/src/app/learnocaml_exercise_main.ml index db13a77a1..c72a7cdb0 100644 --- a/src/app/learnocaml_exercise_main.ml +++ b/src/app/learnocaml_exercise_main.ml @@ -12,280 +12,304 @@ open Lwt.Infix open Learnocaml_common open Learnocaml_data open Learnocaml_config - module H = Tyxml_js.Html let init_tabs, select_tab = - mk_tab_handlers "text" [ "toplevel" ; "report" ; "editor"; "meta" ] + mk_tab_handlers "text" ["toplevel"; "report"; "editor"; "meta"] let check_if_need_refresh has_server = - if has_server then + if has_server then ( let local_server_id = Learnocaml_local_storage.(retrieve server_id) in retrieve @@ Learnocaml_api.Version () - >|= (fun (_, server_id) -> + >|= fun (_, server_id) -> if local_server_id <> server_id then - let title = [%i "WARNING: You have an older grader version than the server"] + let title = + [%i "WARNING: You have an older grader version than the server"] and ok_label = [%i "Refresh the page"] and refresh () = Dom_html.window##.location##reload and cancel_label = [%i "I will do it myself!"] - and message = [%i "The server has been updated, please refresh the page to make sure you are using the latest version of Learn-OCaml server (none of your work will be lost)."] in - let contents = [ H.p [H.txt (String.trim message) ] ] in - confirm ~title ~ok_label ~cancel_label contents refresh) - else - Lwt.return_unit + and message = + [%i + "The server has been updated, please refresh the page to make sure \ + you are using the latest version of Learn-OCaml server (none of \ + your work will be lost)."] + in + let contents = [H.p [H.txt (String.trim message)]] in + confirm ~title ~ok_label ~cancel_label contents refresh ) + else Lwt.return_unit let get_grade = let get_worker = get_worker_code "learnocaml-grader-worker.js" in fun ?callback ?timeout exercise -> - get_worker () >>= fun worker_js_file -> + get_worker () + >>= fun worker_js_file -> Grading_jsoo.get_grade ~worker_js_file ?callback ?timeout exercise let display_report exo report = let score, _failed = Report.result report in let report_button = find_component "learnocaml-exo-button-report" in - Manip.removeClass report_button "success" ; - Manip.removeClass report_button "failure" ; - Manip.removeClass report_button "partial" ; + Manip.removeClass report_button "success"; + Manip.removeClass report_button "failure"; + Manip.removeClass report_button "partial"; let grade = let max = Learnocaml_exercise.(access File.max_score exo) in if max = 0 then 999 else score * 100 / max in - if grade >= 100 then begin - Manip.addClass report_button "success" ; - Manip.replaceChildren report_button - Tyxml_js.Html5.[ txt [%i"Report"] ] - end else if grade = 0 then begin - Manip.addClass report_button "failure" ; - Manip.replaceChildren report_button - Tyxml_js.Html5.[ txt [%i"Report"] ] - end else begin - Manip.addClass report_button "partial" ; + if grade >= 100 then ( + Manip.addClass report_button "success"; + Manip.replaceChildren report_button Tyxml_js.Html5.[txt [%i "Report"]] ) + else if grade = 0 then ( + Manip.addClass report_button "failure"; + Manip.replaceChildren report_button Tyxml_js.Html5.[txt [%i "Report"]] ) + else ( + Manip.addClass report_button "partial"; let pct = Format.asprintf "%2d%%" grade in Manip.replaceChildren report_button - Tyxml_js.Html5.[ txt [%i"Report"] ; - span ~a: [ a_class [ "score" ] ] [ txt pct ]] - end ; + Tyxml_js.Html5.[txt [%i "Report"]; span ~a:[a_class ["score"]] [txt pct]] ); let report_container = find_component "learnocaml-exo-tab-report" in Manip.setInnerHtml report_container - (Format.asprintf "%a" Report.(output_html ~bare: true) report) ; + (Format.asprintf "%a" Report.(output_html ~bare:true) report); grade -module Exercise_link = - struct - let exercise_link ?(cl = []) id content = - let open Tyxml_js.Html5 in - a ~a:[ a_href ("/exercises/"^id^"/") ; - a_class cl ; - ] - content - end - -module Display = Display_exercise(Exercise_link) -open Display +module Exercise_link = struct + let exercise_link ?(cl = []) id content = + let open Tyxml_js.Html5 in + a ~a:[a_href ("/exercises/" ^ id ^ "/"); a_class cl] content +end + +module Display = Display_exercise (Exercise_link) +open Display let is_readonly = ref false let make_readonly () = is_readonly := true; - alert ~title:[%i"TIME'S UP"] - [%i"The deadline for this exercise has expired. Any changes you make \ - from now on will remain local only."] + alert ~title:[%i "TIME'S UP"] + [%i + "The deadline for this exercise has expired. Any changes you make from \ + now on will remain local only."] let () = - run_async_with_log @@ fun () -> + run_async_with_log + @@ fun () -> set_string_translations_exercises (); Learnocaml_local_storage.init (); - Server_caller.request (Learnocaml_api.Version ()) >>= - (function - | Ok (_, server_id) -> Learnocaml_local_storage.(store server_id) server_id; Lwt.return_true - | Error _ -> Lwt.return_false) >>= fun has_server -> - let token = get_token ~has_server () - in + Server_caller.request (Learnocaml_api.Version ()) + >>= (function + | Ok (_, server_id) -> + Learnocaml_local_storage.(store server_id) server_id; + Lwt.return_true + | Error _ -> Lwt.return_false) + >>= fun has_server -> + let token = get_token ~has_server () in (* ---- launch everything --------------------------------------------- *) let toplevel_buttons_group = button_group () in - disable_button_group toplevel_buttons_group (* enabled after init *) ; + disable_button_group toplevel_buttons_group (* enabled after init *); let toplevel_toolbar = find_component "learnocaml-exo-toplevel-toolbar" in let editor_toolbar = find_component "learnocaml-exo-editor-toolbar" in let toplevel_button = - button ~container: toplevel_toolbar ~theme: "dark" ~group:toplevel_buttons_group ?state:None in - let id = match Url.Current.path with + button ~container:toplevel_toolbar ~theme:"dark" + ~group:toplevel_buttons_group ?state:None + in + let id = + match Url.Current.path with | "" :: "exercises" :: p | "exercises" :: p -> - String.concat "/" (List.map Url.urldecode (List.filter ((<>) "") p)) + String.concat "/" (List.map Url.urldecode (List.filter (( <> ) "") p)) | _ -> arg "id" in Dom_html.document##.title := - Js.string (id ^ " - " ^ "Learn OCaml" ^" v."^ Learnocaml_api.version); + Js.string (id ^ " - " ^ "Learn OCaml" ^ " v." ^ Learnocaml_api.version); let exercise_fetch = - token >>= fun token -> - retrieve (Learnocaml_api.Exercise (token, id)) + token >>= fun token -> retrieve (Learnocaml_api.Exercise (token, id)) in let after_init top = - exercise_fetch >>= fun (_meta, exo, _deadline) -> - begin match Learnocaml_exercise.(decipher File.prelude exo) with - | "" -> Lwt.return true - | prelude -> - Learnocaml_toplevel.load ~print_outcome:true top - ~message: [%i"loading the prelude..."] - prelude - end >>= fun r1 -> + exercise_fetch + >>= fun (_meta, exo, _deadline) -> + ( match Learnocaml_exercise.(decipher File.prelude exo) with + | "" -> Lwt.return true + | prelude -> + Learnocaml_toplevel.load ~print_outcome:true top + ~message:[%i "loading the prelude..."] prelude ) + >>= fun r1 -> Learnocaml_toplevel.load ~print_outcome:false top - (Learnocaml_exercise.(decipher File.prepare exo)) >>= fun r2 -> - if not r1 || not r2 then failwith [%i"error in prelude"] ; - Learnocaml_toplevel.set_checking_environment top >>= fun () -> - Lwt.return () in + Learnocaml_exercise.(decipher File.prepare exo) + >>= fun r2 -> + if (not r1) || not r2 then failwith [%i "error in prelude"]; + Learnocaml_toplevel.set_checking_environment top + >>= fun () -> Lwt.return () + in let toplevel_launch = - toplevel_launch ~after_init (find_component "learnocaml-exo-toplevel-pane") + toplevel_launch ~after_init + (find_component "learnocaml-exo-toplevel-pane") Learnocaml_local_storage.exercise_toplevel_history - (fun () -> select_tab "toplevel") toplevel_buttons_group id + (fun () -> select_tab "toplevel") + toplevel_buttons_group id in - init_tabs () ; + init_tabs (); set_nickname_div (); - toplevel_launch >>= fun top -> - exercise_fetch >>= fun (ex_meta, exo, deadline) -> - (match deadline with - | None -> () - | Some 0. -> make_readonly () - | Some t -> - match Manip.by_id "learnocaml-countdown" with - | Some elt -> countdown elt t ~ontimeout:make_readonly - | None -> ()); + toplevel_launch + >>= fun top -> + exercise_fetch + >>= fun (ex_meta, exo, deadline) -> + ( match deadline with + | None -> () + | Some 0. -> make_readonly () + | Some t -> ( + match Manip.by_id "learnocaml-countdown" with + | Some elt -> countdown elt t ~ontimeout:make_readonly + | None -> () ) ); let solution = match Learnocaml_local_storage.(retrieve (exercise_state id)) with - | { Answer.report = Some report ; solution ; _ } -> + | {Answer.report = Some report; solution; _} -> let _ : int = display_report exo report in solution - | { Answer.report = None ; solution ; _ } -> - solution - | exception Not_found -> Learnocaml_exercise.(access File.template exo) in + | {Answer.report = None; solution; _} -> solution + | exception Not_found -> Learnocaml_exercise.(access File.template exo) + in (* ---- details pane -------------------------------------------------- *) let load_meta () = - Lwt.async (fun () -> - token >>= fun token -> - display_meta token ex_meta id) + Lwt.async (fun () -> token >>= fun token -> display_meta token ex_meta id) in - if arg "tab" = "meta" then load_meta () else + if arg "tab" = "meta" then load_meta () + else Manip.Ev.onclick (find_component "learnocaml-exo-button-meta") (fun _ -> - load_meta (); - select_tab "meta"; - true); + load_meta (); select_tab "meta"; true ); (* ---- toplevel pane ------------------------------------------------- *) - init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button ; + init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button; (* ---- text pane ----------------------------------------------------- *) let text_container = find_component "learnocaml-exo-tab-text" in let text_iframe = Dom_html.createIframe Dom_html.document in Manip.replaceChildren text_container - Tyxml_js.Html5.[ h1 [ txt ex_meta.Exercise.Meta.title ] ; - Tyxml_js.Of_dom.of_iFrame text_iframe ] ; + Tyxml_js.Html5. + [ h1 [txt ex_meta.Exercise.Meta.title] + ; Tyxml_js.Of_dom.of_iFrame text_iframe ]; (* ---- editor pane --------------------------------------------------- *) let editor, ace = setup_editor solution in - let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in - EB.cleanup (Learnocaml_exercise.(access File.template exo)); + let module EB = Editor_button (struct + let ace = ace + + let buttons_container = editor_toolbar + end) in + EB.cleanup Learnocaml_exercise.(access File.template exo); EB.sync token id; EB.download id; EB.eval top select_tab; let typecheck = typecheck top ace editor in -(*------------- prelude -----------------*) + (*------------- prelude -----------------*) setup_prelude_pane ace Learnocaml_exercise.(decipher File.prelude exo); Js.Opt.case - (text_iframe##.contentDocument) + text_iframe##.contentDocument (fun () -> failwith "cannot edit iframe document") (fun d -> - d##open_; - d##write (Js.string (exercise_text ex_meta exo)); - d##close) ; + d##open_; + d##write (Js.string (exercise_text ex_meta exo)); + d##close ); (* ---- main toolbar -------------------------------------------------- *) let exo_toolbar = find_component "learnocaml-exo-toolbar" in - let toolbar_button = button ~container: exo_toolbar ~theme: "light" in - begin toolbar_button - ~icon: "list" [%i"Exercises"] @@ fun () -> - Dom_html.window##.location##assign - (Js.string (api_server ^ "/index.html#activity=exercises")) ; - Lwt.return () - end ; + let toolbar_button = button ~container:exo_toolbar ~theme:"light" in + ( toolbar_button ~icon:"list" [%i "Exercises"] + @@ fun () -> + Dom_html.window##.location##assign + (Js.string (api_server ^ "/index.html#activity=exercises")); + Lwt.return () ); let messages = Tyxml_js.Html5.ul [] in let callback text = - Manip.appendChild messages Tyxml_js.Html5.(li [ txt text ]) in - let worker = - ref (get_grade ~callback exo) + Manip.appendChild messages Tyxml_js.Html5.(li [txt text]) + in + let worker = ref (get_grade ~callback exo) in + (toolbar_button ~icon:"typecheck" [%i "Compile"] @@ fun () -> typecheck true); + ( toolbar_button ~icon:"reload" [%i "Grade!"] + @@ fun () -> + check_if_need_refresh has_server + >>= fun () -> + let aborted, abort_message = + let t, u = Lwt.task () in + let btn = Tyxml_js.Html5.(button [txt [%i "abort"]]) in + Manip.Ev.onclick btn (fun _ -> Lwt.wakeup u (); true); + let div = + Tyxml_js.Html5.( + div + ~a:[a_class ["dialog"]] + [txt [%i "Grading is taking a lot of time, "]; btn; txt " ?"]) + in + Manip.SetCss.opacity div (Some "0"); + (t, div) in - begin toolbar_button - ~icon: "typecheck" [%i"Compile"] @@ fun () -> - typecheck true - end; - begin toolbar_button - ~icon: "reload" [%i"Grade!"] @@ fun () -> - check_if_need_refresh has_server >>= fun () -> - let aborted, abort_message = - let t, u = Lwt.task () in - let btn = Tyxml_js.Html5.(button [ txt [%i"abort"] ]) in - Manip.Ev.onclick btn (fun _ -> Lwt.wakeup u () ; true) ; - let div = - Tyxml_js.Html5.(div ~a: [ a_class [ "dialog" ] ] - [ txt [%i"Grading is taking a lot of time, "] ; - btn ; - txt " ?" ]) in - Manip.SetCss.opacity div (Some "0") ; - t, div in - Manip.replaceChildren messages - Tyxml_js.Html5.[ li [ txt [%i"Launching the grader"] ] ] ; - let submit_report = not !is_readonly in (* Don't count the grading time *) - show_loading ~id:"learnocaml-exo-loading" [ messages ; abort_message ] - @@ fun () -> - Lwt_js.sleep 1. >>= fun () -> - let solution = Ace.get_contents ace in - Learnocaml_toplevel.check top solution >>= fun res -> - match res with - | Toploop_results.Ok ((), _) -> - let grading = - Lwt.finalize - (fun () -> - !worker >>= fun w -> - w solution >>= fun (report, _, _, _) -> - Lwt.return report) - (fun () -> - worker := get_grade ~callback exo; - Lwt.return_unit) - in - let abortion = - Lwt_js.sleep 5. >>= fun () -> - Manip.SetCss.opacity abort_message (Some "1") ; - aborted >>= fun () -> - Lwt.return Learnocaml_report.[ Message ([ Text [%i"Grading aborted by user."] ], Failure) ] in - Lwt.pick [ grading ; abortion ] >>= fun report -> - let grade = display_report exo report in - let editor, answer = - if submit_report then - None, - Some { Answer.grade = Some grade ; - solution ; - report = Some report ; - mtime = max_float } (* To ensure server time will be used *) - else - Some solution, None - in - token >>= fun token -> - sync_exercise token id ?answer ?editor >>= fun _save -> - select_tab "report" ; - Lwt_js.yield () >>= fun () -> - Ace.focus ace ; - Lwt.return () - | Toploop_results.Error _ -> - let msg = - Learnocaml_report.[ Text [%i"Error in your code."] ; Break ; - Text [%i"Cannot start the grader if your code does not typecheck."] ] in - let report = Learnocaml_report.[ Message (msg, Failure) ] in - let grade = display_report exo report in - Learnocaml_local_storage.(store (exercise_state id)) - { Answer.grade = Some grade ; solution ; report = Some report ; - mtime = gettimeofday () } ; - select_tab "report" ; - Lwt_js.yield () >>= fun () -> - Ace.focus ace ; - typecheck true - end ; + Manip.replaceChildren messages + Tyxml_js.Html5.[li [txt [%i "Launching the grader"]]]; + let submit_report = not !is_readonly in + (* Don't count the grading time *) + show_loading ~id:"learnocaml-exo-loading" [messages; abort_message] + @@ fun () -> + Lwt_js.sleep 1. + >>= fun () -> + let solution = Ace.get_contents ace in + Learnocaml_toplevel.check top solution + >>= fun res -> + match res with + | Toploop_results.Ok ((), _) -> + let grading = + Lwt.finalize + (fun () -> + !worker + >>= fun w -> + w solution >>= fun (report, _, _, _) -> Lwt.return report ) + (fun () -> + worker := get_grade ~callback exo; + Lwt.return_unit ) + in + let abortion = + Lwt_js.sleep 5. + >>= fun () -> + Manip.SetCss.opacity abort_message (Some "1"); + aborted + >>= fun () -> + Lwt.return + Learnocaml_report. + [Message ([Text [%i "Grading aborted by user."]], Failure)] + in + Lwt.pick [grading; abortion] + >>= fun report -> + let grade = display_report exo report in + let editor, answer = + if submit_report then + ( None + , Some + { Answer.grade = Some grade + ; solution + ; report = Some report + ; mtime = max_float } ) (* To ensure server time will be used *) + else (Some solution, None) + in + token + >>= fun token -> + sync_exercise token id ?answer ?editor + >>= fun _save -> + select_tab "report"; + Lwt_js.yield () >>= fun () -> Ace.focus ace; Lwt.return () + | Toploop_results.Error _ -> + let msg = + Learnocaml_report. + [ Text [%i "Error in your code."] + ; Break + ; Text + [%i "Cannot start the grader if your code does not typecheck."] + ] + in + let report = Learnocaml_report.[Message (msg, Failure)] in + let grade = display_report exo report in + Learnocaml_local_storage.(store (exercise_state id)) + { Answer.grade = Some grade + ; solution + ; report = Some report + ; mtime = gettimeofday () }; + select_tab "report"; + Lwt_js.yield () >>= fun () -> Ace.focus ace; typecheck true ); Window.onunload (fun _ev -> local_save ace id; true); (* ---- return -------------------------------------------------------- *) - toplevel_launch >>= fun _ -> - typecheck false >>= fun () -> - hide_loading ~id:"learnocaml-exo-loading" () ; + toplevel_launch + >>= fun _ -> + typecheck false + >>= fun () -> + hide_loading ~id:"learnocaml-exo-loading" (); Lwt.return () diff --git a/src/app/learnocaml_index_main.ml b/src/app/learnocaml_index_main.ml index cf67f896c..8b46b0dae 100644 --- a/src/app/learnocaml_index_main.ml +++ b/src/app/learnocaml_index_main.ml @@ -12,13 +12,12 @@ open Lwt open Learnocaml_data open Learnocaml_common open Learnocaml_config - module H = Tyxml_js.Html5 module El = struct (** Defines the static elements that should be present from index.html *) - let id s = s, find_component s + let id s = (s, find_component s) let loading_id, loading = id "learnocaml-main-loading" @@ -43,19 +42,28 @@ module El = struct module Login_overlay = struct let login_overlay_id, login_overlay = id "login-overlay" + let input_nick_id, input_nick = id "login-nickname-input" + let input_secret_id, input_secret = id "login-secret-input" + let button_new_id, button_new = id "login-new-button" + let input_tok_id, input_tok = id "login-token-input" + let button_connect_id, button_connect = id "login-connect-button" + let nickname_field_id, nickname_field = id "learnocaml-nickname" end module Dyn = struct (** Elements that are dynamically created (ids only) *) let exercise_list_id = "learnocaml-main-exercise-list" + let tryocaml_id = "learnocaml-main-tryocaml" + let lesson_id = "learnocaml-main-lesson" + let toplevel_id = "learnocaml-main-toplevel" end end @@ -68,8 +76,10 @@ let get_url token dynamic_url static_url id = | None -> api_server ^ "/" ^ static_url ^ Url.urlencode id let exercises_tab token _ _ () = - show_loading [%i"Loading exercises"] @@ fun () -> - Lwt_js.sleep 0.5 >>= fun () -> + show_loading [%i "Loading exercises"] + @@ fun () -> + Lwt_js.sleep 0.5 + >>= fun () -> retrieve (Learnocaml_api.Exercise_index token) >>= fun (index, deadlines) -> let format_exercise_list all_exercise_states = @@ -79,85 +89,106 @@ let exercises_tab token _ _ () = | Exercise.Index.Exercises exercises -> List.fold_left (fun acc (exercise_id, meta_opt) -> - match meta_opt with None -> acc | Some meta -> - let {Exercise.Meta.kind; title; short_description; stars; _ } = - meta - in - let pct_init = - match SMap.find exercise_id all_exercise_states with - | exception Not_found -> None - | { Answer.grade ; _ } -> grade in - let pct_signal, pct_signal_set = React.S.create pct_init in - Learnocaml_local_storage.(listener (exercise_state exercise_id)) := - Some (function - | Some { Answer.grade ; _ } -> pct_signal_set grade - | None -> pct_signal_set None) ; - let pct_text_signal = - React.S.map - (function - | None -> "--" - | Some 0 -> "0%" - | Some pct -> string_of_int pct ^ "%") - pct_signal in - let time_left = match List.assoc_opt exercise_id deadlines with - | None -> "" - | Some 0. -> [%i"Exercise closed"] - | Some f -> Printf.sprintf [%if"Time left: %s"] - (string_of_seconds (int_of_float f)) - in - let status_classes_signal = - React.S.map - (function - | None -> [ "stats" ] - | Some 0 -> [ "stats" ; "failure" ] - | Some pct when pct >= 100 -> [ "stats" ; "success" ] - | Some _ -> [ "stats" ; "partial" ]) - pct_signal in - a ~a:[ a_href (get_url token "/exercises/" "exercise.html#id=" exercise_id) ; - a_class [ "exercise" ] ] [ - div ~a:[ a_class [ "descr" ] ] ( - h1 [ txt title ] :: - begin match short_description with - | None -> [] - | Some text -> [ txt text ] - end - ); - div ~a:[ a_class [ "time-left" ] ] [H.txt time_left]; - div ~a:[ Tyxml_js.R.Html5.a_class status_classes_signal ] [ - stars_div stars; - div ~a:[ a_class [ "length" ] ] [ - match kind with - | Exercise.Meta.Project -> txt [%i"project"] - | Exercise.Meta.Problem -> txt [%i"problem"] - | Exercise.Meta.Exercise -> txt [%i"exercise"] ] ; - div ~a:[ a_class [ "score" ] ] [ - Tyxml_js.R.Html5.txt pct_text_signal - ] - ] ] :: - acc) + match meta_opt with + | None -> acc + | Some meta -> + let {Exercise.Meta.kind; title; short_description; stars; _} + = + meta + in + let pct_init = + match SMap.find exercise_id all_exercise_states with + | exception Not_found -> None + | {Answer.grade; _} -> grade + in + let pct_signal, pct_signal_set = React.S.create pct_init in + Learnocaml_local_storage.( + listener (exercise_state exercise_id)) + := Some + (function + | Some {Answer.grade; _} -> pct_signal_set grade + | None -> pct_signal_set None); + let pct_text_signal = + React.S.map + (function + | None -> "--" + | Some 0 -> "0%" + | Some pct -> string_of_int pct ^ "%") + pct_signal + in + let time_left = + match List.assoc_opt exercise_id deadlines with + | None -> "" + | Some 0. -> [%i "Exercise closed"] + | Some f -> + Printf.sprintf [%if "Time left: %s"] + (string_of_seconds (int_of_float f)) + in + let status_classes_signal = + React.S.map + (function + | None -> ["stats"] + | Some 0 -> ["stats"; "failure"] + | Some pct when pct >= 100 -> ["stats"; "success"] + | Some _ -> ["stats"; "partial"]) + pct_signal + in + a + ~a: + [ a_href + (get_url token "/exercises/" "exercise.html#id=" + exercise_id) + ; a_class ["exercise"] ] + [ div + ~a:[a_class ["descr"]] + ( h1 [txt title] + :: + ( match short_description with + | None -> [] + | Some text -> [txt text] ) ) + ; div ~a:[a_class ["time-left"]] [H.txt time_left] + ; div + ~a:[Tyxml_js.R.Html5.a_class status_classes_signal] + [ stars_div stars + ; div + ~a:[a_class ["length"]] + [ ( match kind with + | Exercise.Meta.Project -> txt [%i "project"] + | Exercise.Meta.Problem -> txt [%i "problem"] + | Exercise.Meta.Exercise -> txt [%i "exercise"] + ) ] + ; div + ~a:[a_class ["score"]] + [Tyxml_js.R.Html5.txt pct_text_signal] ] ] + :: acc ) acc exercises | Exercise.Index.Groups groups -> let h = match lvl with 1 -> h1 | 2 -> h2 | _ -> h3 in List.fold_left - (fun acc (_, Exercise.Index.{ title ; contents }) -> - format_contents (succ lvl) - (h ~a:[ a_class [ "pack" ] ] [ txt title ] :: acc) - contents) - acc groups in - List.rev (format_contents 1 [] index) in + (fun acc (_, Exercise.Index.({title; contents})) -> + format_contents (succ lvl) + (h ~a:[a_class ["pack"]] [txt title] :: acc) + contents ) + acc groups + in + List.rev (format_contents 1 [] index) + in let list_div = - match format_exercise_list - Learnocaml_local_storage.(retrieve all_exercise_states) + match + format_exercise_list + Learnocaml_local_storage.(retrieve all_exercise_states) with - | [] -> H.div [H.txt [%i"No open exercises at the moment"]] + | [] -> H.div [H.txt [%i "No open exercises at the moment"]] | l -> H.div ~a:[H.a_id El.Dyn.exercise_list_id] l in - Manip.appendChild El.content list_div; - Lwt.return list_div + Manip.appendChild El.content list_div; + Lwt.return list_div let playground_tab token _ _ () = - show_loading [%i"Loading playground"] @@ fun () -> - Lwt_js.sleep 0.5 >>= fun () -> + show_loading [%i "Loading playground"] + @@ fun () -> + Lwt_js.sleep 0.5 + >>= fun () -> retrieve (Learnocaml_api.Playground_index ()) >>= fun index -> let list_div = @@ -165,375 +196,394 @@ let playground_tab token _ _ () = let open Tyxml_js.Html5 in let title = pmeta.Playground.Meta.title in let short_description = pmeta.Playground.Meta.short_description in - a ~a:[ a_href (get_url token "/playground/" "playground.html#id=" id) ; - a_class [ "exercise" ] ] [ - div ~a:[ a_class [ "descr" ] ] ( - h1 [ txt title ] :: - begin match short_description with - | None -> [] - | Some text -> [ txt text ] - end - ); - ] + a + ~a: + [ a_href (get_url token "/playground/" "playground.html#id=" id) + ; a_class ["exercise"] ] + [ div + ~a:[a_class ["descr"]] + ( h1 [txt title] + :: + ( match short_description with + | None -> [] + | Some text -> [txt text] ) ) ] in - List.map format_contents index in + List.map format_contents index + in let list_div = H.div ~a:[H.a_id El.Dyn.exercise_list_id] list_div in Manip.appendChild El.content list_div; Lwt.return list_div let lessons_tab select (arg, set_arg, _delete_arg) () = - show_loading [%i"Loading lessons"] @@ fun () -> - Lwt_js.sleep 0.5 >>= fun () -> - retrieve (Learnocaml_api.Lesson_index ()) >>= fun index -> - let navigation_div = - Tyxml_js.Html5.(div ~a: [ a_class [ "navigation" ] ] []) in - let main_div = - Tyxml_js.Html5.(div ~a: [ a_class [ "toplevel-pane" ] ] []) in + show_loading [%i "Loading lessons"] + @@ fun () -> + Lwt_js.sleep 0.5 + >>= fun () -> + retrieve (Learnocaml_api.Lesson_index ()) + >>= fun index -> + let navigation_div = Tyxml_js.Html5.(div ~a:[a_class ["navigation"]] []) in + let main_div = Tyxml_js.Html5.(div ~a:[a_class ["toplevel-pane"]] []) in let options = List.map (fun (lesson_id, lesson_title) -> - lesson_id, - Tyxml_js.Html5. - (option ~a: [ a_value lesson_id ] (txt lesson_title))) - index in + ( lesson_id + , Tyxml_js.Html5.(option ~a:[a_value lesson_id] (txt lesson_title)) ) + ) + index + in let prev_and_next id = let rec loop = function | [] -> assert false - | [ _ ] (* assumes single id *) -> None, None - | (one, _) :: (two, _) :: _ when id = one -> None, Some two - | (one, _) :: (two, _) :: [] when id = two -> Some one, None - | (one, _) :: (two, _) :: (three, _) :: _ when id = two -> Some one, Some three - | _ :: rest -> loop rest - in loop index in - let selector = - Tyxml_js.Html5.(select (snd (List.split options))) in + | [_] (* assumes single id *) -> (None, None) + | (one, _) :: (two, _) :: _ when id = one -> (None, Some two) + | [(one, _); (two, _)] when id = two -> (Some one, None) + | (one, _) :: (two, _) :: (three, _) :: _ when id = two -> + (Some one, Some three) + | _ :: rest -> loop rest + in + loop index + in + let selector = Tyxml_js.Html5.(select (snd (List.split options))) in let prev_button_state = button_state () in let next_button_state = button_state () in let load_lesson ~loading () = let selector = Tyxml_js.To_dom.of_select selector in let id = Js.to_string selector##.value in - retrieve ~ignore:Lesson.{title=""; steps=[]} - (Learnocaml_api.Lesson id) >>= fun { Lesson.steps; _ } -> - Manip.removeChildren main_div ; - (if loading then show_loading [%i"Running OCaml examples"] - else fun f -> f ()) @@ fun () -> + retrieve ~ignore:Lesson.{title = ""; steps = []} (Learnocaml_api.Lesson id) + >>= fun {Lesson.steps; _} -> + Manip.removeChildren main_div; + ( if loading then show_loading [%i "Running OCaml examples"] + else fun f -> f () ) + @@ fun () -> let toplevel_buttons_group = button_group () in - disable_button_group toplevel_buttons_group (* enabled after init *) ; + disable_button_group toplevel_buttons_group (* enabled after init *); toplevel_launch ~display_welcome:false main_div Learnocaml_local_storage.toplevel_history - (fun () -> Lwt.async select) toplevel_buttons_group ("lesson-" ^ id) + (fun () -> Lwt.async select) + toplevel_buttons_group ("lesson-" ^ id) >>= fun top -> Lwt_list.iter_s - (fun { Lesson.step_title ; step_phrases } -> - Learnocaml_toplevel.print_html top ("

" ^ step_title ^ "

") ; - let do_phrase = function - | Lesson.Text text -> - Learnocaml_toplevel.print_html top text ; - Lwt.return () - | Lesson.Code code -> - Learnocaml_toplevel.execute_phrase top code >>= fun _ -> - Lwt.return () in - Lwt_list.iter_s do_phrase step_phrases >>= fun () -> - Lwt.return () - ) - steps >>= fun () -> - set_arg "lesson" id ; - begin match prev_and_next id with - | None, None -> - disable_button prev_button_state ; - disable_button next_button_state - | Some _, None -> - enable_button prev_button_state ; - disable_button next_button_state - | None, Some _ -> - disable_button prev_button_state ; - enable_button next_button_state - | Some _, Some _ -> - enable_button prev_button_state ; - enable_button next_button_state - end ; - Lwt.return () in + (fun {Lesson.step_title; step_phrases} -> + Learnocaml_toplevel.print_html top ("

" ^ step_title ^ "

"); + let do_phrase = function + | Lesson.Text text -> + Learnocaml_toplevel.print_html top text; + Lwt.return () + | Lesson.Code code -> + Learnocaml_toplevel.execute_phrase top code + >>= fun _ -> Lwt.return () + in + Lwt_list.iter_s do_phrase step_phrases >>= fun () -> Lwt.return () ) + steps + >>= fun () -> + set_arg "lesson" id; + ( match prev_and_next id with + | None, None -> + disable_button prev_button_state; + disable_button next_button_state + | Some _, None -> + enable_button prev_button_state; + disable_button next_button_state + | None, Some _ -> + disable_button prev_button_state; + enable_button next_button_state + | Some _, Some _ -> + enable_button prev_button_state; + enable_button next_button_state ); + Lwt.return () + in let group = button_group () in - begin button - ~group ~state: prev_button_state ~container: navigation_div - ~theme: "black" ~icon: "left" [%i"Prev"] @@ fun () -> - let selector = Tyxml_js.To_dom.of_select selector in - let id = Js.to_string selector##.value in - match prev_and_next id with - | Some prev, _ -> - let option = Tyxml_js.To_dom.of_option (List.assoc prev options) in - option##.selected := Js._true ; - load_lesson ~loading: true () - | _ -> Lwt.return () - end ; - Manip.appendChild navigation_div selector ; - disable_with_button_group (Tyxml_js.To_dom.of_select selector) group ; - (Tyxml_js.To_dom.of_select selector)##.onchange := - Dom_html.handler (fun _ -> Lwt.async (load_lesson ~loading: true) ; Js._true) ; - begin button - ~group ~state: next_button_state ~container: navigation_div - ~theme: "black" ~icon: "right" [%i"Next"] @@ fun () -> - let selector = Tyxml_js.To_dom.of_select selector in - let id = Js.to_string selector##.value in - match prev_and_next id with - | _, Some next -> - let option = Tyxml_js.To_dom.of_option (List.assoc next options) in - option##.selected := Js._true ; - load_lesson ~loading: true () - | _ -> Lwt.return () - end ; + ( button ~group ~state:prev_button_state ~container:navigation_div + ~theme:"black" ~icon:"left" [%i "Prev"] + @@ fun () -> + let selector = Tyxml_js.To_dom.of_select selector in + let id = Js.to_string selector##.value in + match prev_and_next id with + | Some prev, _ -> + let option = Tyxml_js.To_dom.of_option (List.assoc prev options) in + option##.selected := Js._true; + load_lesson ~loading:true () + | _ -> Lwt.return () ); + Manip.appendChild navigation_div selector; + disable_with_button_group (Tyxml_js.To_dom.of_select selector) group; + (Tyxml_js.To_dom.of_select selector)##.onchange + := Dom_html.handler (fun _ -> + Lwt.async (load_lesson ~loading:true); + Js._true ); + ( button ~group ~state:next_button_state ~container:navigation_div + ~theme:"black" ~icon:"right" [%i "Next"] + @@ fun () -> + let selector = Tyxml_js.To_dom.of_select selector in + let id = Js.to_string selector##.value in + match prev_and_next id with + | _, Some next -> + let option = Tyxml_js.To_dom.of_option (List.assoc next options) in + option##.selected := Js._true; + load_lesson ~loading:true () + | _ -> Lwt.return () ); let lesson_div = - Tyxml_js.Html5.(div ~a: [ a_id El.Dyn.lesson_id ]) - [ navigation_div ; main_div ] in - Manip.appendChild El.content lesson_div ; - begin try - let id = match arg "lesson" with + Tyxml_js.Html5.(div ~a:[a_id El.Dyn.lesson_id]) [navigation_div; main_div] + in + Manip.appendChild El.content lesson_div; + ( try + let id = + match arg "lesson" with | id -> id - | exception Not_found -> match index with - | [] -> raise Not_found - | (id, _) :: _ -> id in + | exception Not_found -> ( + match index with [] -> raise Not_found | (id, _) :: _ -> id ) + in let option = Tyxml_js.To_dom.of_option (List.assoc id options) in - option##.selected := Js._true ; - load_lesson ~loading: false () - with Not_found -> failwith "lesson not found" - end >>= fun () -> - Lwt.return lesson_div + option##.selected := Js._true; + load_lesson ~loading:false () + with Not_found -> failwith "lesson not found" ) + >>= fun () -> Lwt.return lesson_div let tryocaml_tab select (arg, set_arg, _delete_arg) () = let open Tutorial in - let navigation_div = - Tyxml_js.Html5.(div ~a: [ a_class [ "navigation" ] ] []) in - let step_title_container = - Tyxml_js.Html5.h3 [] in - let step_title = - Tyxml_js.Html5.span [] in - let step_items_container = - Tyxml_js.Html5.div [] in + let navigation_div = Tyxml_js.Html5.(div ~a:[a_class ["navigation"]] []) in + let step_title_container = Tyxml_js.Html5.h3 [] in + let step_title = Tyxml_js.Html5.span [] in + let step_items_container = Tyxml_js.Html5.div [] in let step_div = - Tyxml_js.Html5.(div ~a: [ a_class [ "step-pane" ] ] - [ step_title_container ; - step_items_container ]) in - let toplevel_div = - Tyxml_js.Html5.(div ~a: [ a_class [ "toplevel-pane" ] ] []) in - let buttons_div = - Tyxml_js.Html5.(div ~a: [ a_class [ "buttons" ] ] []) in + Tyxml_js.Html5.( + div + ~a:[a_class ["step-pane"]] + [step_title_container; step_items_container]) + in + let toplevel_div = Tyxml_js.Html5.(div ~a:[a_class ["toplevel-pane"]] []) in + let buttons_div = Tyxml_js.Html5.(div ~a:[a_class ["buttons"]] []) in let tutorial_div = - Tyxml_js.Html5.(div ~a: [ a_id El.Dyn.tryocaml_id ]) - [ navigation_div ; step_div ; toplevel_div ; buttons_div ] in + Tyxml_js.Html5.(div ~a:[a_id El.Dyn.tryocaml_id]) + [navigation_div; step_div; toplevel_div; buttons_div] + in let toplevel_buttons_group = button_group () in - disable_button_group toplevel_buttons_group (* enabled after init *) ; + disable_button_group toplevel_buttons_group (* enabled after init *); let toplevel_launch = let on_disable () = Manip.addClass step_div "disabled" in let on_enable () = Manip.removeClass step_div "disabled" in toplevel_launch ~on_disable ~on_enable toplevel_div Learnocaml_local_storage.toplevel_history - (fun () -> Lwt.async select) toplevel_buttons_group "tryocaml" + (fun () -> Lwt.async select) + toplevel_buttons_group "tryocaml" in - show_loading [%i"Loading tutorials"] @@ fun () -> - Lwt_js.sleep 0.5 >>= fun () -> - Manip.appendChild El.content tutorial_div ; - retrieve ~ignore:[] (Learnocaml_api.Tutorial_index ()) >>= fun index -> + show_loading [%i "Loading tutorials"] + @@ fun () -> + Lwt_js.sleep 0.5 + >>= fun () -> + Manip.appendChild El.content tutorial_div; + retrieve ~ignore:[] (Learnocaml_api.Tutorial_index ()) + >>= fun index -> let index = - List.flatten @@ List.fold_left - (fun acc (_, { Index.series_tutorials; _ }) -> - series_tutorials :: acc) - [] index in + List.flatten + @@ List.fold_left + (fun acc (_, {Index.series_tutorials; _}) -> series_tutorials :: acc) + [] index + in let options = List.map - (fun { Tutorial.Index.name; title } -> - name, - H.option ~a: [ H.a_value name ] - (H.txt (extract_text_from_rich_text title))) - index in - let selector = - Tyxml_js.Html5.(select (snd (List.split options))) in - let dom_selector = - Tyxml_js.To_dom.of_select selector in + (fun {Tutorial.Index.name; title} -> + ( name + , H.option ~a:[H.a_value name] + (H.txt (extract_text_from_rich_text title)) ) ) + index + in + let selector = Tyxml_js.Html5.(select (snd (List.split options))) in + let dom_selector = Tyxml_js.To_dom.of_select selector in let prev_and_next id = let rec loop = function | [] -> assert false - | [ _ ] (* assumes single id *) -> None, None - | { Tutorial.Index.name = one ; _ } :: - { Tutorial.Index.name = two ; _ } :: _ when id = one -> None, Some two - | { Tutorial.Index.name = one ; _ } :: - { Tutorial.Index.name = two ; _ } :: [] when id = two -> Some one, None - | { Tutorial.Index.name = one ; _ } :: - { Tutorial.Index.name = two ; _ } :: - { Tutorial.Index.name = three ; _} :: _ when id = two -> - Some one, Some three - | _ :: rest -> loop rest - in loop index in - let current_tutorial_name = ref @@ + | [_] (* assumes single id *) -> (None, None) + | {Tutorial.Index.name = one; _} :: {Tutorial.Index.name = two; _} :: _ + when id = one -> + (None, Some two) + | [{Tutorial.Index.name = one; _}; {Tutorial.Index.name = two; _}] + when id = two -> + (Some one, None) + | {Tutorial.Index.name = one; _} + :: {Tutorial.Index.name = two; _} + :: {Tutorial.Index.name = three; _} :: _ + when id = two -> + (Some one, Some three) + | _ :: rest -> loop rest + in + loop index + in + let current_tutorial_name = + ref + @@ match arg "tutorial" with | exception Not_found -> (List.hd index).Tutorial.Index.name - | tutorial_name -> tutorial_name in - let current_step_id = ref @@ + | tutorial_name -> tutorial_name + in + let current_step_id = + ref + @@ match int_of_string (arg "step") with | exception _ -> 0 - | step_id -> step_id in + | step_id -> step_id + in let prev_button_state = button_state () in let next_button_state = button_state () in let prev_step_button_state = button_state () in let next_step_button_state = button_state () in let load_tutorial tutorial_name step_id () = - retrieve ~ignore:{Tutorial.title = []; steps = []} - (Learnocaml_api.Tutorial tutorial_name) >>= fun { Tutorial.steps; _ } -> - set_arg "tutorial" tutorial_name ; - set_arg "step" (string_of_int step_id) ; + retrieve + ~ignore:{Tutorial.title = []; steps = []} + (Learnocaml_api.Tutorial tutorial_name) + >>= fun {Tutorial.steps; _} -> + set_arg "tutorial" tutorial_name; + set_arg "step" (string_of_int step_id); let prev, next = prev_and_next tutorial_name in - begin match prev with - | None -> disable_button prev_button_state - | Some _ -> enable_button prev_button_state - end ; - begin match next with - | None -> disable_button next_button_state - | Some _ -> enable_button next_button_state - end ; + ( match prev with + | None -> disable_button prev_button_state + | Some _ -> enable_button prev_button_state ); + ( match next with + | None -> disable_button next_button_state + | Some _ -> enable_button next_button_state ); let option = - Tyxml_js.To_dom.of_option (List.assoc tutorial_name options) in - option##.selected := Js._true ; - let step = try - List.nth steps step_id - with _ -> failwith "unknown step" in - if step_id = 0 then - disable_button prev_step_button_state - else - enable_button prev_step_button_state ; + Tyxml_js.To_dom.of_option (List.assoc tutorial_name options) + in + option##.selected := Js._true; + let step = + try List.nth steps step_id with _ -> failwith "unknown step" + in + if step_id = 0 then disable_button prev_step_button_state + else enable_button prev_step_button_state; if step_id = List.length steps - 1 then disable_button next_step_button_state - else - enable_button next_step_button_state ; - current_tutorial_name := tutorial_name ; - current_step_id := step_id ; - Manip.replaceChildren step_title - (render_rich_text step.step_title) ; + else enable_button next_step_button_state; + current_tutorial_name := tutorial_name; + current_step_id := step_id; + Manip.replaceChildren step_title (render_rich_text step.step_title); let items = let on_runnable_clicked code = - Lwt.async @@ fun () -> - toplevel_launch >>= fun top -> - if button_group_disabled toplevel_buttons_group then - Lwt.return () + Lwt.async + @@ fun () -> + toplevel_launch + >>= fun top -> + if button_group_disabled toplevel_buttons_group then Lwt.return () else - disabling_button_group toplevel_buttons_group - (fun () -> - Learnocaml_toplevel.execute_phrase top code >>= fun _ -> - Lwt.return ()) in + disabling_button_group toplevel_buttons_group (fun () -> + Learnocaml_toplevel.execute_phrase top code + >>= fun _ -> Lwt.return () ) + in let rec render_phrases phrases = List.map (function | Paragraph text -> - Tyxml_js.Html5.p - (render_rich_text ~on_runnable_clicked text) - | Code_block { code ; runnable } -> - let elt = Tyxml_js.Html.pre [ Tyxml_js.Html.txt code ] in - if runnable then begin - Manip.addClass elt "runnable" ; - Manip.Ev.onclick elt (fun _ -> on_runnable_clicked code ; true) - end ; + Tyxml_js.Html5.p (render_rich_text ~on_runnable_clicked text) + | Code_block {code; runnable} -> + let elt = Tyxml_js.Html.pre [Tyxml_js.Html.txt code] in + if runnable then ( + Manip.addClass elt "runnable"; + Manip.Ev.onclick elt (fun _ -> on_runnable_clicked code; true) ); elt | Enum items -> Tyxml_js.Html5.ul - (List.map (fun phrases -> - Tyxml_js.Html5.li (render_phrases phrases)) - items)) - phrases in - render_phrases step.step_contents in - Manip.replaceChildren step_items_container items ; - toplevel_launch >>= fun top -> - Learnocaml_toplevel.scroll top ; - Lwt.return () in - begin button - ~group: toplevel_buttons_group - ~state: prev_button_state ~container: navigation_div - ~theme: "black" ~icon: "left" [%i"Prev"] @@ fun () -> - match prev_and_next !current_tutorial_name with - | Some prev, _ -> - load_tutorial prev 0 () - | _ -> Lwt.return () - end ; - Manip.appendChild navigation_div selector ; - disable_with_button_group (Tyxml_js.To_dom.of_select selector) - toplevel_buttons_group ; + (List.map + (fun phrases -> Tyxml_js.Html5.li (render_phrases phrases)) + items)) + phrases + in + render_phrases step.step_contents + in + Manip.replaceChildren step_items_container items; + toplevel_launch + >>= fun top -> + Learnocaml_toplevel.scroll top; + Lwt.return () + in + ( button ~group:toplevel_buttons_group ~state:prev_button_state + ~container:navigation_div ~theme:"black" ~icon:"left" [%i "Prev"] + @@ fun () -> + match prev_and_next !current_tutorial_name with + | Some prev, _ -> load_tutorial prev 0 () + | _ -> Lwt.return () ); + Manip.appendChild navigation_div selector; + disable_with_button_group + (Tyxml_js.To_dom.of_select selector) + toplevel_buttons_group; dom_selector##.onchange := Dom_html.handler (fun _ -> - let id = Js.to_string (dom_selector##.value) in - Lwt.async (load_tutorial id 0) ; - Js._true) ; - begin button - ~group: toplevel_buttons_group - ~state: next_button_state ~container: navigation_div - ~theme: "black" ~icon: "right" [%i"Next"] @@ fun () -> - match prev_and_next !current_tutorial_name with - | _, Some next ->load_tutorial next 0 () - | _ -> Lwt.return () - end ; - begin button - ~group: toplevel_buttons_group - ~state: prev_step_button_state ~container: step_title_container - ~theme: "black" ~icon: "left" "" @@ fun () -> - load_tutorial !current_tutorial_name (!current_step_id - 1) () - end ; - Manip.appendChild step_title_container step_title ; - begin button - ~group: toplevel_buttons_group - ~state: next_step_button_state ~container: step_title_container - ~theme: "black" ~icon: "right" "" @@ fun () -> - load_tutorial !current_tutorial_name (!current_step_id + 1) () - end ; - load_tutorial !current_tutorial_name !current_step_id () >>= fun () -> - toplevel_launch >>= fun top -> + let id = Js.to_string dom_selector##.value in + Lwt.async (load_tutorial id 0); + Js._true ); + ( button ~group:toplevel_buttons_group ~state:next_button_state + ~container:navigation_div ~theme:"black" ~icon:"right" [%i "Next"] + @@ fun () -> + match prev_and_next !current_tutorial_name with + | _, Some next -> load_tutorial next 0 () + | _ -> Lwt.return () ); + ( button ~group:toplevel_buttons_group ~state:prev_step_button_state + ~container:step_title_container ~theme:"black" ~icon:"left" "" + @@ fun () -> load_tutorial !current_tutorial_name (!current_step_id - 1) () + ); + Manip.appendChild step_title_container step_title; + ( button ~group:toplevel_buttons_group ~state:next_step_button_state + ~container:step_title_container ~theme:"black" ~icon:"right" "" + @@ fun () -> load_tutorial !current_tutorial_name (!current_step_id + 1) () + ); + load_tutorial !current_tutorial_name !current_step_id () + >>= fun () -> + toplevel_launch + >>= fun top -> let toplevel_button = - button ~container: buttons_div ~theme: "dark" ~group:toplevel_buttons_group ?state:None in - init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button ; + button ~container:buttons_div ~theme:"dark" ~group:toplevel_buttons_group + ?state:None + in + init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button; Lwt.return tutorial_div let toplevel_tab select _ () = - let container = - Tyxml_js.Html5.(div ~a: [ a_class [ "toplevel-pane" ] ]) [] in - let buttons_div = - Tyxml_js.Html5.(div ~a: [ a_class [ "buttons" ] ]) [] in + let container = Tyxml_js.Html5.(div ~a:[a_class ["toplevel-pane"]]) [] in + let buttons_div = Tyxml_js.Html5.(div ~a:[a_class ["buttons"]]) [] in let div = - Tyxml_js.Html5.(div ~a: [ a_id El.Dyn.toplevel_id ]) - [ container ; buttons_div ] in - show_loading [%i"Launching OCaml"] @@ fun () -> + Tyxml_js.Html5.(div ~a:[a_id El.Dyn.toplevel_id]) [container; buttons_div] + in + show_loading [%i "Launching OCaml"] + @@ fun () -> let toplevel_buttons_group = button_group () in - disable_button_group toplevel_buttons_group (* enabled after init *) ; - toplevel_launch container - Learnocaml_local_storage.toplevel_history - (fun _ -> Lwt.async select) toplevel_buttons_group "toplevel" + disable_button_group toplevel_buttons_group (* enabled after init *); + toplevel_launch container Learnocaml_local_storage.toplevel_history + (fun _ -> Lwt.async select) + toplevel_buttons_group "toplevel" >>= fun top -> - Manip.appendChild El.content div ; - let button = button ~container: buttons_div ~theme: "dark" ?group:None ?state:None in - init_toplevel_pane (Lwt.return top) top toplevel_buttons_group button ; + Manip.appendChild El.content div; + let button = + button ~container:buttons_div ~theme:"dark" ?group:None ?state:None + in + init_toplevel_pane (Lwt.return top) top toplevel_buttons_group button; Lwt.return div let teacher_tab token a b () = - show_loading [%i"Loading student info"] @@ fun () -> - Learnocaml_teacher_tab.teacher_tab token a b () >>= fun div -> - Lwt.return div + show_loading [%i "Loading student info"] + @@ fun () -> + Learnocaml_teacher_tab.teacher_tab token a b () >>= fun div -> Lwt.return div -let get_stored_token () = - Learnocaml_local_storage.(retrieve sync_token) +let get_stored_token () = Learnocaml_local_storage.(retrieve sync_token) let sync () = sync (get_stored_token ()) let token_disp_div token = - H.input ~a: [ - H.a_input_type `Text; - H.a_size 17; - H.a_style "font-size: 110%; font-weight: bold;"; - H.a_class ["learnocaml_token"]; - H.a_readonly (); - H.a_value (Token.to_string token); - ] () + H.input + ~a: + [ H.a_input_type `Text + ; H.a_size 17 + ; H.a_style "font-size: 110%; font-weight: bold;" + ; H.a_class ["learnocaml_token"] + ; H.a_readonly () + ; H.a_value (Token.to_string token) ] + () let show_token_dialog token = - ext_alert ~title:[%i"Your Learn-OCaml token"] [ - H.p [H.txt [%i"Your token is displayed below. It identifies you and \ - allows to share your workspace between devices."]]; - H.p [H.txt [%i"Please write it down."]]; - H.div ~a:[H.a_style "text-align: center;"] [token_disp_div token]; - ] + ext_alert ~title:[%i "Your Learn-OCaml token"] + [ H.p + [ H.txt + [%i + "Your token is displayed below. It identifies you and allows to \ + share your workspace between devices."] ] + ; H.p [H.txt [%i "Please write it down."]] + ; H.div ~a:[H.a_style "text-align: center;"] [token_disp_div token] ] let init_token_dialog () = let open El.Login_overlay in @@ -541,58 +591,57 @@ let init_token_dialog () = let get_token, got_token = Lwt.task () in let create_token () = let nickname = String.trim (Manip.value input_nick) in - if Token.check nickname || String.length nickname < 2 then - (Manip.SetCss.borderColor input_nick "#f44"; - Lwt.return_none) + if Token.check nickname || String.length nickname < 2 then ( + Manip.SetCss.borderColor input_nick "#f44"; + Lwt.return_none ) else let secret = Sha.sha512 (String.trim (Manip.value input_secret)) in retrieve (Learnocaml_api.Nonce ()) >>= fun nonce -> let secret = Sha.sha512 (nonce ^ secret) in - (Learnocaml_local_storage.(store nickname) nickname; - retrieve - (Learnocaml_api.Create_token (secret, None, Some nickname)) - >>= fun token -> - Learnocaml_local_storage.(store sync_token) token; - show_token_dialog token; - Lwt.return_some (token, nickname)) + Learnocaml_local_storage.(store nickname) nickname; + retrieve (Learnocaml_api.Create_token (secret, None, Some nickname)) + >>= fun token -> + Learnocaml_local_storage.(store sync_token) token; + show_token_dialog token; + Lwt.return_some (token, nickname) in let rec login_token () = let input = input_tok in match Token.parse (Manip.value input) with - | exception (Failure _) -> + | exception Failure _ -> Manip.SetCss.borderColor input "#f44"; Lwt.return_none - | token -> - Server_caller.request (Learnocaml_api.Fetch_save token) >>= function + | token -> ( + Server_caller.request (Learnocaml_api.Fetch_save token) + >>= function | Ok save -> set_state_from_save_file ~token save; Lwt.return_some (token, save.Save.nickname) | Error (`Not_found _) -> - alert ~title:[%i"TOKEN NOT FOUND"] - [%i"The entered token couldn't be recognised."]; + alert ~title:[%i "TOKEN NOT FOUND"] + [%i "The entered token couldn't be recognised."]; Lwt.return_none | Error e -> - lwt_alert ~title:[%i"REQUEST ERROR"] [ - H.p [H.txt [%i"Could not retrieve data from server"]]; - H.code [H.txt (Server_caller.string_of_error e)]; - ] ~buttons:[ - [%i"Retry"], (fun () -> login_token ()); - [%i"Cancel"], (fun () -> Lwt.return_none); - ] + lwt_alert ~title:[%i "REQUEST ERROR"] + [ H.p [H.txt [%i "Could not retrieve data from server"]] + ; H.code [H.txt (Server_caller.string_of_error e)] ] + ~buttons: + [ ([%i "Retry"], fun () -> login_token ()) + ; ([%i "Cancel"], fun () -> Lwt.return_none) ] ) in - let handler f t = fun _ -> + let handler f t _ = Lwt.async (fun () -> - f () >|= function - | Some token -> Lwt.wakeup got_token token - | None -> ()); + f () + >|= function Some token -> Lwt.wakeup got_token token | None -> () ); t in Manip.Ev.onclick button_new (handler create_token false); Manip.Ev.onreturn input_nick (handler create_token ()); Manip.Ev.onclick button_connect (handler login_token false); Manip.Ev.onreturn input_tok (handler login_token ()); - get_token >|= fun (token, nickname) -> + get_token + >|= fun (token, nickname) -> (Tyxml_js.To_dom.of_input nickname_field)##.value := Js.string nickname; Manip.SetCss.display login_overlay "none"; token @@ -600,185 +649,194 @@ let init_token_dialog () = let init_sync_token button_group = catch (fun () -> - begin try - Lwt.return Learnocaml_local_storage.(retrieve sync_token) - with Not_found -> init_token_dialog () - end >>= fun token -> - enable_button_group button_group ; - Lwt.return (Some token)) + ( try Lwt.return Learnocaml_local_storage.(retrieve sync_token) + with Not_found -> init_token_dialog () ) + >>= fun token -> + enable_button_group button_group; + Lwt.return (Some token) ) (fun _ -> Lwt.return None) let set_string_translations () = let configured v s = Js.Optdef.case v (fun () -> s) Js.to_string in - let translations = [ - "txt_connected_as", - [%i"Connected as"]; - "txt_choose_activity", - [%i"Activities"]; - "txt_login_welcome", configured config##.txtLoginWelcome - [%i"Welcome to Learn OCaml"]; - "txt_first_connection", [%i"First connection"]; - "txt_first_connection_dialog", [%i"Choose a nickname"]; - "txt_first_connection_secret", [%i"Secret"]; - "txt_login_new", [%i"Create new token"]; - "txt_returning", [%i"Returning user"]; - "txt_returning_dialog", [%i"Enter your token"]; - "txt_login_returning", [%i"Connect"]; - ] in + let translations = + [ ("txt_connected_as", [%i "Connected as"]) + ; ("txt_choose_activity", [%i "Activities"]) + ; ( "txt_login_welcome" + , configured config##.txtLoginWelcome [%i "Welcome to Learn OCaml"] ) + ; ("txt_first_connection", [%i "First connection"]) + ; ("txt_first_connection_dialog", [%i "Choose a nickname"]) + ; ("txt_first_connection_secret", [%i "Secret"]) + ; ("txt_login_new", [%i "Create new token"]) + ; ("txt_returning", [%i "Returning user"]) + ; ("txt_returning_dialog", [%i "Enter your token"]) + ; ("txt_login_returning", [%i "Connect"]) ] + in List.iter - (fun (id, text) -> - Manip.setInnerHtml (find_component id) text) + (fun (id, text) -> Manip.setInnerHtml (find_component id) text) translations; - let placeholder_translations = [ - El.nickname_field, configured config##.txtNickname - [%i"Nickname"]; - El.Login_overlay.input_nick, configured config##.txtNickname - [%i"Nickname"]; - ] in + let placeholder_translations = + [ (El.nickname_field, configured config##.txtNickname [%i "Nickname"]) + ; ( El.Login_overlay.input_nick + , configured config##.txtNickname [%i "Nickname"] ) ] + in List.iter (fun (el, text) -> - (Tyxml_js.To_dom.of_input el)##.placeholder := Js.string text) + (Tyxml_js.To_dom.of_input el)##.placeholder := Js.string text ) placeholder_translations - let () = - Lwt.async_exception_hook := begin fun e -> - Firebug.console##log (Js.string - (Printexc.to_string e ^ - if Printexc.backtrace_status () then - Printexc.get_backtrace () - else "")); - match e with - | Lwt.Canceled -> () - | Failure message -> fatal message - | Server_caller.Cannot_fetch message -> fatal message - | exn -> fatal (Printexc.to_string exn) - end ; - (match Js_utils.get_lang() with Some l -> Ocplib_i18n.set_lang l | None -> ()); - Lwt.async @@ fun () -> + (Lwt.async_exception_hook := + fun e -> + Firebug.console##log + (Js.string + ( Printexc.to_string e + ^ + if Printexc.backtrace_status () then Printexc.get_backtrace () + else "" )); + match e with + | Lwt.Canceled -> () + | Failure message -> fatal message + | Server_caller.Cannot_fetch message -> fatal message + | exn -> fatal (Printexc.to_string exn)); + ( match Js_utils.get_lang () with + | Some l -> Ocplib_i18n.set_lang l + | None -> () ); + Lwt.async + @@ fun () -> set_string_translations (); Dom_html.document##.title := - Js.string ("Learn OCaml" ^ " v."^Learnocaml_api.version); - Manip.setInnerText El.version ("v."^Learnocaml_api.version); - Learnocaml_local_storage.init () ; + Js.string ("Learn OCaml" ^ " v." ^ Learnocaml_api.version); + Manip.setInnerText El.version ("v." ^ Learnocaml_api.version); + Learnocaml_local_storage.init (); let sync_button_group = button_group () in disable_button_group sync_button_group; let menu_hidden = ref true in let no_tab_selected () = - Manip.removeChildren El.content ; + Manip.removeChildren El.content; let div = - Tyxml_js.Html5.(div ~a: [ a_class [ "placeholder" ] ]) - Tyxml_js.Html5.[ div [ txt [%i"Choose an activity."] ]] in - Manip.removeChildren El.content ; - Manip.appendChild El.content div ; + Tyxml_js.Html5.(div ~a:[a_class ["placeholder"]]) + Tyxml_js.Html5.[div [txt [%i "Choose an activity."]]] + in + Manip.removeChildren El.content; + Manip.appendChild El.content div; delete_arg "activity" in let init_tabs token = let get_opt o = Js.Optdef.get o (fun () -> false) in let tabs = - (if get_opt config##.enableTryocaml - then [ "tryocaml", ([%i"Try OCaml"], tryocaml_tab) ] else []) @ - (if get_opt config##.enableLessons - then [ "lessons", ([%i"Lessons"], lessons_tab) ] else []) @ - (if get_opt config##.enableExercises then - ["exercises", ([%i"Exercises"], exercises_tab token)] - else []) @ - (if get_opt config##.enableToplevel - then [ "toplevel", ([%i"Toplevel"], toplevel_tab) ] else []) @ - (if get_opt config##.enablePlayground - then [ "playground", ([%i"Playground"], playground_tab token) ] else []) @ - (match token with - | Some t when Token.is_teacher t -> - [ "teacher", ([%i"Teach"], teacher_tab t) ] - | _ -> []) + ( if get_opt config##.enableTryocaml then + [("tryocaml", ([%i "Try OCaml"], tryocaml_tab))] + else [] ) + @ ( if get_opt config##.enableLessons then + [("lessons", ([%i "Lessons"], lessons_tab))] + else [] ) + @ ( if get_opt config##.enableExercises then + [("exercises", ([%i "Exercises"], exercises_tab token))] + else [] ) + @ ( if get_opt config##.enableToplevel then + [("toplevel", ([%i "Toplevel"], toplevel_tab))] + else [] ) + @ ( if get_opt config##.enablePlayground then + [("playground", ([%i "Playground"], playground_tab token))] + else [] ) + @ + match token with + | Some t when Token.is_teacher t -> + [("teacher", ([%i "Teach"], teacher_tab t))] + | _ -> [] in let container = El.tab_buttons_container in let current_btn = ref None in let current_args = ref (ref []) in let select_thread = ref None in - Manip.removeChildren container ; + Manip.removeChildren container; List.map (fun (id, (name, callback)) -> - let btn = Tyxml_js.Html5.(button [ txt name]) in - let div = ref None in - let args = ref [] in - let rec select () = - let th () = - Lwt.pause () >>= fun () -> - begin match !current_btn with - | None -> () - | Some btn -> Manip.removeClass btn "active" - end ; - Manip.removeChildren El.content ; - List.iter (fun (n, _) -> delete_arg n) !(!current_args) ; - begin match !div with - | Some div -> - List.iter (fun (n, v) -> set_arg n v) !args ; - Manip.appendChild El.content div ; - Lwt.return_unit - | None -> - let arg name = - arg name in - let set_arg name value = - args := set_assoc name value !args ; - set_arg name value in - let delete_arg name = - args := delete_assoc name !args ; - delete_arg name in - callback select (arg, set_arg, delete_arg) () >>= fun fresh -> - div := Some fresh ; - Lwt.return_unit - end >>= fun () -> - set_arg "activity" id ; - Manip.addClass btn "active" ; - menu_hidden := true ; - Manip.addClass El.menu "hidden" ; - current_btn := Some btn ; - current_args := args ; - Lwt.return_unit - in - (match !select_thread with None -> () | Some th -> Lwt.cancel th); - Lwt.finalize (fun () -> - let th = th () in - select_thread := Some th; - th) - (fun () -> select_thread := None; Lwt.return_unit) - in - Manip.Ev.onclick btn - (fun _ -> Lwt.async select ; true) ; - Manip.appendChild container btn ; - id, (name, select)) + let btn = Tyxml_js.Html5.(button [txt name]) in + let div = ref None in + let args = ref [] in + let rec select () = + let th () = + Lwt.pause () + >>= fun () -> + ( match !current_btn with + | None -> () + | Some btn -> Manip.removeClass btn "active" ); + Manip.removeChildren El.content; + List.iter (fun (n, _) -> delete_arg n) !(!current_args); + ( match !div with + | Some div -> + List.iter (fun (n, v) -> set_arg n v) !args; + Manip.appendChild El.content div; + Lwt.return_unit + | None -> + let arg name = arg name in + let set_arg name value = + args := set_assoc name value !args; + set_arg name value + in + let delete_arg name = + args := delete_assoc name !args; + delete_arg name + in + callback select (arg, set_arg, delete_arg) () + >>= fun fresh -> + div := Some fresh; + Lwt.return_unit ) + >>= fun () -> + set_arg "activity" id; + Manip.addClass btn "active"; + menu_hidden := true; + Manip.addClass El.menu "hidden"; + current_btn := Some btn; + current_args := args; + Lwt.return_unit + in + (match !select_thread with None -> () | Some th -> Lwt.cancel th); + Lwt.finalize + (fun () -> + let th = th () in + select_thread := Some th; + th ) + (fun () -> + select_thread := None; + Lwt.return_unit ) + in + Manip.Ev.onclick btn (fun _ -> Lwt.async select; true); + Manip.appendChild container btn; + (id, (name, select)) ) tabs in let download_save () = let name = "learnocaml-main.json" in let contents = let json = - Json_repr_browser.Json_encoding.construct - Save.enc - (get_state_as_save_file ~include_reports:true ()) in - Js._JSON##(stringify json) in - Learnocaml_common.fake_download ~name ~contents ; + Json_repr_browser.Json_encoding.construct Save.enc + (get_state_as_save_file ~include_reports:true ()) + in + Js._JSON ## (stringify json) + in + Learnocaml_common.fake_download ~name ~contents; Lwt.return () in let import_save () = - Learnocaml_common.fake_upload () >>= fun (_, contents) -> + Learnocaml_common.fake_upload () + >>= fun (_, contents) -> let save_file = - Json_repr_browser.Json_encoding.destruct - Save.enc - (Js._JSON##(parse contents)) in + Json_repr_browser.Json_encoding.destruct Save.enc + Js._JSON ## (parse contents) + in let token = try Some (get_stored_token ()) with Not_found -> None in - set_state_from_save_file ?token save_file ; - (Tyxml_js.To_dom.of_input El.nickname_field)##.value := - Js.string save_file.Save.nickname; + set_state_from_save_file ?token save_file; + (Tyxml_js.To_dom.of_input El.nickname_field)##.value + := Js.string save_file.Save.nickname; let _tabs = init_tabs token in - no_tab_selected (); - Lwt.return () + no_tab_selected (); Lwt.return () in let download_all () = let token = get_stored_token () |> Token.to_string in Dom_html.window##.location##assign - (Js.string @@ "/archive.zip?token=" ^ token); + (Js.string @@ "/archive.zip?token=" ^ token); Lwt.return_unit in let logout_dialog () = @@ -786,66 +844,67 @@ let () = (Learnocaml_api.Update_save (get_stored_token (), get_state_as_save_file ())) >|= (function - | Ok _ -> - [%i"Be sure to write down your token before logging out:"] - | Error _ -> - [%i"WARNING: the data could not be synchronised with the server. \ - Logging out will lose your local changes, be sure you exported \ - a backup."]) + | Ok _ -> [%i "Be sure to write down your token before logging out:"] + | Error _ -> + [%i + "WARNING: the data could not be synchronised with the server. \ + Logging out will lose your local changes, be sure you \ + exported a backup."]) >|= fun s -> - confirm ~title:[%i"Logout"] ~ok_label:[%i"Logout"] - [H.p [H.txt s]; - H.div ~a:[H.a_style "text-align: center;"] - [token_disp_div (get_stored_token ())]] + confirm ~title:[%i "Logout"] ~ok_label:[%i "Logout"] + [ H.p [H.txt s] + ; H.div + ~a:[H.a_style "text-align: center;"] + [token_disp_div (get_stored_token ())] ] (fun () -> - Lwt.async @@ fun () -> - Learnocaml_local_storage.clear (); - reload (); - Lwt.return_unit) + Lwt.async + @@ fun () -> + Learnocaml_local_storage.clear (); + reload (); + Lwt.return_unit ) in - List.iter (fun (text, icon, f) -> - button ~container:El.sync_buttons ~theme:"white" ~group:sync_button_group ~icon text f) - [ - [%i"Show token"], "token", (fun () -> + List.iter + (fun (text, icon, f) -> + button ~container:El.sync_buttons ~theme:"white" ~group:sync_button_group + ~icon text f ) + [ ( [%i "Show token"] + , "token" + , fun () -> show_token_dialog (get_stored_token ()); - Lwt.return_unit); - [%i"Sync workspace"], "sync", (fun () -> - catch_with_alert @@ fun () -> - sync () >>= fun _ -> Lwt.return_unit); - [%i"Export to file"], "download", download_save; - [%i"Import"], "upload", import_save; - [%i"Download all source files"], "download", download_all; - [%i"Logout"], "logout", - (fun () -> Lwt.async logout_dialog; Lwt.return_unit); - ]; - begin button - ~container:El.toolbar - ~theme:"white" ~icon: "menu" [%i"Menu"] @@ fun () -> - menu_hidden := not !menu_hidden ; - if !menu_hidden then - Manip.addClass El.menu "hidden" - else - Manip.removeClass El.menu "hidden" ; - Lwt.return () - end ; - begin - (try - let nickname = Learnocaml_local_storage.(retrieve nickname) in - (Tyxml_js.To_dom.of_input El.nickname_field)##.value := Js.string nickname; - with Not_found -> ()); + Lwt.return_unit ) + ; ( [%i "Sync workspace"] + , "sync" + , fun () -> + catch_with_alert @@ fun () -> sync () >>= fun _ -> Lwt.return_unit ) + ; ([%i "Export to file"], "download", download_save) + ; ([%i "Import"], "upload", import_save) + ; ([%i "Download all source files"], "download", download_all) + ; ( [%i "Logout"] + , "logout" + , fun () -> Lwt.async logout_dialog; Lwt.return_unit ) ]; + ( button ~container:El.toolbar ~theme:"white" ~icon:"menu" [%i "Menu"] + @@ fun () -> + menu_hidden := not !menu_hidden; + if !menu_hidden then Manip.addClass El.menu "hidden" + else Manip.removeClass El.menu "hidden"; + Lwt.return () ); + ( ( try + let nickname = Learnocaml_local_storage.(retrieve nickname) in + (Tyxml_js.To_dom.of_input El.nickname_field)##.value + := Js.string nickname + with Not_found -> () ); let save_nickname () = - Learnocaml_local_storage.(store nickname) @@ - Js.to_string (Tyxml_js.To_dom.of_input El.nickname_field)##.value + Learnocaml_local_storage.(store nickname) + @@ Js.to_string (Tyxml_js.To_dom.of_input El.nickname_field)##.value in Manip.Ev.onreturn El.nickname_field (fun _ -> save_nickname ()); - Manip.Ev.onblur El.nickname_field (fun _ -> save_nickname (); true); - end ; + Manip.Ev.onblur El.nickname_field (fun _ -> save_nickname (); true) ); Manip.Ev.onclick El.hide_panel (fun _ -> Manip.SetCss.display El.toolbar "none"; Manip.SetCss.display El.menu "none"; Manip.SetCss.left El.content "0"; Manip.SetCss.display El.show_panel "block"; - true); + true ); Manip.Ev.onclick El.show_panel (fun _ -> let xset elt f = Js.Opt.iter (Dom_html.CoerceTo.element (H.toelt elt)) f @@ -854,15 +913,14 @@ let () = xset El.menu (fun s -> s##.style##.display := Js.string ""); xset El.content (fun s -> s##.style##.left := Js.string ""); Manip.SetCss.display El.show_panel "none"; - true); - Server_caller.request (Learnocaml_api.Version ()) >>= - (function - | Ok _ -> init_sync_token sync_button_group >|= init_tabs - | Error _ -> Lwt.return (init_tabs None)) >>= fun tabs -> + true ); + Server_caller.request (Learnocaml_api.Version ()) + >>= (function + | Ok _ -> init_sync_token sync_button_group >|= init_tabs + | Error _ -> Lwt.return (init_tabs None)) + >>= fun tabs -> try let activity = arg "activity" in - let (_, select) = List.assoc activity tabs in + let _, select = List.assoc activity tabs in select () - with Not_found -> - no_tab_selected (); - Lwt.return () + with Not_found -> no_tab_selected (); Lwt.return () diff --git a/src/app/learnocaml_local_storage.ml b/src/app/learnocaml_local_storage.ml index d2e5e0d66..4b9770aed 100644 --- a/src/app/learnocaml_local_storage.ml +++ b/src/app/learnocaml_local_storage.ml @@ -10,12 +10,12 @@ open Js_of_ocaml open Learnocaml_data type 'a storage_key = - { key : string option ; - dependent_keys : string -> bool ; - store : 'a -> unit ; - retrieve : unit -> 'a ; - delete : unit -> unit ; - mutable listeners : ('a option -> unit) option ref list } + { key : string option + ; dependent_keys : string -> bool + ; store : 'a -> unit + ; retrieve : unit -> 'a + ; delete : unit -> unit + ; mutable listeners : ('a option -> unit) option ref list } type any_key = Key : _ storage_key -> any_key @@ -27,67 +27,61 @@ let notify = function keys_with_listeners := List.fold_left (fun acc (Key storage_key) -> - if storage_key.dependent_keys key then - let listeners = - List.fold_left - (fun acc listener -> - match !listener with - | None -> acc - | Some cb -> - let acc, value = match acc with - | (acc, None) -> - let value = - try Some (storage_key.retrieve ()) - with Not_found -> None in - acc, value - | (acc, Some value) -> acc, value in - cb value ; - (listener :: acc, Some value)) - ([], None) storage_key.listeners - |> fst in - storage_key.listeners <- listeners ; - if listeners = [] then - acc - else (Key storage_key) :: acc - else (Key storage_key) :: acc) + if storage_key.dependent_keys key then ( + let listeners = + List.fold_left + (fun acc listener -> + match !listener with + | None -> acc + | Some cb -> + let acc, value = + match acc with + | acc, None -> + let value = + try Some (storage_key.retrieve ()) + with Not_found -> None + in + (acc, value) + | acc, Some value -> (acc, value) + in + cb value; + (listener :: acc, Some value) ) + ([], None) storage_key.listeners + |> fst + in + storage_key.listeners <- listeners; + if listeners = [] then acc else Key storage_key :: acc ) + else Key storage_key :: acc ) [] !keys_with_listeners let init () = - let storage_event_typ - : Dom_html.storageEvent Js.t Dom_html.Event.typ - = Dom_html.Event.make "storage" in - Dom_html.addEventListener - Dom_html.window storage_event_typ + let storage_event_typ : Dom_html.storageEvent Js.t Dom_html.Event.typ = + Dom_html.Event.make "storage" + in + Dom_html.addEventListener Dom_html.window storage_event_typ (Dom_html.handler (fun evt -> - Js.Opt.case (evt##.key) + Js.Opt.case evt##.key (fun () -> Js._false) (fun name -> - let name = Js.to_string name in - notify (Some name) ; - Js._true))) - Js._true |> ignore + let name = Js.to_string name in + notify (Some name); Js._true ) )) + Js._true + |> ignore -let store { store ; key ; _ } v = - store v ; - notify key +let store {store; key; _} v = store v; notify key -let retrieve { retrieve ; _ } = - retrieve () +let retrieve {retrieve; _} = retrieve () -let delete { delete ; key ; _ } = - delete () ; - notify key +let delete {delete; key; _} = delete (); notify key let listener key = let listener = ref None in - if key.listeners = [] then begin - keys_with_listeners := Key key :: !keys_with_listeners ; - end ; - key.listeners <- listener :: key.listeners ; + if key.listeners = [] then + keys_with_listeners := Key key :: !keys_with_listeners; + key.listeners <- listener :: key.listeners; listener -let mangle parts = - String.concat ":" ("learnocaml" :: parts) +let mangle parts = String.concat ":" ("learnocaml" :: parts) let demangle str = match Stringext.split ~on:':' str with @@ -96,105 +90,126 @@ let demangle str = let store_single name enc value = Js.Optdef.case - (Dom_html.window##.localStorage) + Dom_html.window##.localStorage (fun () -> failwith "local storage support required") (fun localStorage -> - let json = Json_repr_browser.Json_encoding.construct enc value in - localStorage##(setItem (Js.string name) (Js._JSON##(stringify json)))) + let json = Json_repr_browser.Json_encoding.construct enc value in + localStorage ## (setItem (Js.string name) Js._JSON ## (stringify json)) + ) let retrieve_single ?default name enc () = Js.Optdef.case - (Dom_html.window##.localStorage) + Dom_html.window##.localStorage (fun () -> failwith "local storage support required") (fun localStorage -> - Js.Opt.case - (localStorage##(getItem (Js.string name))) - (fun () -> - match default with - | Some default -> default - | None -> raise Not_found) - (fun v -> - let open Json_repr_browser.Json_encoding in - try - destruct enc (Js._JSON##(parse v)) - with exn -> - raise (Json_encoding.Cannot_destruct - ([ `Field "localStorage" ; `Field name ], exn)))) + Js.Opt.case + localStorage ## (getItem (Js.string name)) + (fun () -> + match default with + | Some default -> default + | None -> raise Not_found ) + (fun v -> + let open Json_repr_browser.Json_encoding in + try destruct enc Js._JSON ## (parse v) with exn -> + raise + (Json_encoding.Cannot_destruct + ([`Field "localStorage"; `Field name], exn)) ) ) let delete_single name _enc () = Js.Optdef.case - (Dom_html.window##.localStorage) + Dom_html.window##.localStorage (fun () -> failwith "local storage support required") - (fun localStorage -> - localStorage##(removeItem (Js.string name))) + (fun localStorage -> localStorage ## (removeItem (Js.string name))) let clear () = - Js.Optdef.iter - (Dom_html.window##.localStorage) - (fun localStorage -> localStorage##clear) + Js.Optdef.iter Dom_html.window##.localStorage (fun localStorage -> + localStorage##clear ) let server_id = - let key = mangle [ "server_id" ] + let key = mangle ["server_id"] and enc = Json_encoding.(obj1 (req "server_id" int)) in let store = store_single key enc and retrieve = retrieve_single key enc and delete = delete_single key enc in - { key = Some key ; dependent_keys = (=) key ; - store ; retrieve ; delete ; listeners = [] } + { key = Some key + ; dependent_keys = ( = ) key + ; store + ; retrieve + ; delete + ; listeners = [] } let sync_token = - let key = mangle [ "sync-token" ] in + let key = mangle ["sync-token"] in let enc = Json_encoding.(obj1 (req "token" string)) in let store value = store_single key enc (Token.to_string value) and retrieve () = retrieve_single key enc () |> Token.parse and delete () = delete_single key enc () in - { key = Some key ; dependent_keys = (=) key ; - store ; retrieve ; delete ; listeners = [] } + { key = Some key + ; dependent_keys = ( = ) key + ; store + ; retrieve + ; delete + ; listeners = [] } let nickname = - let key = mangle [ "nickname" ] in + let key = mangle ["nickname"] in let enc = Json_encoding.(obj1 (req "nickname" string)) in let store value = store_single key enc value - and retrieve () = - try retrieve_single key enc () with Not_found -> "" + and retrieve () = try retrieve_single key enc () with Not_found -> "" and delete () = delete_single key enc () in - { key = Some key ; dependent_keys = (=) key ; - store ; retrieve ; delete ; listeners = [] } + { key = Some key + ; dependent_keys = ( = ) key + ; store + ; retrieve + ; delete + ; listeners = [] } let cached_exercise name = - let key = mangle [ "cached-exercise" ; name ] in + let key = mangle ["cached-exercise"; name] in let enc = Learnocaml_exercise.enc in let store value = store_single key enc value and retrieve () = retrieve_single key enc () and delete () = delete_single key enc () in - { key = Some key ; dependent_keys = (=) key ; - store ; retrieve ; delete ; listeners = [] } + { key = Some key + ; dependent_keys = ( = ) key + ; store + ; retrieve + ; delete + ; listeners = [] } let listed list_key item_prefix ?default enc = let list = let key = mangle list_key in let enc = Json_encoding.(list string) in let store value = store_single key enc value - and retrieve () = retrieve_single ~default: [] key enc () + and retrieve () = retrieve_single ~default:[] key enc () and delete () = delete_single key enc () in - { key = Some key ; dependent_keys = (=) key ; - store ; retrieve ; delete ; listeners = [] } in + { key = Some key + ; dependent_keys = ( = ) key + ; store + ; retrieve + ; delete + ; listeners = [] } + in let item name = - let key = mangle (item_prefix @ [ name ]) in + let key = mangle (item_prefix @ [name]) in let store value = - store_single key enc value ; + store_single key enc value; let all = retrieve list in - if not (List.mem name all) then - store list (name :: all) - and retrieve () = - retrieve_single ?default key enc () + if not (List.mem name all) then store list (name :: all) + and retrieve () = retrieve_single ?default key enc () and delete () = - delete_single key enc () ; + delete_single key enc (); let all = retrieve list in - if List.mem name all then - store list (List.filter ((<>) name) all) in - { key = Some key ; dependent_keys = (=) key ; - store ; retrieve ; delete ; listeners = [] } in + if List.mem name all then store list (List.filter (( <> ) name) all) + in + { key = Some key + ; dependent_keys = ( = ) key + ; store + ; retrieve + ; delete + ; listeners = [] } + in let assoc = let retrieve () = try @@ -204,52 +219,49 @@ let listed list_key item_prefix ?default enc = with Not_found -> SMap.empty and delete () = let all = retrieve list in - List.iter (fun name -> delete (item name)) all ; - delete list in + List.iter (fun name -> delete (item name)) all; + delete list + in let store index = - delete () ; + delete (); let all = SMap.fold (fun name state acc -> - store (item name) state ; - name :: acc) - index [] in - store list all in + store (item name) state; + name :: acc ) + index [] + in + store list all + in let dependent_keys name = let name = demangle name in - name = list_key || - let rec is_prefix p l = match (p, l) with - | [], [ _ ] -> true + name = list_key + || + let rec is_prefix p l = + match (p, l) with + | [], [_] -> true | [], _ | _, [] -> false - | pw :: p, lw :: l -> - pw = lw && is_prefix p l in - is_prefix item_prefix name in - { key = None ; dependent_keys ; - store ; retrieve ; delete ; listeners = [] } in - list, item, assoc - -let exercise_list, - exercise_state, - all_exercise_states = - listed - [ "exercise-state-list" ] - [ "exercise-state" ] - Answer.enc + | pw :: p, lw :: l -> pw = lw && is_prefix p l + in + is_prefix item_prefix name + in + {key = None; dependent_keys; store; retrieve; delete; listeners = []} + in + (list, item, assoc) -let toplevel_history_list, - toplevel_history, - all_toplevel_histories = - listed - [ "toplevel-history-list" ] - [ "toplevel-history" ] - ~default: Learnocaml_toplevel_history.empty_snapshot +let exercise_list, exercise_state, all_exercise_states = + listed ["exercise-state-list"] ["exercise-state"] Answer.enc + +let toplevel_history_list, toplevel_history, all_toplevel_histories = + listed ["toplevel-history-list"] ["toplevel-history"] + ~default:Learnocaml_toplevel_history.empty_snapshot Learnocaml_toplevel_history.snapshot_enc -let exercise_toplevel_history_list, - exercise_toplevel_history, - all_exercise_toplevel_histories = +let ( exercise_toplevel_history_list + , exercise_toplevel_history + , all_exercise_toplevel_histories ) = listed - [ "exercise-toplevel-history-list" ] - [ "exercise-toplevel-history" ] - ~default: Learnocaml_toplevel_history.empty_snapshot + ["exercise-toplevel-history-list"] + ["exercise-toplevel-history"] + ~default:Learnocaml_toplevel_history.empty_snapshot Learnocaml_toplevel_history.snapshot_enc diff --git a/src/app/learnocaml_local_storage.mli b/src/app/learnocaml_local_storage.mli index 1755e6342..8c18999cf 100644 --- a/src/app/learnocaml_local_storage.mli +++ b/src/app/learnocaml_local_storage.mli @@ -30,17 +30,21 @@ val exercise_state : string -> Answer.t storage_key val all_exercise_states : Answer.t SMap.t storage_key -val exercise_toplevel_history : string -> Learnocaml_toplevel_history.snapshot storage_key +val exercise_toplevel_history : + string -> Learnocaml_toplevel_history.snapshot storage_key val exercise_toplevel_history_list : string list storage_key -val all_exercise_toplevel_histories : Learnocaml_toplevel_history.snapshot SMap.t storage_key +val all_exercise_toplevel_histories : + Learnocaml_toplevel_history.snapshot SMap.t storage_key val toplevel_history_list : string list storage_key -val toplevel_history : string -> Learnocaml_toplevel_history.snapshot storage_key +val toplevel_history : + string -> Learnocaml_toplevel_history.snapshot storage_key -val all_toplevel_histories : Learnocaml_toplevel_history.snapshot SMap.t storage_key +val all_toplevel_histories : + Learnocaml_toplevel_history.snapshot SMap.t storage_key val server_id : int storage_key diff --git a/src/app/learnocaml_partition_view.ml b/src/app/learnocaml_partition_view.ml index 0913afd90..329ba776c 100644 --- a/src/app/learnocaml_partition_view.ml +++ b/src/app/learnocaml_partition_view.ml @@ -11,94 +11,89 @@ open Js_utils open Lwt open Learnocaml_data open Learnocaml_common - module H = Tyxml_js.Html5 module React = Lwt_react -let find_tab name = (find_component ("learnocaml-exo-tab-" ^ name)) +let find_tab name = find_component ("learnocaml-exo-tab-" ^ name) let tab_select_signal, init_tab, select_tab = let init_tab, select_tab = mk_tab_handlers "details" ["list"; "answer"] in - let tab_select_signal, tab_select_signal_set = - React.S.create "details" in + let tab_select_signal, tab_select_signal_set = React.S.create "details" in let select_tab str = tab_select_signal_set str; select_tab str in - tab_select_signal, init_tab, select_tab + (tab_select_signal, init_tab, select_tab) let update_answer_tab, clear_answer_tab = ace_display (find_tab "answer") let selected_class_signal, set_selected_class = React.S.create None -let selected_repr_signal, set_selected_repr = React.S.create None + +let selected_repr_signal, set_selected_repr = React.S.create None let open_tok tok = - let _win = window_open ("/student-view.html?token="^tok) "_blank" in + let _win = window_open ("/student-view.html?token=" ^ tok) "_blank" in false let list_of_tok = - List.map @@ fun x -> - let tok = Token.to_string x in - H.a ~a:[H.a_onclick (fun _ -> open_tok tok)] [H.txt (tok ^ " ")] + List.map + @@ fun x -> + let tok = Token.to_string x in + H.a ~a:[H.a_onclick (fun _ -> open_tok tok)] [H.txt (tok ^ " ")] let rec render_tree = let open Asak.Wtree in function | Leaf xs -> - [H.p ~a:[ H.a_onclick (fun _ -> - set_selected_class (Some xs); true)] - [H.txt ("Leaf with " ^ string_of_int (List.length xs) ^ " student(s)")]] - | Node (f,l,r) -> - [ - H.p [ H.txt ("Node " ^ string_of_int f) ] - ; H.ul [ - H.li (render_tree l) - ; H.li (render_tree r) - ] - ] + [ H.p + ~a: + [ H.a_onclick (fun _ -> + set_selected_class (Some xs); + true ) ] + [ H.txt + ("Leaf with " ^ string_of_int (List.length xs) ^ " student(s)") + ] ] + | Node (f, l, r) -> + [ H.p [H.txt ("Node " ^ string_of_int f)] + ; H.ul [H.li (render_tree l); H.li (render_tree r)] ] let weight_of_tree t = Asak.Wtree.fold_tree (fun _ -> ( + )) t let render_trees xs = - let aux (i,acc) t = + let aux (i, acc) t = let str = - "Class n°" ^ string_of_int i - ^ " (" ^ string_of_int (weight_of_tree List.length t) ^ " students)" in - i+1, - H.li - ( H.txt str - :: render_tree t) - :: acc + "Class n°" ^ string_of_int i ^ " (" + ^ string_of_int (weight_of_tree List.length t) + ^ " students)" + in + (i + 1, H.li (H.txt str :: render_tree t) :: acc) in - List.rev @@ snd @@ List.fold_left aux (1,[]) xs + List.rev @@ snd @@ List.fold_left aux (1, []) xs let render_classes xs = - let aux (grade,values) acc = + let aux (grade, values) acc = let str = string_of_int grade ^ "pts :" in - H.p [H.txt str] :: - H.ul (render_trees values) :: - acc - in List.fold_right aux xs [] + H.p [H.txt str] :: H.ul (render_trees values) :: acc + in + List.fold_right aux xs [] let sum_with f = List.fold_left (fun acc x -> acc + f x) 0 let exercises_tab part = let open Partition in let not_graded = - string_of_int (List.length part.not_graded) - ^ " codes were not graded: " in + string_of_int (List.length part.not_graded) ^ " codes were not graded: " + in let bad_type = - string_of_int (List.length part.bad_type) - ^ " codes had the wrong type: " in + string_of_int (List.length part.bad_type) ^ " codes had the wrong type: " + in let total_sum = let s = sum_with - (fun (_,x) -> - sum_with (fun x -> weight_of_tree List.length x) - x - ) - part.partition_by_grade in - string_of_int s - ^ " codes implemented the function with the right type." in + (fun (_, x) -> sum_with (fun x -> weight_of_tree List.length x) x) + part.partition_by_grade + in + string_of_int s ^ " codes implemented the function with the right type." + in H.p (H.txt not_graded :: list_of_tok part.not_graded) - :: H.p ( H.txt bad_type :: list_of_tok part.bad_type) + :: H.p (H.txt bad_type :: list_of_tok part.bad_type) :: H.p [H.txt total_sum] :: render_classes part.partition_by_grade @@ -106,38 +101,43 @@ let _class_selection_updater = let previous = ref None in let of_repr repr = [H.code [H.txt repr]] in let onclick p tok repr = - H.a_onclick @@ - fun _ -> - (match !previous with - | None -> () - | Some prev -> Manip.replaceChildren prev []); - previous := Some p; - Manip.replaceChildren p (of_repr repr); - set_selected_repr (Some (tok,repr)); - true in + H.a_onclick + @@ fun _ -> + ( match !previous with + | None -> () + | Some prev -> Manip.replaceChildren prev [] ); + previous := Some p; + Manip.replaceChildren p (of_repr repr); + set_selected_repr (Some (tok, repr)); + true + in let to_li tok repr p = let strtok = Token.to_string tok in H.li - ~a:[ onclick p tok repr ; H.a_ondblclick (fun _ -> open_tok strtok)] - [H.txt strtok; p] in - let mkfirst (tok,repr) = - let p = H.p (of_repr repr) in + ~a:[onclick p tok repr; H.a_ondblclick (fun _ -> open_tok strtok)] + [H.txt strtok; p] + in + let mkfirst (tok, repr) = + let p = H.p (of_repr repr) in previous := Some p; - to_li tok repr p in - let mkelem (tok,repr) = - to_li tok repr @@ H.p [] + to_li tok repr p in - selected_class_signal |> React.S.map @@ fun id -> - match id with - | None -> () - | Some xs -> - set_selected_repr (Some (List.hd xs)); - Manip.replaceChildren (find_tab "details") - [H.ul @@ mkfirst (List.hd xs) :: List.map mkelem (List.tl xs)] + let mkelem (tok, repr) = to_li tok repr @@ H.p [] in + selected_class_signal + |> React.S.map + @@ fun id -> + match id with + | None -> () + | Some xs -> + set_selected_repr (Some (List.hd xs)); + Manip.replaceChildren (find_tab "details") + [H.ul @@ (mkfirst (List.hd xs) :: List.map mkelem (List.tl xs))] let main () = Learnocaml_local_storage.init (); - (match Js_utils.get_lang() with Some l -> Ocplib_i18n.set_lang l | None -> ()); + ( match Js_utils.get_lang () with + | Some l -> Ocplib_i18n.set_lang l + | None -> () ); set_string_translations_view (); let teacher_token = Learnocaml_local_storage.(retrieve sync_token) in if not (Token.is_teacher teacher_token) then @@ -145,41 +145,44 @@ let main () = registered server-side *) failwith "The page you are trying to access is for teachers only"; let exercise_id = - try (List.assoc "id" Url.Current.arguments) - with Not_found -> failwith "Exercise id is missing" in + try List.assoc "id" Url.Current.arguments with Not_found -> + failwith "Exercise id is missing" + in let fun_id = - try (List.assoc "function" Url.Current.arguments) - with Not_found -> failwith "function name is missing" in + try List.assoc "function" Url.Current.arguments with Not_found -> + failwith "function name is missing" + in let prof = - try (int_of_string @@ List.assoc "prof" Url.Current.arguments) - with Not_found | Failure _ -> failwith "prof is missing or malformed" in - + try int_of_string @@ List.assoc "prof" Url.Current.arguments with + | Not_found | Failure _ -> failwith "prof is missing or malformed" + in let update_repr_code = function | None -> true - | Some (tok,_) -> - Lwt.async (fun () -> - retrieve (Learnocaml_api.Fetch_save tok) - >|= fun save -> - match SMap.find_opt exercise_id save.Save.all_exercise_states with - | None -> () - | Some x -> - update_answer_tab x.Answer.solution); - true in - + | Some (tok, _) -> + Lwt.async (fun () -> + retrieve (Learnocaml_api.Fetch_save tok) + >|= fun save -> + match SMap.find_opt exercise_id save.Save.all_exercise_states with + | None -> () + | Some x -> update_answer_tab x.Answer.solution ); + true + in let _repr_selection_updater = - selected_repr_signal |> React.S.map @@ fun id -> - let tab = React.S.value tab_select_signal in - if tab = "answer" - then update_repr_code id - else true in - - retrieve (Learnocaml_api.Partition (teacher_token, exercise_id, fun_id, prof)) + selected_repr_signal + |> React.S.map + @@ fun id -> + let tab = React.S.value tab_select_signal in + if tab = "answer" then update_repr_code id else true + in + retrieve + (Learnocaml_api.Partition (teacher_token, exercise_id, fun_id, prof)) >>= fun part -> hide_loading ~id:"learnocaml-exo-loading" (); Manip.replaceChildren (find_tab "list") (exercises_tab part); init_tab (); - Manip.Ev.onclick (find_component "learnocaml-exo-button-answer") - (fun _ -> select_tab "answer"; update_repr_code (React.S.value selected_repr_signal)); + Manip.Ev.onclick (find_component "learnocaml-exo-button-answer") (fun _ -> + select_tab "answer"; + update_repr_code (React.S.value selected_repr_signal) ); Lwt.return_unit -let () = run_async_with_log main +let () = run_async_with_log main diff --git a/src/app/learnocaml_playground_main.ml b/src/app/learnocaml_playground_main.ml index 19edd7830..2a4478f78 100644 --- a/src/app/learnocaml_playground_main.ml +++ b/src/app/learnocaml_playground_main.ml @@ -12,77 +12,84 @@ open Lwt.Infix open Learnocaml_common open Learnocaml_data open Learnocaml_config - module H = Tyxml_js.Html -let init_tabs, select_tab = - mk_tab_handlers "toplevel" ["editor"] +let init_tabs, select_tab = mk_tab_handlers "toplevel" ["editor"] let main () = set_string_translations_exercises (); - Learnocaml_local_storage.init () ; + Learnocaml_local_storage.init (); (* ---- launch everything --------------------------------------------- *) let toplevel_buttons_group = button_group () in - disable_button_group toplevel_buttons_group (* enabled after init *) ; + disable_button_group toplevel_buttons_group (* enabled after init *); let toplevel_toolbar = find_component "learnocaml-exo-toplevel-toolbar" in let editor_toolbar = find_component "learnocaml-exo-editor-toolbar" in let toplevel_button = - button ~container: toplevel_toolbar ~theme: "dark" ~group:toplevel_buttons_group ?state:None in - let id = match Url.Current.path with + button ~container:toplevel_toolbar ~theme:"dark" + ~group:toplevel_buttons_group ?state:None + in + let id = + match Url.Current.path with | "" :: "playground" :: p | "playground" :: p -> - String.concat "/" (List.map Url.urldecode (List.filter ((<>) "") p)) + String.concat "/" (List.map Url.urldecode (List.filter (( <> ) "") p)) | _ -> arg "id" in Dom_html.document##.title := - Js.string (id ^ " - " ^ "Learn OCaml" ^" v."^ Learnocaml_api.version); + Js.string (id ^ " - " ^ "Learn OCaml" ^ " v." ^ Learnocaml_api.version); let exercise_fetch = retrieve (Learnocaml_api.Playground id) in let after_init top = - exercise_fetch >>= fun playground -> + exercise_fetch + >>= fun playground -> Learnocaml_toplevel.load ~print_outcome:true top - ~message: [%i"loading the prelude..."] - playground.Playground.prelude + ~message:[%i "loading the prelude..."] playground.Playground.prelude >>= fun r1 -> - if not r1 then failwith [%i"error in prelude"] ; - Learnocaml_toplevel.set_checking_environment top in + if not r1 then failwith [%i "error in prelude"]; + Learnocaml_toplevel.set_checking_environment top + in let toplevel_launch = - toplevel_launch ~after_init (find_component "learnocaml-exo-toplevel-pane") + toplevel_launch ~after_init + (find_component "learnocaml-exo-toplevel-pane") Learnocaml_local_storage.exercise_toplevel_history - (fun () -> select_tab "toplevel") toplevel_buttons_group id + (fun () -> select_tab "toplevel") + toplevel_buttons_group id in - init_tabs () ; + init_tabs (); set_nickname_div (); - toplevel_launch >>= fun top -> - exercise_fetch >>= fun playground -> + toplevel_launch + >>= fun top -> + exercise_fetch + >>= fun playground -> let solution = - try Learnocaml_local_storage.(retrieve (exercise_state id)).Answer.solution with - | Not_found -> playground.Playground.template in + try Learnocaml_local_storage.(retrieve (exercise_state id)).Answer.solution + with Not_found -> playground.Playground.template + in (* ---- toplevel pane ------------------------------------------------- *) - init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button ; + init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button; (* ---- editor pane --------------------------------------------------- *) let editor, ace = setup_editor solution in - let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in + let module EB = Editor_button (struct + let ace = ace + + let buttons_container = editor_toolbar + end) in EB.cleanup playground.Playground.template; EB.download id; EB.eval top select_tab; setup_prelude_pane ace playground.Playground.prelude; (* ---- main toolbar -------------------------------------------------- *) let exo_toolbar = find_component "learnocaml-exo-toolbar" in - let toolbar_button = button ~container: exo_toolbar ~theme: "light" in - begin toolbar_button - ~icon: "list" [%i"Playground"] @@ fun () -> - Dom_html.window##.location##assign - (Js.string (api_server ^ "/index.html#activity=playground")) ; - Lwt.return () - end ; + let toolbar_button = button ~container:exo_toolbar ~theme:"light" in + ( toolbar_button ~icon:"list" [%i "Playground"] + @@ fun () -> + Dom_html.window##.location##assign + (Js.string (api_server ^ "/index.html#activity=playground")); + Lwt.return () ); let typecheck = typecheck top ace editor in - begin toolbar_button - ~icon: "typecheck" [%i"Compile"] @@ fun () -> - typecheck true - end; + (toolbar_button ~icon:"typecheck" [%i "Compile"] @@ fun () -> typecheck true); Window.onunload (fun _ev -> local_save ace id; true); (* ---- return -------------------------------------------------------- *) - toplevel_launch >>= fun _ -> - typecheck false >|= fun () -> - hide_loading ~id:"learnocaml-exo-loading" () + toplevel_launch + >>= fun _ -> + typecheck false >|= fun () -> hide_loading ~id:"learnocaml-exo-loading" () let () = run_async_with_log main diff --git a/src/app/learnocaml_student_view.ml b/src/app/learnocaml_student_view.ml index 6b116b628..b4efddbf0 100644 --- a/src/app/learnocaml_student_view.ml +++ b/src/app/learnocaml_student_view.ml @@ -11,14 +11,13 @@ open Js_utils open Lwt open Learnocaml_data open Learnocaml_common - module H = Tyxml_js.Html5 module React = Lwt_react module El = struct (** Defines the static elements that should be present from index.html *) - let id s = s, find_component s + let id s = (s, find_component s) let loading_id, loading = id "learnocaml-exo-loading" @@ -29,20 +28,22 @@ module El = struct let tabs_id, tabs = id "learnocaml-exo-tabs" module Tabs = struct - type t = { - name: string; - btn: Html_types.div H.elt; - tab: Html_types.div H.elt; - } - let tid name = { - name; - btn = snd (id ("learnocaml-exo-button-" ^ name)); - tab = snd (id ("learnocaml-exo-tab-" ^ name)); - } + type t = + {name : string; btn : Html_types.div H.elt; tab : Html_types.div H.elt} + + let tid name = + { name + ; btn = snd (id ("learnocaml-exo-button-" ^ name)) + ; tab = snd (id ("learnocaml-exo-tab-" ^ name)) } + let stats = tid "stats" + let list = tid "list" + let report = tid "report" + let editor = tid "editor" + let text = tid "text" let all = [stats; list; report; editor; text] @@ -53,18 +54,15 @@ module El = struct let token_id, token = id "learnocaml-token" module Dyn = struct - let exercise_line_id id = "learnocaml-exercise-line-"^id + let exercise_line_id id = "learnocaml-exercise-line-" ^ id end - end let tab_select_signal, select_tab = let open El.Tabs in let current = ref stats in let cls = "front-tab" in - let tab_select_signal, tab_select_signal_set = - React.S.create !current - in + let tab_select_signal, tab_select_signal_set = React.S.create !current in let select_tab el = let prev = !current in current := el; @@ -76,93 +74,102 @@ let tab_select_signal, select_tab = Manip.disable el.btn; tab_select_signal_set el in - List.iter (fun tab -> - Manip.Ev.onclick tab.btn (fun _ -> select_tab tab; true)) + List.iter + (fun tab -> Manip.Ev.onclick tab.btn (fun _ -> select_tab tab; true)) all; - tab_select_signal, select_tab + (tab_select_signal, select_tab) let selected_exercise_signal, set_selected_exercise = React.S.create None let hl_prereq_signal, set_hl_prereq_signal = React.S.create None + let hl_focus_signal, set_hl_focus_signal = React.S.create None let gather_assignments student_token index status = let status_map = - List.fold_left (fun m ex -> SMap.add ex.Exercise.Status.id ex m) + List.fold_left + (fun m ex -> SMap.add ex.Exercise.Status.id ex m) SMap.empty status in let assignments = get_assignments (Token.Set.singleton student_token) status_map - |> List.filter (fun (_, toks, _, _) -> - Token.Set.mem student_token toks) + |> List.filter (fun (_, toks, _, _) -> Token.Set.mem student_token toks) in let open_exercises = let open Exercise.Status in - SMap.fold (fun id ex acc -> + SMap.fold + (fun id ex acc -> match Token.Map.find_opt student_token ex.assignments.token_map with - | Some Open -> id::acc - | None when ex.assignments.default = Open -> id::acc - | _ -> acc) + | Some Open -> id :: acc + | None when ex.assignments.default = Open -> id :: acc + | _ -> acc ) status_map [] in let assgs = - List.map (fun ((start, stop), _tok, _dft, ids) -> Some (start, stop), ids) - assignments @ - match open_exercises with [] -> [] | l -> [None, SSet.of_list l] + List.map + (fun ((start, stop), _tok, _dft, ids) -> (Some (start, stop), ids)) + assignments + @ match open_exercises with [] -> [] | l -> [(None, SSet.of_list l)] in - List.map (fun (dates, ids) -> + List.map + (fun (dates, ids) -> (* Reorder the exercises in the index order *) - let index = - Exercise.Index.filter (fun id _ -> SSet.mem id ids) index - in - dates, - List.rev @@ Exercise.Index.fold_exercises (fun l id meta -> - if SSet.mem id ids then - let st = SMap.find_opt id status_map in - (id, meta, - (match st with None -> [] | Some st -> - Exercise.Status.skills_prereq meta st), - (match st with None -> [] | Some st -> - Exercise.Status.skills_focus meta st)) - :: l - else l) - [] index) + let index = Exercise.Index.filter (fun id _ -> SSet.mem id ids) index in + ( dates + , List.rev + @@ Exercise.Index.fold_exercises + (fun l id meta -> + if SSet.mem id ids then + let st = SMap.find_opt id status_map in + ( id + , meta + , ( match st with + | None -> [] + | Some st -> Exercise.Status.skills_prereq meta st ) + , match st with + | None -> [] + | Some st -> Exercise.Status.skills_focus meta st ) + :: l + else l ) + [] index ) ) assgs let exercises_tab assignments answers = - let grade_sty grade = - H.a_style ("background-color:"^grade_color grade) - in + let grade_sty grade = H.a_style ("background-color:" ^ grade_color grade) in let exercise_line (id, meta, _, _) ans = - let grade, mtime = match ans with - | None -> None, None - | Some Answer.{grade; mtime; _} -> grade, Some mtime + let grade, mtime = + match ans with + | None -> (None, None) + | Some Answer.({grade; mtime; _}) -> (grade, Some mtime) in let line = - H.tr ~a:[ H.a_id (El.Dyn.exercise_line_id id); - H.a_class ["learnocaml-exercise-line"]; - H.a_onclick (fun _ -> - set_selected_exercise (Some id); - true) ] [ - H.td ~a:[ H.a_class ["exercise-id"] ] [ H.txt id ]; - H.td ~a:[ H.a_class ["exercise-title"] ] - [ H.txt meta.Exercise.Meta.title ]; - H.td ~a:[ H.a_class ["exercise-kind"] ] [ - H.txt (string_of_exercise_kind meta.Exercise.Meta.kind); - ]; - H.td ~a:[ H.a_class ["exercise-stars"] ] - [ stars_div meta.Exercise.Meta.stars ]; - H.td ~a:[ H.a_class ["grade"]; grade_sty grade ] [ - match grade with - | None -> H.txt "" - | Some g -> H.txt (Printf.sprintf "%d%%" g) - ]; - H.td ~a:[ H.a_class ["last-updated"] ] [ - match mtime with - | None -> H.txt "" - | Some t -> date ~time:true t - ]; - ] + H.tr + ~a: + [ H.a_id (El.Dyn.exercise_line_id id) + ; H.a_class ["learnocaml-exercise-line"] + ; H.a_onclick (fun _ -> + set_selected_exercise (Some id); + true ) ] + [ H.td ~a:[H.a_class ["exercise-id"]] [H.txt id] + ; H.td + ~a:[H.a_class ["exercise-title"]] + [H.txt meta.Exercise.Meta.title] + ; H.td + ~a:[H.a_class ["exercise-kind"]] + [H.txt (string_of_exercise_kind meta.Exercise.Meta.kind)] + ; H.td + ~a:[H.a_class ["exercise-stars"]] + [stars_div meta.Exercise.Meta.stars] + ; H.td + ~a:[H.a_class ["grade"]; grade_sty grade] + [ ( match grade with + | None -> H.txt "" + | Some g -> H.txt (Printf.sprintf "%d%%" g) ) ] + ; H.td + ~a:[H.a_class ["last-updated"]] + [ ( match mtime with + | None -> H.txt "" + | Some t -> date ~time:true t ) ] ] in let cls = "exercise-highlight" in let prereq_sigs = @@ -181,27 +188,29 @@ let exercises_tab assignments answers = | _ -> Manip.removeClass line cls) hl_focus_signal in - line, (prereq_sigs, focus_sigs) + (line, (prereq_sigs, focus_sigs)) in let[@warning "-3"] assg_lines = (* tyxml_js marks a_scope as deprecated in HTML5, which is wrong: it's deprecated for but not for . *) let now = gettimeofday () in - List.map (fun (assg, ids) -> + List.map + (fun (assg, ids) -> let states = List.map (fun (id, _, _, _) -> SMap.find_opt id answers) ids in let avg_grade, mtime = let tot, n, mtime = - List.fold_left (fun (tot, n, tm) -> function - | Some Answer.{grade = Some g; mtime; _} -> - tot + g, n + 1, max mtime tm - | Some Answer.{mtime; _} -> tot, n+1, max mtime tm - | _ -> tot, n + 1, tm) + List.fold_left + (fun (tot, n, tm) -> function + | Some Answer.({grade = Some g; mtime; _}) -> + (tot + g, n + 1, max mtime tm) + | Some Answer.({mtime; _}) -> (tot, n + 1, max mtime tm) + | _ -> (tot, n + 1, tm) ) (0, 0, 0.) states in - (if n = 0 then 0. else float_of_int tot /. float_of_int n), - (if mtime = 0. then None else Some mtime) + ( (if n = 0 then 0. else float_of_int tot /. float_of_int n) + , if mtime = 0. then None else Some mtime ) in let lines, sighandlers = List.split (List.map2 exercise_line ids states) @@ -209,35 +218,28 @@ let exercises_tab assignments answers = let text = match assg with | Some (start, _) when start > now -> - [H.txt [%i"Future assignment (starting "]; - date start; - H.txt ")"] + [H.txt [%i "Future assignment (starting "]; date start; H.txt ")"] | Some (_, stop) when stop < now -> - [H.txt [%i"Terminated assignment ("]; - date stop; - H.txt ")"] + [H.txt [%i "Terminated assignment ("]; date stop; H.txt ")"] | Some (_, stop) -> - [H.txt [%i"Ongoing assignment (due "]; - date stop; - H.txt ")"] - | None -> - [H.txt [%i"Open exercises"]]; + [H.txt [%i "Ongoing assignment (due "]; date stop; H.txt ")"] + | None -> [H.txt [%i "Open exercises"]] in - H.tr ~a:[ H.a_class ["learnocaml-assignment-line"]; - grade_sty (Some (int_of_float avg_grade)) ] [ - H.th ~a:[ H.a_scope `Rowgroup; H.a_colspan 4 ] text; - H.th ~a:[ H.a_scope `Rowgroup; - H.a_class ["grade"] ] [ - H.txt (Printf.sprintf "%01.1f%%" avg_grade) - ]; - H.th ~a:[ H.a_scope `Rowgroup; - H.a_class ["last-updated"] ] [ - match mtime with Some t -> date ~time:true t | None -> H.txt ""; - ]; - ] :: - lines, - sighandlers - ) + ( H.tr + ~a: + [ H.a_class ["learnocaml-assignment-line"] + ; grade_sty (Some (int_of_float avg_grade)) ] + [ H.th ~a:[H.a_scope `Rowgroup; H.a_colspan 4] text + ; H.th + ~a:[H.a_scope `Rowgroup; H.a_class ["grade"]] + [H.txt (Printf.sprintf "%01.1f%%" avg_grade)] + ; H.th + ~a:[H.a_scope `Rowgroup; H.a_class ["last-updated"]] + [ ( match mtime with + | Some t -> date ~time:true t + | None -> H.txt "" ) ] ] + :: lines + , sighandlers ) ) assignments in match assg_lines with @@ -251,100 +253,103 @@ let stats_tab assignments answers = try let tot, count = SMap.find key m in SMap.add key (n + tot, count + 1) m - with Not_found -> - SMap.add key (n, 1) m + with Not_found -> SMap.add key (n, 1) m in let total_grade, n_attempted, n_total, by_prereq, by_focus = List.fold_left (fun acc (_dates, exercises) -> - List.fold_left - (fun - (total_grade, n_attempted, n_total, by_prereq, by_focus) - (id, _meta, prereq, focus) -> - match SMap.find_opt id answers with - | None -> - (total_grade, n_attempted, n_total + 1, - List.fold_left (smap_add 0) by_prereq prereq, - List.fold_left (smap_add 0) by_focus focus) - | Some a -> - let g = match a.Answer.grade with None -> 0 | Some g -> g in - total_grade + g, - n_attempted + 1, - n_total + 1, - List.fold_left (smap_add g) by_prereq prereq, - List.fold_left (smap_add g) by_focus focus) - acc exercises) + List.fold_left + (fun (total_grade, n_attempted, n_total, by_prereq, by_focus) + (id, _meta, prereq, focus) -> + match SMap.find_opt id answers with + | None -> + ( total_grade + , n_attempted + , n_total + 1 + , List.fold_left (smap_add 0) by_prereq prereq + , List.fold_left (smap_add 0) by_focus focus ) + | Some a -> + let g = match a.Answer.grade with None -> 0 | Some g -> g in + ( total_grade + g + , n_attempted + 1 + , n_total + 1 + , List.fold_left (smap_add g) by_prereq prereq + , List.fold_left (smap_add g) by_focus focus ) ) + acc exercises ) (0, 0, 0, SMap.empty, SMap.empty) assignments in - let item ?(indent=0) ?(fmt = H.txt) lbl title v = - H.tr ~a:[H.a_title title] [ - H.td ~a:[H.a_class ["stats-label"]; - H.a_style ("padding-left:"^string_of_int (indent * 8)^"px")] - [fmt lbl]; - H.td v - ] + let item ?(indent = 0) ?(fmt = H.txt) lbl title v = + H.tr ~a:[H.a_title title] + [ H.td + ~a: + [ H.a_class ["stats-label"] + ; H.a_style ("padding-left:" ^ string_of_int (indent * 8) ^ "px") + ] + [fmt lbl] + ; H.td v ] in let pct x y = let cls = H.a_class ["grade"; "stats-pct"] in if y = 0 then - H.div ~a:[cls; H.a_style ("background-color:"^grade_color None)] + H.div + ~a:[cls; H.a_style ("background-color:" ^ grade_color None)] [H.txt "--%"] else - let r = 100. *. float_of_int x /. float_of_int y in - let color = grade_color (Some (int_of_float r)) in - let background = - Printf.sprintf "background:linear-gradient(to right,\ - %s 0%%,%s %.0f%%,transparent %.0f%%)" - color color r r - in - H.div ~a:[H.a_class ["grade"; "stats-pct"]; - H.a_style background] - [H.txt (Printf.sprintf "%02.1f%%" r)] - in [ - H.h3 [H.txt [%i"Student stats"]]; - H.table ~a:[H.a_class ["student-stats"]] begin - [ - item [%i"completion"] - [%i"The average grade over all accessible exercises"] - [pct total_grade (100 * n_total)]; - item [%i"attempted"] - [%i"The amount of accessible exercises that have been attempted"] - [pct n_attempted n_total]; - item [%i"success"] - [%i"The average grade over attempted exercises"] - [pct total_grade (100 * n_attempted)]; - ] - @ - (if SMap.is_empty by_focus then [] else - H.tr [H.th ~a:[H.a_colspan 2] - [H.txt [%i"success over exercises training skills"]]] :: - List.map (fun (sk, (tot, count)) -> - let i = - item ~indent:1 ~fmt:tag_span sk - ([%i"Success over exercises training skill "]^sk) - [pct tot (100 * count)] - in - mouseover_toggle_signal i sk set_hl_focus_signal; - i - ) - (SMap.bindings by_focus)) + let r = 100. *. float_of_int x /. float_of_int y in + let color = grade_color (Some (int_of_float r)) in + let background = + Printf.sprintf + "background:linear-gradient(to right,%s 0%%,%s %.0f%%,transparent \ + %.0f%%)" + color color r r + in + H.div + ~a:[H.a_class ["grade"; "stats-pct"]; H.a_style background] + [H.txt (Printf.sprintf "%02.1f%%" r)] + in + [ H.h3 [H.txt [%i "Student stats"]] + ; H.table + ~a:[H.a_class ["student-stats"]] + ( [ item [%i "completion"] + [%i "The average grade over all accessible exercises"] + [pct total_grade (100 * n_total)] + ; item [%i "attempted"] + [%i "The amount of accessible exercises that have been attempted"] + [pct n_attempted n_total] + ; item [%i "success"] [%i "The average grade over attempted exercises"] + [pct total_grade (100 * n_attempted)] ] + @ ( if SMap.is_empty by_focus then [] + else + H.tr + [ H.th ~a:[H.a_colspan 2] + [H.txt [%i "success over exercises training skills"]] ] + :: List.map + (fun (sk, (tot, count)) -> + let i = + item ~indent:1 ~fmt:tag_span sk + ([%i "Success over exercises training skill "] ^ sk) + [pct tot (100 * count)] + in + mouseover_toggle_signal i sk set_hl_focus_signal; + i ) + (SMap.bindings by_focus) ) @ - (if SMap.is_empty by_prereq then [] else - H.tr [H.th ~a:[H.a_colspan 2] - [H.txt [%i"success over exercises requiring skills"]]] :: - List.map (fun (sk, (tot, count)) -> - let i = - item ~indent:1 ~fmt:tag_span sk - ([%i"Success over exercises requiring skill "]^sk) - [pct tot (100 * count)] - in - mouseover_toggle_signal i sk set_hl_prereq_signal; - i - ) - (SMap.bindings by_prereq)) - end - ] + if SMap.is_empty by_prereq then [] + else + H.tr + [ H.th ~a:[H.a_colspan 2] + [H.txt [%i "success over exercises requiring skills"]] ] + :: List.map + (fun (sk, (tot, count)) -> + let i = + item ~indent:1 ~fmt:tag_span sk + ([%i "Success over exercises requiring skill "] ^ sk) + [pct tot (100 * count)] + in + mouseover_toggle_signal i sk set_hl_prereq_signal; + i ) + (SMap.bindings by_prereq) ) ] let init_exercises_and_stats_tabs teacher_token student_token answers = retrieve (Learnocaml_api.Exercise_index (Some teacher_token)) @@ -353,33 +358,35 @@ let init_exercises_and_stats_tabs teacher_token student_token answers = >>= fun status -> let assignments = gather_assignments student_token index status in Manip.replaceChildren El.Tabs.(stats.tab) (stats_tab assignments answers); - exercises_tab assignments answers >|= fun (tbl, sighandlers) -> + exercises_tab assignments answers + >|= fun (tbl, sighandlers) -> Manip.replaceChildren El.Tabs.(list.tab) [tbl]; sighandlers let _exercise_selection_updater = let previously_selected = ref None in - selected_exercise_signal |> React.S.map @@ fun id -> - let line id = find_component (El.Dyn.exercise_line_id id) in - (match !previously_selected with - | None -> () - | Some id -> Manip.removeClass (line id) "selected"); - previously_selected := id; - match id with - | None -> () - | Some id -> - Manip.addClass (line id) "selected"; - let selected_tab = React.S.value tab_select_signal in - if selected_tab = El.Tabs.list || selected_tab = El.Tabs.stats then - select_tab El.Tabs.report + selected_exercise_signal + |> React.S.map + @@ fun id -> + let line id = find_component (El.Dyn.exercise_line_id id) in + ( match !previously_selected with + | None -> () + | Some id -> Manip.removeClass (line id) "selected" ); + previously_selected := id; + match id with + | None -> () + | Some id -> + Manip.addClass (line id) "selected"; + let selected_tab = React.S.value tab_select_signal in + if selected_tab = El.Tabs.list || selected_tab = El.Tabs.stats then + select_tab El.Tabs.report let restore_report_button () = let report_button = El.Tabs.(report.btn) in Manip.removeClass report_button "success"; Manip.removeClass report_button "failure"; Manip.removeClass report_button "partial"; - Manip.replaceChildren report_button - Tyxml_js.Html5.[ txt [%i"Report"] ] + Manip.replaceChildren report_button Tyxml_js.Html5.[txt [%i "Report"]] let display_report exo report = let score, _failed = Report.result report in @@ -389,61 +396,61 @@ let display_report exo report = let max = Learnocaml_exercise.(access File.max_score exo) in if max = 0 then 999 else score * 100 / max in - if grade >= 100 then begin - Manip.addClass report_button "success" ; - Manip.replaceChildren report_button - Tyxml_js.Html5.[ txt [%i"Report"] ] - end else if grade = 0 then begin - Manip.addClass report_button "failure" ; - Manip.replaceChildren report_button - Tyxml_js.Html5.[ txt [%i"Report"] ] - end else begin - Manip.addClass report_button "partial" ; + if grade >= 100 then ( + Manip.addClass report_button "success"; + Manip.replaceChildren report_button Tyxml_js.Html5.[txt [%i "Report"]] ) + else if grade = 0 then ( + Manip.addClass report_button "failure"; + Manip.replaceChildren report_button Tyxml_js.Html5.[txt [%i "Report"]] ) + else ( + Manip.addClass report_button "partial"; let pct = Format.asprintf "%2d%%" grade in Manip.replaceChildren report_button - Tyxml_js.Html5.[ txt [%i"Report"] ; - span ~a: [ a_class [ "score" ] ] [ txt pct ]] - end ; - Manip.setInnerHtml El.Tabs.(report.tab) - (Format.asprintf "%a" Report.(output_html ~bare: true) report) ; + Tyxml_js.Html5.[txt [%i "Report"]; span ~a:[a_class ["score"]] [txt pct]] ); + Manip.setInnerHtml + El.Tabs.(report.tab) + (Format.asprintf "%a" Report.(output_html ~bare:true) report); grade let update_answer_tab, clear_answer_tab = ace_display El.Tabs.(editor.tab) let clear_tabs () = restore_report_button (); - List.iter (fun t -> - Manip.replaceChildren El.Tabs.(t.tab) []) - El.Tabs.([report; text]); + List.iter + (fun t -> Manip.replaceChildren El.Tabs.(t.tab) []) + El.Tabs.[report; text]; clear_answer_tab () let update_text_tab meta exo = let text_iframe = Dom_html.createIframe Dom_html.document in - Manip.replaceChildren El.Tabs.(text.tab) [ - H.h1 [H.txt meta.Exercise.Meta.title]; - Tyxml_js.Of_dom.of_iFrame text_iframe - ]; + Manip.replaceChildren + El.Tabs.(text.tab) + [ H.h1 [H.txt meta.Exercise.Meta.title] + ; Tyxml_js.Of_dom.of_iFrame text_iframe ]; Js.Opt.case - (text_iframe##.contentDocument) + text_iframe##.contentDocument (fun () -> failwith "cannot edit iframe document") (fun d -> - d##open_; - d##write (Js.string (exercise_text meta exo)); - d##close) + d##open_; + d##write (Js.string (exercise_text meta exo)); + d##close ) let update_report_tab exo ans = match ans.Answer.report with - | Some report -> + | Some report -> ( let grade = display_report exo report in - (match ans.Answer.grade with - | Some g when g <> grade -> - Manip.appendChildFirst El.Tabs.(report.tab) - (H.div ~a:[H.a_class ["warning"]] - [H.txt [%i"GRADE DOESN'T MATCH: cheating suspected"]]) - | _ -> ()) + match ans.Answer.grade with + | Some g when g <> grade -> + Manip.appendChildFirst + El.Tabs.(report.tab) + (H.div + ~a:[H.a_class ["warning"]] + [H.txt [%i "GRADE DOESN'T MATCH: cheating suspected"]]) + | _ -> () ) | None -> - Manip.replaceChildren El.Tabs.(report.tab) - [H.div [H.txt [%i"No report available"]]] + Manip.replaceChildren + El.Tabs.(report.tab) + [H.div [H.txt [%i "No report available"]]] let update_tabs meta exo ans = update_text_tab meta exo; @@ -454,11 +461,14 @@ let update_tabs meta exo ans = update_answer_tab ans.Answer.solution let () = - run_async_with_log @@ fun () -> + run_async_with_log + @@ fun () -> (* set_string_translations (); *) (* Manip.setInnerText El.version ("v."^Learnocaml_api.version); *) Learnocaml_local_storage.init (); - (match Js_utils.get_lang() with Some l -> Ocplib_i18n.set_lang l | None -> ()); + ( match Js_utils.get_lang () with + | Some l -> Ocplib_i18n.set_lang l + | None -> () ); set_string_translations_view (); let teacher_token = Learnocaml_local_storage.(retrieve sync_token) in if not (Token.is_teacher teacher_token) then @@ -466,28 +476,30 @@ let () = registered server-side *) failwith "The page you are trying to access is for teachers only"; let student_token = - try Token.parse (List.assoc "token" Url.Current.arguments) - with Not_found | Failure _ -> failwith "Student token missing or invalid" + try Token.parse (List.assoc "token" Url.Current.arguments) with + | Not_found | Failure _ -> failwith "Student token missing or invalid" in Manip.setInnerText El.token - ([%i"Status of student: "] ^ Token.to_string student_token); + ([%i "Status of student: "] ^ Token.to_string student_token); retrieve (Learnocaml_api.Fetch_save student_token) >>= fun save -> Manip.setInnerText El.nickname save.Save.nickname; - init_exercises_and_stats_tabs - teacher_token student_token save.Save.all_exercise_states + init_exercises_and_stats_tabs teacher_token student_token + save.Save.all_exercise_states >>= fun _sighandlers -> hide_loading ~id:El.loading_id (); let _sig = - selected_exercise_signal |> React.S.map @@ function - | None -> () - | Some ex_id -> - Lwt.async @@ fun () -> - retrieve (Learnocaml_api.Exercise (Some teacher_token, ex_id)) - >>= fun (meta, exo, _) -> - clear_tabs (); - let ans = SMap.find_opt ex_id save.Save.all_exercise_states in - update_tabs meta exo ans; - Lwt.return_unit + selected_exercise_signal + |> React.S.map + @@ function + | None -> () + | Some ex_id -> + Lwt.async + @@ fun () -> + retrieve (Learnocaml_api.Exercise (Some teacher_token, ex_id)) + >>= fun (meta, exo, _) -> + clear_tabs (); + let ans = SMap.find_opt ex_id save.Save.all_exercise_states in + update_tabs meta exo ans; Lwt.return_unit in Lwt.return_unit diff --git a/src/app/learnocaml_teacher_tab.ml b/src/app/learnocaml_teacher_tab.ml index db47f5eaa..86fda1f54 100644 --- a/src/app/learnocaml_teacher_tab.ml +++ b/src/app/learnocaml_teacher_tab.ml @@ -11,82 +11,73 @@ open Js_utils open Lwt open Learnocaml_data open Learnocaml_common - module H = Tyxml_js.Html5 module ES = Exercise.Status let filter_input input_id list_id apply_fun = let input_field = - H.input ~a:[ - H.a_id input_id; - H.a_input_type `Search; - H.a_list list_id; - ] () + H.input ~a:[H.a_id input_id; H.a_input_type `Search; H.a_list list_id] () in Manip.Ev.oninput input_field (fun _ev -> apply_fun (Manip.value input_field); - true); - H.div ~a:[H.a_class ["filter_input"]] [ - H.datalist ~a:[H.a_id list_id] (); - input_field; - H.span ~a:[ - H.a_class ["filter_reset_cross"]; - H.a_onclick (fun _ -> - (Tyxml_js.To_dom.of_input input_field)##.value := Js.string ""; - apply_fun ""; - true); - ] - [H.txt "\xe2\x9c\x96" (* U+2716 heavy multiplication x *)]; - ] + true ); + H.div + ~a:[H.a_class ["filter_input"]] + [ H.datalist ~a:[H.a_id list_id] () + ; input_field + ; H.span + ~a: + [ H.a_class ["filter_reset_cross"] + ; H.a_onclick (fun _ -> + (Tyxml_js.To_dom.of_input input_field)##.value := Js.string ""; + apply_fun ""; + true ) ] + [H.txt "\xe2\x9c\x96" (* U+2716 heavy multiplication x *)] ] let tag_addremove list_id placeholder add_fun remove_fun = let tag_input = - H.input ~a:[ - H.a_input_type `Text; - H.a_list list_id; - H.a_placeholder placeholder; - ] () + H.input + ~a:[H.a_input_type `Text; H.a_list list_id; H.a_placeholder placeholder] + () in let get_input_list () = - List.filter ((<>) "") - (String.split_on_char ' ' (Manip.value tag_input)) - in - H.div [ - tag_input; - H.button ~a:[ - H.a_class ["addremove"]; - H.a_onclick (fun _ev -> - add_fun (get_input_list ()); - true) - ] [ H.txt "\xe2\x9e\x95" (* U+2795 heavy plus sign *) ]; - H.button ~a:[ - H.a_class ["addremove"]; - H.a_onclick (fun _ev -> - remove_fun (get_input_list ()); - true); - ] [ H.txt "\xe2\x9e\x96" (* U+2796 heavy minus sign *) ]; - ] + List.filter (( <> ) "") (String.split_on_char ' ' (Manip.value tag_input)) + in + H.div + [ tag_input + ; H.button + ~a: + [ H.a_class ["addremove"] + ; H.a_onclick (fun _ev -> + add_fun (get_input_list ()); + true ) ] + [H.txt "\xe2\x9e\x95" (* U+2795 heavy plus sign *)] + ; H.button + ~a: + [ H.a_class ["addremove"] + ; H.a_onclick (fun _ev -> + remove_fun (get_input_list ()); + true ) ] + [H.txt "\xe2\x9e\x96" (* U+2796 heavy minus sign *)] ] let rec teacher_tab token _select _params () = let action_new_token () = retrieve (Learnocaml_api.Create_teacher_token token) >|= fun new_token -> - alert ~title:[%i"TEACHER TOKEN"] - (Printf.sprintf [%if"New teacher token created:\n%s\n\n\ - write it down."] + alert ~title:[%i "TEACHER TOKEN"] + (Printf.sprintf [%if "New teacher token created:\n%s\n\nwrite it down."] (Token.to_string new_token)) in let action_csv_export () = retrieve (Learnocaml_api.Students_csv (token, [], [])) >|= fun csv -> - Learnocaml_common.fake_download - ~name:"learnocaml.csv" + Learnocaml_common.fake_download ~name:"learnocaml.csv" ~contents:(Js.string csv) in let indent_style lvl = H.a_style (Printf.sprintf "text-align: left; padding-left: %dem;" lvl) in - let htbl_keys t = Hashtbl.fold (fun k _ acc -> k::acc) t [] in + let htbl_keys t = Hashtbl.fold (fun k _ acc -> k :: acc) t [] in let elt e = Js.Opt.case (Dom_html.CoerceTo.element (H.toelt e)) @@ -94,15 +85,16 @@ let rec teacher_tab token _select _params () = (fun x -> x) in let descendants_by_tag parent tag = - List.fold_left (fun acc node -> - Js.Opt.case (Dom_html.CoerceTo.element node) + List.fold_left + (fun acc node -> + Js.Opt.case + (Dom_html.CoerceTo.element node) (fun () -> acc) - (fun elt -> elt :: acc)) + (fun elt -> elt :: acc) ) [] (Dom.list_of_nodeList ((elt parent)##getElementsByTagName (Js.string tag))) in - (* State storage *) let selected_exercises = Hashtbl.create 117 in let selected_students = Hashtbl.create 117 in @@ -114,65 +106,61 @@ let rec teacher_tab token _select _params () = let students_changes = ref Token.Map.empty in let all_student_tags = ref SSet.empty in let status_map = ref SMap.empty in - let (status_changes: ES.t SMap.t ref) = ref SMap.empty in + let (status_changes : ES.t SMap.t ref) = ref SMap.empty in let status_current () = - SMap.merge (fun _ old newer -> match newer with None -> old | s -> s) - !status_map !status_changes; + SMap.merge + (fun _ old newer -> match newer with None -> old | s -> s) + !status_map !status_changes in let get_status id = - try SMap.find id !status_changes with Not_found -> - try SMap.find id !status_map with Not_found -> - ES.default id + try SMap.find id !status_changes with Not_found -> ( + try SMap.find id !status_map with Not_found -> ES.default id ) in let exercise_changed_status id = match SMap.find_opt id !status_changes with | None -> false - | Some ch -> match SMap.find_opt id !status_map with + | Some ch -> ( + match SMap.find_opt id !status_map with | None -> ch <> ES.default id - | Some st -> ch <> st + | Some st -> ch <> st ) in let students_current () = - Token.Map.merge (fun _ old newer -> match newer with None -> old | s -> s) + Token.Map.merge + (fun _ old newer -> match newer with None -> old | s -> s) !students_map !students_changes in let get_student token = - try Token.Map.find token !students_changes with Not_found -> - try Token.Map.find token !students_map with Not_found -> - Student.default token + try Token.Map.find token !students_changes with Not_found -> ( + try Token.Map.find token !students_map with Not_found -> + Student.default token ) in - let exercise_line_id id = "learnocaml_exercise_"^id in - let exercise_group_id id = "exercise_group_"^id in + let exercise_line_id id = "learnocaml_exercise_" ^ id in + let exercise_group_id id = "exercise_group_" ^ id in let student_line_id student = - "learnocaml_student_" ^ match student with - | `Any -> "any" - | `Token tok -> Token.to_string tok + "learnocaml_student_" + ^ match student with `Any -> "any" | `Token tok -> Token.to_string tok in - let assg_line_id id = "assg_line_"^string_of_int id in - + let assg_line_id id = "assg_line_" ^ string_of_int id in let all_exercises g = - Exercise.Index.fold_exercises (fun acc id _ -> id :: acc) - [] g - |> List.rev + Exercise.Index.fold_exercises (fun acc id _ -> id :: acc) [] g |> List.rev in - let get_student_selection () = - Hashtbl.fold (fun st () (toks, dft) -> match st with - | `Token tk -> Token.Set.add tk toks, dft - | `Any -> toks, true) + Hashtbl.fold + (fun st () (toks, dft) -> + match st with + | `Token tk -> (Token.Set.add tk toks, dft) + | `Any -> (toks, true) ) selected_students (Token.Set.empty, false) in - - let auto_checkbox () = - H.div ~a:[H.a_class ["auto_checkbox"]] [] - in + let auto_checkbox () = H.div ~a:[H.a_class ["auto_checkbox"]] [] in let auto_checkbox_td () = H.td ~a:[H.a_class ["auto_checkbox"]] [auto_checkbox ()] in - (* Action function callbacks *) let update_changed_status = ref (fun () -> assert false) in let toggle_selected_exercises = - ref (fun ?force:_ ?update:_ _ -> assert false) in + ref (fun ?force:_ ?update:_ _ -> assert false) + in let toggle_selected_students = ref (fun ?force:_ ?update:_ _ -> assert false) in @@ -181,134 +169,158 @@ let rec teacher_tab token _select _params () = let student_change = ref (fun _ -> assert false) in let assignment_change = ref (fun _ -> assert false) in let assignment_remove = ref (fun _ -> assert false) in - (* Exercises table *) let rec mk_table group_level acc status group = match group with | Exercise.Index.Groups groups_list -> - List.fold_left (fun acc (id, g) -> + List.fold_left + (fun acc (id, g) -> let all_children = all_exercises g.Exercise.Index.contents in let acc = - H.tr ~a:[ - H.a_id (exercise_group_id id); - H.a_class ["exercise_group"]; - H.a_onclick (fun _ -> - !toggle_selected_exercises all_children; false); - ] [ - H.td []; - H.th ~a:[H.a_colspan 10; indent_style group_level] - [H.txt g.Exercise.Index.title]; - ] :: acc + H.tr + ~a: + [ H.a_id (exercise_group_id id) + ; H.a_class ["exercise_group"] + ; H.a_onclick (fun _ -> + !toggle_selected_exercises all_children; + false ) ] + [ H.td [] + ; H.th + ~a:[H.a_colspan 10; indent_style group_level] + [H.txt g.Exercise.Index.title] ] + :: acc in - mk_table (group_level + 1) acc status g.Exercise.Index.contents) + mk_table (group_level + 1) acc status g.Exercise.Index.contents ) acc groups_list | Exercise.Index.Exercises exlist -> - List.fold_left (fun acc (id, meta) -> + List.fold_left + (fun acc (id, meta) -> let open_exercise_ () = - let _win = window_open ("/exercises/"^id^"/") "_blank" in + let _win = window_open ("/exercises/" ^ id ^ "/") "_blank" in false in let open_partition_ () = Lwt.async (fun () -> - ask_string ~title:"Choose a function name" - [H.txt @@ "Choose a function name to partition codes from "^ id ^": "] - >|= fun funname -> - let _win = - window_open - ("/partition-view.html?id="^ id ^"&function="^funname^"&prof=30") "_blank" - in ()); - false - in - match meta with None -> acc | Some meta -> - let st = status id in - let hid = exercise_line_id id in - let classes = - (if exercise_changed_status id then ["changed"] else []) @ - (if Hashtbl.mem selected_exercises id then ["selected"] else []) + ask_string ~title:"Choose a function name" + [ H.txt @@ "Choose a function name to partition codes from " + ^ id ^ ": " ] + >|= fun funname -> + let _win = + window_open + ( "/partition-view.html?id=" ^ id ^ "&function=" ^ funname + ^ "&prof=30" ) + "_blank" + in + () ); + false in - let skills_prereq = ES.skills_prereq meta st in - let skills_focus = ES.skills_focus meta st in - H.tr ~a:[ - H.a_id hid; - H.a_class ("exercise_line" :: classes); - H.a_onclick (fun _ -> !toggle_selected_exercises [id]; false); - H.a_ondblclick (fun _ -> open_exercise_ ()); - H.a_onmouseup (fun ev -> - Js.Optdef.case ev##.which (fun () -> true) @@ fun btn -> - if btn = Dom_html.Middle_button then open_partition_ () else true); - ] [ - auto_checkbox_td (); - H.td ~a:[indent_style group_level] - [ H.txt meta.Exercise.Meta.title ]; - H.td ~a:[H.a_class ["skills-prereq"]] - (List.map tag_span skills_prereq @ - if skills_prereq <> [] || skills_focus <> [] - then [H.txt "\xe2\x87\xa2"] - (* U+21E2, rightwards dashed arrow *) - else []); - H.td ~a:[H.a_class ["skills-focus"]] - (List.map tag_span skills_focus); - H.td [stars_div meta.Exercise.Meta.stars]; - H.td [ - let cls, text = - if Token.Map.is_empty ES.(st.assignments.token_map) then - match ES.(st.assignments.default) with - | ES.Open -> "exo_open", [%i"Open"] - | ES.Closed -> "exo_closed", [%i"Closed"] - | ES.Assigned _ -> "exo_assigned", [%i"Assigned"] - else "exo_assigned", [%i"Assigned"] + match meta with + | None -> acc + | Some meta -> + let st = status id in + let hid = exercise_line_id id in + let classes = + (if exercise_changed_status id then ["changed"] else []) + @ + if Hashtbl.mem selected_exercises id then ["selected"] + else [] in - H.span ~a:[H.a_class [cls]] [H.txt text] - ]; - ] :: acc) + let skills_prereq = ES.skills_prereq meta st in + let skills_focus = ES.skills_focus meta st in + H.tr + ~a: + [ H.a_id hid + ; H.a_class ("exercise_line" :: classes) + ; H.a_onclick (fun _ -> + !toggle_selected_exercises [id]; + false ) + ; H.a_ondblclick (fun _ -> open_exercise_ ()) + ; H.a_onmouseup (fun ev -> + Js.Optdef.case ev##.which (fun () -> true) + @@ fun btn -> + if btn = Dom_html.Middle_button then + open_partition_ () + else true ) ] + [ auto_checkbox_td () + ; H.td ~a:[indent_style group_level] + [H.txt meta.Exercise.Meta.title] + ; H.td + ~a:[H.a_class ["skills-prereq"]] + ( List.map tag_span skills_prereq + @ + if skills_prereq <> [] || skills_focus <> [] then + [H.txt "\xe2\x87\xa2"] + (* U+21E2, rightwards dashed arrow *) + else [] ) + ; H.td + ~a:[H.a_class ["skills-focus"]] + (List.map tag_span skills_focus) + ; H.td [stars_div meta.Exercise.Meta.stars] + ; H.td + [ (let cls, text = + if Token.Map.is_empty ES.(st.assignments.token_map) + then + match ES.(st.assignments.default) with + | ES.Open -> ("exo_open", [%i "Open"]) + | ES.Closed -> ("exo_closed", [%i "Closed"]) + | ES.Assigned _ -> + ("exo_assigned", [%i "Assigned"]) + else ("exo_assigned", [%i "Assigned"]) + in + H.span ~a:[H.a_class [cls]] [H.txt text]) ] ] + :: acc ) acc exlist in let set_exercise_filtering str = let skills, keywords = - List.partition (fun s -> SSet.mem s !all_exercise_skills) - (List.filter ((<>) "") (String.split_on_char ' ' str)) + List.partition + (fun s -> SSet.mem s !all_exercise_skills) + (List.filter (( <> ) "") (String.split_on_char ' ' str)) in let res = - List.map (fun s -> - new%js Js.regExp_withFlags (Js.string s) (Js.string "i")) + List.map + (fun s -> new%js Js.regExp_withFlags (Js.string s) (Js.string "i")) keywords in let matches id meta = let st = get_status id in - (List.for_all (fun re -> - let strmatch = function - | None -> false - | Some s -> Js.to_bool (re##test (Js.string s)) - in - strmatch meta.Exercise.Meta.id || - strmatch (Some meta.Exercise.Meta.title) || - strmatch meta.Exercise.Meta.short_description) - res) - && - List.for_all (fun skill -> - List.mem skill (ES.skills_focus meta st) || - List.mem skill (ES.skills_prereq meta st)) - skills + List.for_all + (fun re -> + let strmatch = function + | None -> false + | Some s -> Js.to_bool (re##test (Js.string s)) + in + strmatch meta.Exercise.Meta.id + || strmatch (Some meta.Exercise.Meta.title) + || strmatch meta.Exercise.Meta.short_description ) + res + && List.for_all + (fun skill -> + List.mem skill (ES.skills_focus meta st) + || List.mem skill (ES.skills_prereq meta st) ) + skills in let rec hide = function | Exercise.Index.Groups groups_list -> - List.fold_left (fun (empty0, hidden0) (id, g) -> + List.fold_left + (fun (empty0, hidden0) (id, g) -> let empty, hidden = hide g.Exercise.Index.contents in let elt = find_component (exercise_group_id id) in if empty then Manip.addClass elt "exercise_hidden" else Manip.removeClass elt "exercise_hidden"; - empty && empty0, List.rev_append hidden hidden0) + (empty && empty0, List.rev_append hidden hidden0) ) (true, []) groups_list | Exercise.Index.Exercises l -> - List.fold_left (fun (empty, hidden) (id, ex) -> + List.fold_left + (fun (empty, hidden) (id, ex) -> let elt = find_component (exercise_line_id id) in match ex with | Some ex when matches id ex -> Manip.removeClass elt "exercise_hidden"; - false, hidden + (false, hidden) | _ -> Manip.addClass elt "exercise_hidden"; - empty, (id::hidden)) + (empty, id :: hidden) ) (true, []) l in let _empty, hidden = hide !exercises_index in @@ -316,27 +328,29 @@ let rec teacher_tab token _select _params () = !toggle_selected_exercises ~force:false ~update:true hidden in let exercises_list_div = - H.div ~a:[H.a_id "exercises_list"] [H.txt [%i"Loading..."]] + H.div ~a:[H.a_id "exercises_list"] [H.txt [%i "Loading..."]] in let exercise_skills_list_id = "exercise_skills_list" in let exercises_div = let legend = - H.legend ~a:[ - H.a_onclick (fun _ -> - !toggle_selected_exercises (all_exercises !exercises_index); - true); - ] [H.txt [%i"Exercises"]; H.txt " \xe2\x98\x90" (* U+2610 *)] + H.legend + ~a: + [ H.a_onclick (fun _ -> + !toggle_selected_exercises (all_exercises !exercises_index); + true ) ] + [H.txt [%i "Exercises"]; H.txt " \xe2\x98\x90" (* U+2610 *)] in - H.div ~a:[H.a_id "exercises_pane"; H.a_class ["learnocaml_pane"]] [ - H.div ~a:[H.a_id "exercises_filter_box"] [ - H.datalist ~a:[H.a_id exercise_skills_list_id] (); - filter_input "exercises_search_field" - exercise_skills_list_id set_exercise_filtering]; - H.fieldset ~legend [ exercises_list_div ]; - ] + H.div + ~a:[H.a_id "exercises_pane"; H.a_class ["learnocaml_pane"]] + [ H.div + ~a:[H.a_id "exercises_filter_box"] + [ H.datalist ~a:[H.a_id exercise_skills_list_id] () + ; filter_input "exercises_search_field" exercise_skills_list_id + set_exercise_filtering ] + ; H.fieldset ~legend [exercises_list_div] ] in let students_list_div = - H.div ~a:[H.a_id "students_list"] [H.txt [%i"Loading..."]]; + H.div ~a:[H.a_id "students_list"] [H.txt [%i "Loading..."]] in let student_tags_list_id = "student_tags_list" in let sort_type = [`Nick; `Token; `Date; `Tags] in @@ -346,9 +360,7 @@ let rec teacher_tab token _select _params () = | `Date -> "date" | `Tags -> "tags" in - let str_to_sort s = - List.find (fun t -> sort_to_str t = s) sort_type - in + let str_to_sort s = List.find (fun t -> sort_to_str t = s) sort_type in let get_student_sorting () = match Dom_html.getElementById_coerce "student_sort" Dom_html.CoerceTo.select @@ -357,42 +369,42 @@ let rec teacher_tab token _select _params () = | None -> raise Not_found in let html_token tk = - H.span ~a:[H.a_class ["learnocaml_token"]] - [H.txt (Token.to_string tk)] + H.span ~a:[H.a_class ["learnocaml_token"]] [H.txt (Token.to_string tk)] in - let make_student_line ?(selected=false) st contents = + let make_student_line ?(selected = false) st contents = let open_student_tab = let f t = let _win = - window_open (Printf.sprintf "/student-view.html?token=%s" - (Token.to_string t)) "_blank" + window_open + (Printf.sprintf "/student-view.html?token=%s" (Token.to_string t)) + "_blank" in false in match st with | `Token t -> - [ H.a_ondblclick (fun _ -> f t); - H.a_onmouseup (fun ev -> - Js.Optdef.case ev##.which (fun () -> true) @@ fun btn -> - if btn = Dom_html.Middle_button then f t else true) - ] + [ H.a_ondblclick (fun _ -> f t) + ; H.a_onmouseup (fun ev -> + Js.Optdef.case ev##.which (fun () -> true) + @@ fun btn -> + if btn = Dom_html.Middle_button then f t else true ) ] | `Any -> [] in - H.tr ~a:([ - H.a_id (student_line_id st); - H.a_class (["student_line"] @ if selected then ["selected"] else []); - H.a_onclick (fun _ -> - !toggle_selected_students [st]; - true); - ] @ open_student_tab) + H.tr + ~a: + ( [ H.a_id (student_line_id st) + ; H.a_class (["student_line"] @ if selected then ["selected"] else []) + ; H.a_onclick (fun _ -> + !toggle_selected_students [st]; + true ) ] + @ open_student_tab ) (auto_checkbox_td () :: contents) in let anystudents_line = - make_student_line `Any [ - H.td ~a:[H.a_colspan 10; H.a_class ["future_students"]] [ - H.txt [%i"any future students"] - ]; - ] + make_student_line `Any + [ H.td + ~a:[H.a_colspan 10; H.a_class ["future_students"]] + [H.txt [%i "any future students"]] ] in let student_progression_id tok = "student-progression-" ^ Token.to_string tok @@ -400,7 +412,8 @@ let rec teacher_tab token _select _params () = let fill_students_pane () = let compare = let open Student in - let compare_nick st1 st2 = match st1.nickname, st2.nickname with + let compare_nick st1 st2 = + match (st1.nickname, st2.nickname) with | None, None -> 0 | None, Some _ -> 1 | Some _, None -> -1 @@ -409,91 +422,100 @@ let rec teacher_tab token _select _params () = in match get_student_sorting () with | `Token -> fun st1 st2 -> compare st1.token st2.token - | `Nick -> fun st1 st2 -> - (match compare_nick st1 st2 with - | 0 -> compare st1 st2 - | n -> n) - | `Date -> fun st1 st2 -> - (match compare st1.creation_date st2.creation_date with - | 0 -> compare st1 st2 - | n -> n) - | `Tags -> fun st1 st2 -> - (match compare st1.tags st2.tags with - | 0 -> (match compare_nick st1 st2 with - | 0 -> compare st1 st2 - | n -> n) - | n -> n) + | `Nick -> ( + fun st1 st2 -> + match compare_nick st1 st2 with 0 -> compare st1 st2 | n -> n ) + | `Date -> ( + fun st1 st2 -> + match compare st1.creation_date st2.creation_date with + | 0 -> compare st1 st2 + | n -> n ) + | `Tags -> ( + fun st1 st2 -> + match compare st1.tags st2.tags with + | 0 -> ( + match compare_nick st1 st2 with 0 -> compare st1 st2 | n -> n ) + | n -> n ) in let all_students = Token.Map.fold (fun _ st acc -> st :: acc) (students_current ()) [] |> List.sort compare in let table = - anystudents_line :: - List.map (fun st -> - make_student_line - ~selected:(Hashtbl.mem selected_students (`Token st.Student.token)) - (`Token st.Student.token) [ - H.td [html_token st.Student.token]; - H.td (match st.Student.nickname with - | Some n -> [H.txt n] - | _ -> []); - H.td (List.map tag_span (SSet.elements st.Student.tags)); - try find_component (student_progression_id st.Student.token) - with Failure _ -> - H.td ~a:[H.a_id (student_progression_id st.Student.token); - H.a_class ["student-progression"]] - []; - ]) - all_students + anystudents_line + :: List.map + (fun st -> + make_student_line + ~selected: + (Hashtbl.mem selected_students (`Token st.Student.token)) + (`Token st.Student.token) + [ H.td [html_token st.Student.token] + ; H.td + ( match st.Student.nickname with + | Some n -> [H.txt n] + | _ -> [] ) + ; H.td (List.map tag_span (SSet.elements st.Student.tags)) + ; ( try find_component (student_progression_id st.Student.token) + with Failure _ -> + H.td + ~a: + [ H.a_id (student_progression_id st.Student.token) + ; H.a_class ["student-progression"] ] + [] ) ] ) + all_students in Manip.replaceChildren students_list_div [H.table table]; all_student_tags := - List.fold_left (fun tags st -> SSet.union tags st.Student.tags) + List.fold_left + (fun tags st -> SSet.union tags st.Student.tags) SSet.empty all_students; - Manip.replaceSelf (find_component student_tags_list_id) - (H.datalist ~a:[H.a_id student_tags_list_id] ~children:( - `Options - (SSet.fold (fun tag acc -> H.option (H.txt tag) :: acc) - !all_student_tags [] - )) - ()) + Manip.replaceSelf + (find_component student_tags_list_id) + (H.datalist + ~a:[H.a_id student_tags_list_id] + ~children: + (`Options + (SSet.fold + (fun tag acc -> H.option (H.txt tag) :: acc) + !all_student_tags [])) + ()) in let set_student_filtering str = let tags, keywords = - List.partition (fun s -> SSet.mem s !all_student_tags) - (List.filter ((<>) "") (String.split_on_char ' ' str)) + List.partition + (fun s -> SSet.mem s !all_student_tags) + (List.filter (( <> ) "") (String.split_on_char ' ' str)) in let res = - List.map (fun s -> - new%js Js.regExp_withFlags (Js.string s) (Js.string "i")) + List.map + (fun s -> new%js Js.regExp_withFlags (Js.string s) (Js.string "i")) keywords in let matches std = - (match std.Student.nickname with - | None -> keywords = [] - | Some n -> - List.for_all (fun re -> Js.to_bool (re##test (Js.string n))) res) - && - List.for_all (fun tag -> SSet.mem tag std.Student.tags) tags + ( match std.Student.nickname with + | None -> keywords = [] + | Some n -> + List.for_all (fun re -> Js.to_bool (re##test (Js.string n))) res ) + && List.for_all (fun tag -> SSet.mem tag std.Student.tags) tags in let hidden = - if tags = [] && keywords = [] then - (Manip.removeClass anystudents_line "student_hidden"; []) - else - (Manip.addClass anystudents_line "student_hidden"; [`Any]) + if tags = [] && keywords = [] then ( + Manip.removeClass anystudents_line "student_hidden"; + [] ) + else ( + Manip.addClass anystudents_line "student_hidden"; + [`Any] ) in let hidden = - Token.Map.fold (fun tok std hidden-> + Token.Map.fold + (fun tok std hidden -> let elt = find_component (student_line_id (`Token tok)) in - if matches std then - (Manip.removeClass elt "student_hidden"; - hidden) + if matches std then ( + Manip.removeClass elt "student_hidden"; + hidden ) else ( Manip.addClass elt "student_hidden"; - `Token tok :: hidden - ) - ) + `Token tok :: hidden ) ) (students_current ()) hidden in if !selected_assignment = None then @@ -502,17 +524,17 @@ let rec teacher_tab token _select _params () = let change_student_tags f = let selected, _ = get_student_selection () in students_changes := - Token.Set.fold (fun tok acc -> + Token.Set.fold + (fun tok acc -> let student = get_student tok in let tags = f student.Student.tags in if tags <> student.Student.tags then Token.Map.add tok {student with Student.tags} acc - else acc) + else acc ) selected !students_changes; fill_students_pane (); - set_student_filtering - (Manip.value (find_component "student_search_field")); - !update_changed_status (); + set_student_filtering (Manip.value (find_component "student_search_field")); + !update_changed_status () in let add_student_tags tags = change_student_tags (SSet.union (SSet.of_list tags)) @@ -523,152 +545,170 @@ let rec teacher_tab token _select _params () = in let students_div = let legend = - H.legend ~a:[ - H.a_onclick (fun _ -> - let all = - Token.Map.fold (fun k _ acc -> (`Token k)::acc) - !students_map [`Any] - in - let all = - List.filter (fun t -> - not (Manip.hasClass (find_component (student_line_id t)) - "student_hidden")) - all - in - !toggle_selected_students all; - true - ); - ] [H.txt [%i"Students"]; - H.txt " \xe2\x98\x90" (* U+2610 ballot box *)] + H.legend + ~a: + [ H.a_onclick (fun _ -> + let all = + Token.Map.fold + (fun k _ acc -> `Token k :: acc) + !students_map [`Any] + in + let all = + List.filter + (fun t -> + not + (Manip.hasClass + (find_component (student_line_id t)) + "student_hidden") ) + all + in + !toggle_selected_students all; + true ) ] + [H.txt [%i "Students"]; H.txt " \xe2\x98\x90" (* U+2610 ballot box *)] in - H.div ~a:[H.a_id "students_pane"; H.a_class ["learnocaml_pane"]] [ - H.div ~a:[H.a_id "students_filter_box"] [ - H.datalist ~a:[H.a_id student_tags_list_id] (); - filter_input "student_search_field" - student_tags_list_id set_student_filtering; - H.div ~a:[H.a_class ["filler_h"]] []; - H.label ~a:[H.a_label_for "student_sort"] - [H.txt [%i"Sort by"]]; - H.select ~a:[ - H.a_id "student_sort"; - H.a_oninput (fun _ev -> fill_students_pane (); true); - ] [ - H.option ~a:[H.a_value (sort_to_str `Nick); H.a_selected ()] - (H.txt [%i"Nickname"]); - H.option ~a:[H.a_value (sort_to_str `Token)] - (H.txt [%i"Token"]); - H.option ~a:[H.a_value (sort_to_str `Date)] - (H.txt [%i"Creation date"]); - H.option ~a:[H.a_value (sort_to_str `Tags)] - (H.txt [%i"Tags"]); - ] - ]; - H.fieldset ~legend [ students_list_div ]; - H.div ~a:[H.a_id "student_controls"] [ - tag_addremove student_tags_list_id [%i"tags"] - add_student_tags remove_student_tags; - ] - ] + H.div + ~a:[H.a_id "students_pane"; H.a_class ["learnocaml_pane"]] + [ H.div + ~a:[H.a_id "students_filter_box"] + [ H.datalist ~a:[H.a_id student_tags_list_id] () + ; filter_input "student_search_field" student_tags_list_id + set_student_filtering + ; H.div ~a:[H.a_class ["filler_h"]] [] + ; H.label ~a:[H.a_label_for "student_sort"] [H.txt [%i "Sort by"]] + ; H.select + ~a: + [ H.a_id "student_sort" + ; H.a_oninput (fun _ev -> fill_students_pane (); true) ] + [ H.option + ~a:[H.a_value (sort_to_str `Nick); H.a_selected ()] + (H.txt [%i "Nickname"]) + ; H.option + ~a:[H.a_value (sort_to_str `Token)] + (H.txt [%i "Token"]) + ; H.option + ~a:[H.a_value (sort_to_str `Date)] + (H.txt [%i "Creation date"]) + ; H.option ~a:[H.a_value (sort_to_str `Tags)] (H.txt [%i "Tags"]) + ] ] + ; H.fieldset ~legend [students_list_div] + ; H.div + ~a:[H.a_id "student_controls"] + [ tag_addremove student_tags_list_id [%i "tags"] add_student_tags + remove_student_tags ] ] in let fill_exercises_pane () = let table = List.rev (mk_table 0 [] get_status !exercises_index) in Manip.replaceChildren exercises_list_div [H.table table]; all_exercise_skills := - Exercise.Index.fold_exercises (fun acc id meta -> + Exercise.Index.fold_exercises + (fun acc id meta -> let st = get_status id in let acc = - List.fold_left (fun acc sk -> SSet.add sk acc) + List.fold_left + (fun acc sk -> SSet.add sk acc) acc (ES.skills_prereq meta st) in - List.fold_left (fun acc sk -> SSet.add sk acc) - acc (ES.skills_focus meta st)) - SSet.empty - !exercises_index; - Manip.replaceSelf (find_component exercise_skills_list_id) - (H.datalist ~a:[H.a_id exercise_skills_list_id] ~children:( - `Options - (SSet.fold (fun skill acc -> H.option (H.txt skill) :: acc) - !all_exercise_skills [] - )) - ()); + List.fold_left + (fun acc sk -> SSet.add sk acc) + acc (ES.skills_focus meta st) ) + SSet.empty !exercises_index; + Manip.replaceSelf + (find_component exercise_skills_list_id) + (H.datalist + ~a:[H.a_id exercise_skills_list_id] + ~children: + (`Options + (SSet.fold + (fun skill acc -> H.option (H.txt skill) :: acc) + !all_exercise_skills [])) + ()); match Manip.value (find_component "exercises_search_field") with | "" -> () | s -> set_exercise_filtering s in - let assignment_line id = let selected = !selected_assignment = Some id in let now = gettimeofday () in let date id assg_id t = let date = new%js Js.date_fromTimeValue (t *. 1000.) in let cls = if t <= now then ["date_past"] else ["date_future"] in - H.div [ - H.input ~a:([ - H.a_id ("date_"^id); - H.a_class ("assignment_date" :: cls); - H.a_input_type `Date; - H.a_value - (Printf.sprintf "%04d-%02d-%02d" - date##getFullYear (date##getMonth + 1) date##getDate); - H.a_onblur (fun _ -> !assignment_change assg_id; true); - H.a_onkeydown (fun ev -> - if ev##.keyCode = 13 then !assignment_change assg_id; true); - H.a_pattern "[0-9]{4}-[0-9]{2}-[0-9]{2}"; - H.a_required (); - ] @ if selected then [] else [H.a_readonly ()]) - (); - H.input ~a:([ - H.a_id ("time_"^id); - H.a_class ("assignment_date" :: cls); - H.a_input_type `Time; - H.a_value - (Printf.sprintf "%02d:%02d" - date##getHours date##getMinutes); - H.a_onblur (fun _ -> !assignment_change assg_id; true); - H.a_onkeydown (fun ev -> - if ev##.keyCode = 13 then !assignment_change assg_id; true); - H.a_pattern "[0-9]{2}:[0-9]{2}"; - H.a_required (); - ] @ if selected then [] else [H.a_readonly ()]) - (); - ] + H.div + [ H.input + ~a: + ( [ H.a_id ("date_" ^ id) + ; H.a_class ("assignment_date" :: cls) + ; H.a_input_type `Date + ; H.a_value + (Printf.sprintf "%04d-%02d-%02d" date##getFullYear + (date##getMonth + 1) + date##getDate) + ; H.a_onblur (fun _ -> !assignment_change assg_id; true) + ; H.a_onkeydown (fun ev -> + if ev##.keyCode = 13 then !assignment_change assg_id; + true ) + ; H.a_pattern "[0-9]{4}-[0-9]{2}-[0-9]{2}" + ; H.a_required () ] + @ if selected then [] else [H.a_readonly ()] ) + () + ; H.input + ~a: + ( [ H.a_id ("time_" ^ id) + ; H.a_class ("assignment_date" :: cls) + ; H.a_input_type `Time + ; H.a_value + (Printf.sprintf "%02d:%02d" date##getHours date##getMinutes) + ; H.a_onblur (fun _ -> !assignment_change assg_id; true) + ; H.a_onkeydown (fun ev -> + if ev##.keyCode = 13 then !assignment_change assg_id; + true ) + ; H.a_pattern "[0-9]{2}:[0-9]{2}" + ; H.a_required () ] + @ if selected then [] else [H.a_readonly ()] ) + () ] in let hid = assg_line_id id in - let ((start, stop), tokens, exo_ids, default) = + let (start, stop), tokens, exo_ids, default = Hashtbl.find assignments_tbl id in let cls = [] in - let cls = if selected then "selected"::cls else cls in - let cls = "assg_line"::cls in - let n_exos = match SSet.cardinal exo_ids with - | 1 -> [%i"1 exercise"] - | n -> Printf.sprintf [%if"%d exercises"] n + let cls = if selected then "selected" :: cls else cls in + let cls = "assg_line" :: cls in + let n_exos = + match SSet.cardinal exo_ids with + | 1 -> [%i "1 exercise"] + | n -> Printf.sprintf [%if "%d exercises"] n in - let n_students = match Token.Set.cardinal tokens, default with - | 1, false -> [%i"1 student"] - | n, false -> Printf.sprintf [%if"%d students"] n - | n, true -> Printf.sprintf [%if"%d+ students"] n + let n_students = + match (Token.Set.cardinal tokens, default) with + | 1, false -> [%i "1 student"] + | n, false -> Printf.sprintf [%if "%d students"] n + | n, true -> Printf.sprintf [%if "%d+ students"] n in - H.tr ~a:[ - H.a_id hid; - H.a_class cls; - H.a_onclick (fun ev -> - if - Js.Opt.case ev##.target (fun () -> true) (fun e -> - String.lowercase_ascii (Js.to_string e##.tagName) <> "input" - || Js.to_bool (e##hasAttribute (Js.string "readonly"))) - then !toggle_select_assignment id; - true) - ] [ - H.td [date ("start_"^hid) id start]; - H.td [date ("stop_"^hid) id stop]; - H.td [H.txt n_exos]; - H.td [H.txt n_students]; - H.td ~a:[H.a_onclick (fun _ -> !assignment_remove id; false); - H.a_class ["remove-cross"]] - [H.txt "\xe2\x9c\x96" (* U+2716 heavy multiplication x *)]; + H.tr + ~a: + [ H.a_id hid + ; H.a_class cls + ; H.a_onclick (fun ev -> + if + Js.Opt.case ev##.target + (fun () -> true) + (fun e -> + String.lowercase_ascii (Js.to_string e##.tagName) + <> "input" + || Js.to_bool (e##hasAttribute (Js.string "readonly")) ) + then !toggle_select_assignment id; + true ) ] + [ H.td [date ("start_" ^ hid) id start] + ; H.td [date ("stop_" ^ hid) id stop] + ; H.td [H.txt n_exos] + ; H.td [H.txt n_students] + ; H.td + ~a: + [ H.a_onclick (fun _ -> !assignment_remove id; false) + ; H.a_class ["remove-cross"] ] + [H.txt "\xe2\x9c\x96" (* U+2716 heavy multiplication x *)] (* todo: add common tags *) - ] + ] in let is_assigned status token = let open ES in @@ -677,23 +717,28 @@ let rec teacher_tab token _select _params () = | Open | Closed -> false in let already_assigned_exercises students default = - List.fold_left (fun acc ex -> + List.fold_left + (fun acc ex -> (* fixme: inefficient *) let stat = get_status ex in - if default && - ES.(match stat.assignments.default - with Assigned _ -> true | _ -> false) - || Token.Set.exists (is_assigned stat) students - then SSet.add ex acc else acc) + if + ( default + && ES.( + match stat.assignments.default with + | Assigned _ -> true + | _ -> false) ) + || Token.Set.exists (is_assigned stat) students + then SSet.add ex acc + else acc ) SSet.empty (all_exercises !exercises_index) in let unassigned_students exercises tokens = - List.fold_left (fun acc ex -> + List.fold_left + (fun acc ex -> let st = get_status ex in - Token.Set.filter (fun t -> not (is_assigned st t)) acc) - tokens - exercises + Token.Set.filter (fun t -> not (is_assigned st t)) acc ) + tokens exercises in let already_assigned_students exercises = let all_tokens = @@ -714,21 +759,17 @@ let rec teacher_tab token _select _params () = assignment_line id in let new_assg_id = "new_assignment" in - let new_assg_button = H.button [H.txt [%i"New assignment"]] in + let new_assg_button = H.button [H.txt [%i "New assignment"]] in let table = H.table [] in let new_assg_line = - H.tr ~a:[ - H.a_id new_assg_id; - ] [ - H.td ~a:[H.a_colspan 10] [ new_assg_button ] - ] + H.tr ~a:[H.a_id new_assg_id] [H.td ~a:[H.a_colspan 10] [new_assg_button]] in let new_assignment () = let start, stop = let tm = Unix.(gmtime (time ())) in let tm = Unix.{tm with tm_hour = 0; tm_min = 0; tm_sec = 0} in - fst Unix.(mktime {tm with tm_mday = tm.tm_mday + 1}), - fst Unix.(mktime {tm with tm_mday = tm.tm_mday + 8}) + ( fst Unix.(mktime {tm with tm_mday = tm.tm_mday + 1}) + , fst Unix.(mktime {tm with tm_mday = tm.tm_mday + 8}) ) in let tokens, default = get_student_selection () in let exercises = @@ -737,39 +778,35 @@ let rec teacher_tab token _select _params () = let exercises = SSet.diff exercises (already_assigned_exercises tokens default) in - let line = - make_line (start, stop) tokens exercises default - in + let line = make_line (start, stop) tokens exercises default in let id = !line_n in - Dom.insertBefore (H.toelt table) - (H.toelt line) + Dom.insertBefore (H.toelt table) (H.toelt line) (Js.some (H.toelt new_assg_line)); !assignment_change id; !toggle_select_assignment id in Manip.Ev.onclick new_assg_button (fun _ -> new_assignment (); false); - Manip.replaceChildren table @@ - List.map (fun (assg, tokens, dft, exos) -> make_line assg tokens exos dft) - assignments @ - [new_assg_line]; + Manip.replaceChildren table + @@ List.map + (fun (assg, tokens, dft, exos) -> make_line assg tokens exos dft) + assignments + @ [new_assg_line]; table in let change_exercise_skills op of_meta of_status update_status = status_changes := Hashtbl.fold (fun id () -> - let base = of_meta (Exercise.Index.find !exercises_index id) in - let st = get_status id in - let current = op (ES.get_skills ~base (of_status st)) in - SMap.add id (update_status st (ES.make_skills ~base current))) - selected_exercises - !status_changes; + let base = of_meta (Exercise.Index.find !exercises_index id) in + let st = get_status id in + let current = op (ES.get_skills ~base (of_status st)) in + SMap.add id (update_status st (ES.make_skills ~base current)) ) + selected_exercises !status_changes; fill_exercises_pane (); !update_changed_status () in let add_requirements skills = - change_exercise_skills - (List.rev_append skills) + change_exercise_skills (List.rev_append skills) (fun meta -> meta.Exercise.Meta.requirements) (fun st -> st.ES.skills_prereq) (fun st skills_prereq -> {st with ES.skills_prereq}) @@ -782,8 +819,7 @@ let rec teacher_tab token _select _params () = (fun st skills_prereq -> {st with ES.skills_prereq}) in let add_focus skills = - change_exercise_skills - (List.rev_append skills) + change_exercise_skills (List.rev_append skills) (fun meta -> meta.Exercise.Meta.focus) (fun st -> st.ES.skills_focus) (fun st skills_focus -> {st with ES.skills_focus}) @@ -796,60 +832,65 @@ let rec teacher_tab token _select _params () = (fun st skills_focus -> {st with ES.skills_focus}) in let open_close_button = - H.button ~a:[ - H.a_onclick (fun _ -> - let ids = htbl_keys selected_exercises in - let fstat = - if List.exists (fun id -> - let st = get_status id in - ES.(default_assignment st.assignments = Open)) - ids - then ES.(fun assg -> - (* fixme: invisible change if the exercise is assigned! *) - match default_assignment assg with - | Open -> set_default_assignment assg Closed - | _ -> assg) - else ES.(fun assg -> - (* fixme: invisible change if the exercise is assigned! *) - match default_assignment assg with - | Closed -> set_default_assignment assg Open - | _ -> assg) - in - !exercise_status_change (htbl_keys selected_exercises) fstat; - true) - ] [H.txt [%i"Open/Close"]]; + H.button + ~a: + [ H.a_onclick (fun _ -> + let ids = htbl_keys selected_exercises in + let fstat = + if + List.exists + (fun id -> + let st = get_status id in + ES.(default_assignment st.assignments = Open) ) + ids + then + ES.( + fun assg -> + (* fixme: invisible change if the exercise is assigned! *) + match default_assignment assg with + | Open -> set_default_assignment assg Closed + | _ -> assg) + else + ES.( + fun assg -> + (* fixme: invisible change if the exercise is assigned! *) + match default_assignment assg with + | Closed -> set_default_assignment assg Open + | _ -> assg) + in + !exercise_status_change (htbl_keys selected_exercises) fstat; + true ) ] + [H.txt [%i "Open/Close"]] in let exercise_control_div = - H.div ~a:[H.a_id "exercise_controls"] [ - open_close_button; - H.div ~a:[H.a_class ["filler_h"]] []; - tag_addremove exercise_skills_list_id [%i"required skills"] - (add_requirements) (remove_requirements); - H.div ~a:[H.a_id "skills-arrow"] - [H.txt "\xe2\x87\xa2"]; (* U+21E2, rightwards dashed arrow *) - tag_addremove exercise_skills_list_id [%i"trained skills"] - (add_focus) (remove_focus); - ] + H.div + ~a:[H.a_id "exercise_controls"] + [ open_close_button + ; H.div ~a:[H.a_class ["filler_h"]] [] + ; tag_addremove exercise_skills_list_id [%i "required skills"] + add_requirements remove_requirements + ; H.div ~a:[H.a_id "skills-arrow"] [H.txt "\xe2\x87\xa2"] + ; (* U+21E2, rightwards dashed arrow *) + tag_addremove exercise_skills_list_id [%i "trained skills"] add_focus + remove_focus ] in Manip.appendChild exercises_div exercise_control_div; let assignments_div = H.div [] in let control_div = - H.div ~a:[H.a_id "control_pane"] [ - H.fieldset - ~legend:(H.legend [H.txt [%i"Assignments"]]) - [assignments_div]; - ] + H.div ~a:[H.a_id "control_pane"] + [ H.fieldset + ~legend:(H.legend [H.txt [%i "Assignments"]]) + [assignments_div] ] in let fill_control_div () = - Manip.replaceSelf assignments_div - (assignments_table ()) + Manip.replaceSelf assignments_div (assignments_table ()) in let set_readonly line onoff = let attr = Js.string "readonly" in List.iter (fun e -> - if onoff then e##setAttribute attr (Js.string "") - else e##removeAttribute attr) + if onoff then e##setAttribute attr (Js.string "") + else e##removeAttribute attr ) (descendants_by_tag line "input") in let unselect_assignment id = @@ -861,44 +902,44 @@ let rec teacher_tab token _select _params () = set_readonly line true in let apply_changes () = - Lwt.async @@ fun () -> + Lwt.async + @@ fun () -> let changes_map = - SMap.merge (fun id st0 -> function - | None -> None + SMap.merge + (fun id st0 -> function None -> None | Some st -> - let st0 = match st0 with + let st0 = + match st0 with | Some s -> s | None -> Exercise.Status.(default id) in - if st <> st0 then Some (st0, st) else None) + if st <> st0 then Some (st0, st) else None ) !status_map !status_changes in - let changes = SMap.fold (fun _ x acc -> x::acc) changes_map [] in + let changes = SMap.fold (fun _ x acc -> x :: acc) changes_map [] in let students_changes_map = - Token.Map.merge (fun tok std0 -> function - | None -> None + Token.Map.merge + (fun tok std0 -> function None -> None | Some std -> - let std0 = match std0 with - | Some s -> s - | None -> Student.default tok + let std0 = + match std0 with Some s -> s | None -> Student.default tok in - if std <> std0 then Some (std0, std) else None) + if std <> std0 then Some (std0, std) else None ) !students_map !students_changes in let students_changes = - Token.Map.fold (fun _ x acc -> x::acc) students_changes_map [] + Token.Map.fold (fun _ x acc -> x :: acc) students_changes_map [] in - (if changes = [] then Lwt.return () else - retrieve - (Learnocaml_api.Set_exercise_status (token, changes))) + ( if changes = [] then Lwt.return () + else retrieve (Learnocaml_api.Set_exercise_status (token, changes)) ) >|= fun () -> - (if students_changes = [] then Lwt.return () else - retrieve - (Learnocaml_api.Set_students_list (token, students_changes))) + ( if students_changes = [] then Lwt.return () + else retrieve (Learnocaml_api.Set_students_list (token, students_changes)) + ) >|= fun () -> (* Reload the full tab: a bit more costly, but safer & simpler *) - teacher_tab token _select _params () >|= - Manip.replaceSelf (find_component "learnocaml-main-teacher") + teacher_tab token _select _params () + >|= Manip.replaceSelf (find_component "learnocaml-main-teacher") (* status_map := status_current (); * status_changes := SMap.empty; * Hashtbl.clear selected_exercises; @@ -913,29 +954,33 @@ let rec teacher_tab token _select _params () = in let status_text_div = H.div ~a:[H.a_id "status-text-div"] [] in let actions_div = - H.div ~a:[H.a_id "teacher_menubar"] [ - status_text_div; - H.button ~a:[ - H.a_id "button_apply"; - (* H.a_disabled (); *) - H.a_onclick (fun _ -> apply_changes (); true); - ] [H.txt [%i"Apply"]]; - dropdown ~id:"teacher-actions" ~title:[H.txt [%i"Actions"]] [ - H.ul [ - H.li ~a: [ H.a_onclick (fun _ -> Lwt.async action_new_token; true) ] - [ H.txt [%i"Create new teacher token"] ]; - H.li ~a: [ H.a_onclick (fun _ -> Lwt.async action_csv_export; true) ] - [ H.txt [%i"Download student data as CSV"] ]; - ] - ]; - ] + H.div ~a:[H.a_id "teacher_menubar"] + [ status_text_div + ; H.button + ~a: + [ H.a_id "button_apply" + ; (* H.a_disabled (); *) + H.a_onclick (fun _ -> apply_changes (); true) ] + [H.txt [%i "Apply"]] + ; dropdown ~id:"teacher-actions" + ~title:[H.txt [%i "Actions"]] + [ H.ul + [ H.li + ~a:[H.a_onclick (fun _ -> Lwt.async action_new_token; true)] + [H.txt [%i "Create new teacher token"]] + ; H.li + ~a: + [ H.a_onclick (fun _ -> + Lwt.async action_csv_export; + true ) ] + [H.txt [%i "Download student data as CSV"]] ] ] ] in - (* Implementation of the callbacks *) let select_exercise onoff id = - let class_f, tbl_f = match onoff with - | true -> Manip.addClass, (fun t k -> Hashtbl.replace t k ()) - | false -> Manip.removeClass, Hashtbl.remove + let class_f, tbl_f = + match onoff with + | true -> (Manip.addClass, fun t k -> Hashtbl.replace t k ()) + | false -> (Manip.removeClass, Hashtbl.remove) in tbl_f selected_exercises id; match Manip.by_id (exercise_line_id id) with @@ -943,9 +988,10 @@ let rec teacher_tab token _select _params () = | None -> () in let select_student onoff std = - let class_f, tbl_f = match onoff with - | true -> Manip.addClass, (fun std k -> Hashtbl.replace std k ()) - | false -> Manip.removeClass, Hashtbl.remove + let class_f, tbl_f = + match onoff with + | true -> (Manip.addClass, fun std k -> Hashtbl.replace std k ()) + | false -> (Manip.removeClass, Hashtbl.remove) in tbl_f selected_students std; match Manip.by_id (student_line_id std) with @@ -955,41 +1001,43 @@ let rec teacher_tab token _select _params () = let update_disabled_exercises () = let disabled ex_ids = SSet.iter (Hashtbl.remove selected_exercises) ex_ids; - List.iter (fun ex -> + List.iter + (fun ex -> match Manip.by_id (exercise_line_id ex) with | None -> () | Some el -> - if SSet.mem ex ex_ids then - (Manip.addClass el "disabled"; - Manip.removeClass el "selected") - else Manip.removeClass el "disabled") + if SSet.mem ex ex_ids then ( + Manip.addClass el "disabled"; + Manip.removeClass el "selected" ) + else Manip.removeClass el "disabled" ) (all_exercises !exercises_index) in - let current_assignment = match !selected_assignment with - | Some id -> let _, _, exos, _ = Hashtbl.find assignments_tbl id in exos + let current_assignment = + match !selected_assignment with + | Some id -> + let _, _, exos, _ = Hashtbl.find assignments_tbl id in + exos | None -> SSet.empty in let tokens, default = get_student_selection () in disabled - (SSet.diff - (already_assigned_exercises tokens default) - current_assignment) + (SSet.diff (already_assigned_exercises tokens default) current_assignment) in let set_assignment ?assg ?students ?exos ?default id = - let (assg0, students0, exos0, default0) = Hashtbl.find assignments_tbl id in + let assg0, students0, exos0, default0 = Hashtbl.find assignments_tbl id in let dft x0 = function Some x -> x | None -> x0 in let start, stop = dft assg0 assg in let students = dft students0 students in let exos = dft exos0 exos in let default = dft default0 default in - Hashtbl.replace assignments_tbl id - ((start, stop), students, exos, default); - (match Manip.by_id (assg_line_id id) with - | Some l -> Manip.replaceSelf l (assignment_line id) - | None -> failwith "Assignment line not found"); + Hashtbl.replace assignments_tbl id ((start, stop), students, exos, default); + ( match Manip.by_id (assg_line_id id) with + | Some l -> Manip.replaceSelf l (assignment_line id) + | None -> failwith "Assignment line not found" ); let status = ES.(Assigned {start; stop}) in let exercise_status_changes = - SSet.fold (fun ex_id acc -> + SSet.fold + (fun ex_id acc -> let st = get_status ex_id in let assg = st.ES.assignments in let old_default = assg.ES.default in @@ -999,31 +1047,27 @@ let rec teacher_tab token _select _params () = else old_default in let add tk st tmap = - if st = new_default then tmap - else Token.Map.add tk st tmap + if st = new_default then tmap else Token.Map.add tk st tmap in let token_map = - Token.Map.fold (fun tk _ acc -> + Token.Map.fold + (fun tk _ acc -> if Token.Set.mem tk students then - if default then acc - else Token.Map.add tk status acc + if default then acc else Token.Map.add tk status acc else if Token.Set.mem tk students0 then if default then Token.Map.add tk ES.Closed acc else Token.Map.remove tk acc - else add tk (ES.get_status tk assg) acc) + else add tk (ES.get_status tk assg) acc ) !students_map Token.Map.empty in SMap.add ex_id - ES.{st with assignments = { - token_map; - default = new_default; - }} - acc) - exos - !status_changes + ES.{st with assignments = {token_map; default = new_default}} + acc ) + exos !status_changes in let exercise_status_changes = - SSet.fold (fun ex_id acc -> + SSet.fold + (fun ex_id acc -> let st = get_status ex_id in let assg = st.ES.assignments in let dft_status = @@ -1036,26 +1080,21 @@ let rec teacher_tab token _select _params () = Token.Map.filter (fun _ a -> a <> dft_status) token_map in SMap.add ex_id - ES.{st with assignments = { - token_map; - default = dft_status - }} - acc) - (SSet.diff exos0 exos) - exercise_status_changes + ES.{st with assignments = {token_map; default = dft_status}} + acc ) + (SSet.diff exos0 exos) exercise_status_changes in status_changes := exercise_status_changes; fill_exercises_pane (); - !update_changed_status (); + !update_changed_status () in let update_disabled_students () = let set_enabled onoff student = let el = find_component (student_line_id student) in - if onoff then - Manip.removeClass el "disabled" - else - (Manip.addClass el "disabled"; - Manip.removeClass el "selected") + if onoff then Manip.removeClass el "disabled" + else ( + Manip.addClass el "disabled"; + Manip.removeClass el "selected" ) in let disabled tokens = Token.Set.iter @@ -1065,22 +1104,26 @@ let rec teacher_tab token _select _params () = (fun tk _ -> set_enabled (not (Token.Set.mem tk tokens)) (`Token tk)) !students_map in - let current_assignment, cur_default = match !selected_assignment with + let current_assignment, cur_default = + match !selected_assignment with | Some id -> - let _, std, _, dft = Hashtbl.find assignments_tbl id in std, dft - | None -> Token.Set.empty, false + let _, std, _, dft = Hashtbl.find assignments_tbl id in + (std, dft) + | None -> (Token.Set.empty, false) in disabled (Token.Set.diff (already_assigned_students (htbl_keys selected_exercises)) current_assignment); let has_default = - not cur_default && + (not cur_default) + && try - Hashtbl.iter (fun ex _ -> + Hashtbl.iter + (fun ex _ -> match (get_status ex).ES.assignments.ES.default with | ES.Assigned _ -> raise Exit - | _ -> ()) + | _ -> () ) selected_exercises; false with Exit -> true @@ -1089,244 +1132,249 @@ let rec teacher_tab token _select _params () = in let update_disabled_both () = update_disabled_exercises (); - update_disabled_students (); - in - update_changed_status := begin fun () -> - if SMap.is_empty !status_changes && - Token.Map.is_empty !students_changes then - (Manip.replaceChildren status_text_div []; - Manip.removeClass status_text_div "warning") - else - (Manip.replaceChildren status_text_div [H.txt [%i"Unsaved changes"]]; - Manip.addClass status_text_div "warning") - end; - toggle_selected_exercises := begin - fun ?force ?(update = force=None) ids -> - Lwt.async @@ fun () -> - let ids, onoff = match force with - | Some set -> ids, set - | None -> - let ids = - List.filter (fun id -> - match Manip.by_id (exercise_line_id id) with - | Some elt -> - not (Manip.hasClass elt "disabled") && - not (Manip.hasClass elt "exercise_hidden") - | None -> false) - ids - in - ids, not @@ List.exists (Hashtbl.mem selected_exercises) ids - in - List.iter (select_exercise onoff) ids; - (match !selected_assignment with - | None -> () - | Some aid -> - set_assignment aid ~exos:(SSet.of_list (htbl_keys selected_exercises))); - if update then update_disabled_both (); - Lwt.return_unit - end; - toggle_selected_students := begin - fun ?force ?(update = force=None) students -> - Lwt.async @@ fun () -> - let students, onoff = match force with - | Some set -> students, set - | None -> - let students = - List.filter (fun tk -> - match Manip.by_id (student_line_id tk) with - | Some elt -> not (Manip.hasClass elt "disabled") - | None -> false) - students - in - students, not @@ List.exists (Hashtbl.mem selected_students) students - in - List.iter (select_student onoff) students; - if update then - ((match !selected_assignment with - | None -> () - | Some aid -> - let students, default = get_student_selection () in - set_assignment aid ~students ~default); - update_disabled_exercises ()); - Lwt.return_unit - end; - toggle_select_assignment := begin fun assg_id -> - Lwt.async @@ fun () -> - let select id = - match Manip.by_id (assg_line_id id) with - | None -> () - | Some line -> - let (_assg, students, exos, default) = - Hashtbl.find assignments_tbl id - in - !toggle_selected_exercises ~force:false - (all_exercises !exercises_index); - !toggle_selected_exercises ~force:true (SSet.elements exos); - - !toggle_selected_students ~force:false - (Token.Map.fold (fun tk _ acc -> (`Token tk)::acc) - !students_map - [`Any]); - !toggle_selected_students ~force:true - (Token.Set.fold (fun tk acc -> (`Token tk)::acc) - students - (if default then [`Any] else [])); - - selected_assignment := Some id; - Manip.addClass line "selected"; - set_readonly line false - in - (match !selected_assignment with - | Some aid -> - unselect_assignment aid; - if aid <> assg_id then select assg_id - | None -> - select assg_id); - (match !selected_assignment with - | Some _ -> Manip.disable open_close_button - | None -> Manip.enable open_close_button); - update_disabled_both (); - Lwt.return_unit - end; - exercise_status_change := begin fun ids f -> - status_changes := - List.fold_left (fun acc id -> - let st = get_status id in - SMap.add id ES.{st with assignments = f st.assignments} acc) - !status_changes ids; - fill_exercises_pane (); - update_disabled_exercises (); - !update_changed_status (); - end; - student_change := begin fun _tk () -> - fill_students_pane (); - end; - assignment_change := begin fun assg_id -> - let ((start0, stop0), _, _, _) = Hashtbl.find assignments_tbl assg_id in - let get_date id = - let retr = - match Manip.by_id ("date_"^id), Manip.by_id ("time_"^id) with - | Some d, Some t -> - (try + update_disabled_students () + in + (update_changed_status := + fun () -> + if SMap.is_empty !status_changes && Token.Map.is_empty !students_changes + then ( + Manip.replaceChildren status_text_div []; + Manip.removeClass status_text_div "warning" ) + else ( + Manip.replaceChildren status_text_div [H.txt [%i "Unsaved changes"]]; + Manip.addClass status_text_div "warning" )); + (toggle_selected_exercises := + fun ?force ?(update = force = None) ids -> + Lwt.async + @@ fun () -> + let ids, onoff = + match force with + | Some set -> (ids, set) + | None -> + let ids = + List.filter + (fun id -> + match Manip.by_id (exercise_line_id id) with + | Some elt -> + (not (Manip.hasClass elt "disabled")) + && not (Manip.hasClass elt "exercise_hidden") + | None -> false ) + ids + in + (ids, not @@ List.exists (Hashtbl.mem selected_exercises) ids) + in + List.iter (select_exercise onoff) ids; + ( match !selected_assignment with + | None -> () + | Some aid -> + set_assignment aid + ~exos:(SSet.of_list (htbl_keys selected_exercises)) ); + if update then update_disabled_both (); + Lwt.return_unit); + (toggle_selected_students := + fun ?force ?(update = force = None) students -> + Lwt.async + @@ fun () -> + let students, onoff = + match force with + | Some set -> (students, set) + | None -> + let students = + List.filter + (fun tk -> + match Manip.by_id (student_line_id tk) with + | Some elt -> not (Manip.hasClass elt "disabled") + | None -> false ) + students + in + ( students + , not @@ List.exists (Hashtbl.mem selected_students) students ) + in + List.iter (select_student onoff) students; + if update then ( + ( match !selected_assignment with + | None -> () + | Some aid -> + let students, default = get_student_selection () in + set_assignment aid ~students ~default ); + update_disabled_exercises () ); + Lwt.return_unit); + (toggle_select_assignment := + fun assg_id -> + Lwt.async + @@ fun () -> + let select id = + match Manip.by_id (assg_line_id id) with + | None -> () + | Some line -> + let _assg, students, exos, default = + Hashtbl.find assignments_tbl id + in + !toggle_selected_exercises ~force:false + (all_exercises !exercises_index); + !toggle_selected_exercises ~force:true (SSet.elements exos); + !toggle_selected_students ~force:false + (Token.Map.fold + (fun tk _ acc -> `Token tk :: acc) + !students_map [`Any]); + !toggle_selected_students ~force:true + (Token.Set.fold + (fun tk acc -> `Token tk :: acc) + students + (if default then [`Any] else [])); + selected_assignment := Some id; + Manip.addClass line "selected"; + set_readonly line false + in + ( match !selected_assignment with + | Some aid -> + unselect_assignment aid; + if aid <> assg_id then select assg_id + | None -> select assg_id ); + ( match !selected_assignment with + | Some _ -> Manip.disable open_close_button + | None -> Manip.enable open_close_button ); + update_disabled_both (); Lwt.return_unit); + (exercise_status_change := + fun ids f -> + status_changes := + List.fold_left + (fun acc id -> + let st = get_status id in + SMap.add id ES.{st with assignments = f st.assignments} acc ) + !status_changes ids; + fill_exercises_pane (); + update_disabled_exercises (); + !update_changed_status ()); + (student_change := fun _tk () -> fill_students_pane ()); + (assignment_change := + fun assg_id -> + let (start0, stop0), _, _, _ = Hashtbl.find assignments_tbl assg_id in + let get_date id = + let retr = + match (Manip.by_id ("date_" ^ id), Manip.by_id ("time_" ^ id)) with + | Some d, Some t -> ( + try Some - (Scanf.sscanf (Manip.value d) "%d-%d-%d" - (fun yr mon d -> yr, mon, d), - try Scanf.sscanf (Manip.value t) "%d:%d" (fun hr mn -> hr, mn) - with Scanf.Scan_failure _ | End_of_file -> 0, 0) - with Scanf.Scan_failure _ | End_of_file -> None) - | _ -> None - in - match retr with - | Some ((yr, mon, d), (hr, min)) -> - let t = new%js Js.date_min yr (mon - 1) d hr min in - Some (t##getTime /. 1000.) - | None -> None - in - let start = match get_date ("start_"^assg_line_id assg_id) with - | Some t -> t - | None -> start0 - in - let stop = match get_date ("stop_"^assg_line_id assg_id) with - | Some t -> if t < start then start else t - | None -> stop0 - in - set_assignment assg_id ~assg:(start, stop) - end; - - assignment_remove := begin fun assg_id -> - Lwt.async @@ fun () -> - set_assignment assg_id - ~students:Token.Set.empty - ~exos:SSet.empty; - Hashtbl.remove assignments_tbl assg_id; - selected_assignment := None; - fill_exercises_pane (); - update_disabled_both (); - (match Manip.by_id (assg_line_id assg_id) with - | None -> () - | Some el -> Manip.removeSelf el); - Lwt.return_unit - end; - + ( Scanf.sscanf (Manip.value d) "%d-%d-%d" (fun yr mon d -> + (yr, mon, d) ) + , try + Scanf.sscanf (Manip.value t) "%d:%d" (fun hr mn -> (hr, mn) + ) + with + | Scanf.Scan_failure _ | End_of_file -> (0, 0) ) + with + | Scanf.Scan_failure _ | End_of_file -> None ) + | _ -> None + in + match retr with + | Some ((yr, mon, d), (hr, min)) -> + let t = new%js Js.date_min yr (mon - 1) d hr min in + Some (t##getTime /. 1000.) + | None -> None + in + let start = + match get_date ("start_" ^ assg_line_id assg_id) with + | Some t -> t + | None -> start0 + in + let stop = + match get_date ("stop_" ^ assg_line_id assg_id) with + | Some t -> if t < start then start else t + | None -> stop0 + in + set_assignment assg_id ~assg:(start, stop)); + (assignment_remove := + fun assg_id -> + Lwt.async + @@ fun () -> + set_assignment assg_id ~students:Token.Set.empty ~exos:SSet.empty; + Hashtbl.remove assignments_tbl assg_id; + selected_assignment := None; + fill_exercises_pane (); + update_disabled_both (); + ( match Manip.by_id (assg_line_id assg_id) with + | None -> () + | Some el -> Manip.removeSelf el ); + Lwt.return_unit); let fill_students_progression () = let now = gettimeofday () in let students_assignments = let addl x tok tmap = Token.Map.add tok - (x :: try Token.Map.find tok tmap with Not_found -> []) + (x :: (try Token.Map.find tok tmap with Not_found -> [])) tmap in - Hashtbl.fold (fun _ ((start, stop), tokens, exos, _dft) acc -> + Hashtbl.fold + (fun _ ((start, stop), tokens, exos, _dft) acc -> if start > now then acc - else Token.Set.fold (addl (stop, exos)) tokens acc) + else Token.Set.fold (addl (stop, exos)) tokens acc ) assignments_tbl (Token.Map.map (fun _ -> []) !students_map) in let open_exercises = - SMap.fold (fun ex st acc -> - if ES.(st.assignments.default = Open) then ex::acc else acc) + SMap.fold + (fun ex st acc -> + if ES.(st.assignments.default = Open) then ex :: acc else acc ) !status_map [] |> List.rev in let css_gradient = function | [] -> "background:white" - | _::_ as l -> + | _ :: _ as l -> let step = 100. /. float_of_int (List.length l) in Printf.sprintf "background:linear-gradient(to right,%s)" (String.concat "," - (List.mapi (fun i score -> - Printf.sprintf "%s %.0f%%,%s %.0f%%" - (grade_color score) (float_of_int i *. step) - (grade_color score) (float_of_int (i+1) *. step)) - l)) + (List.mapi + (fun i score -> + Printf.sprintf "%s %.0f%%,%s %.0f%%" (grade_color score) + (float_of_int i *. step) + (grade_color score) + (float_of_int (i + 1) *. step) ) + l)) in let div_assg ~cls grades = H.div ~a:[H.a_class [cls]; H.a_style (css_gradient grades)] [] in - Token.Map.iter (fun tok st -> + Token.Map.iter + (fun tok st -> let parent_div = find_component (student_progression_id tok) in let assgs = Token.Map.find tok students_assignments in let assgs = List.sort compare assgs in let status = st.Student.results in let grades exlist = - List.map (fun ex -> - match SMap.find_opt ex status with - | Some (_, g) -> g - | _ -> None) + List.map + (fun ex -> + match SMap.find_opt ex status with Some (_, g) -> g | _ -> None + ) exlist in - Manip.replaceChildren parent_div @@ - List.map (fun (deadl,exos) -> - div_assg - ~cls:(if deadl < now then "progr-closed" else "progr-assigned") - (grades (SSet.elements exos))) - assgs; + Manip.replaceChildren parent_div + @@ List.map + (fun (deadl, exos) -> + div_assg + ~cls:(if deadl < now then "progr-closed" else "progr-assigned") + (grades (SSet.elements exos)) ) + assgs; if open_exercises <> [] then - Manip.appendChild parent_div @@ - div_assg ~cls:"progr-open" (grades open_exercises)) + Manip.appendChild parent_div + @@ div_assg ~cls:"progr-open" (grades open_exercises) ) !students_map in - let div = - H.div ~a: [H.a_id "learnocaml-main-teacher"] [ - exercises_div; - students_div; - (* skills_div; *) - control_div; - actions_div; - ] + H.div + ~a:[H.a_id "learnocaml-main-teacher"] + [exercises_div; students_div; (* skills_div; *) control_div; actions_div] in let fetch_exercises = retrieve (Learnocaml_api.Exercise_index (Some token)) - >|= fun (index, _) -> - exercises_index := index + >|= fun (index, _) -> exercises_index := index in let fetch_stats = retrieve (Learnocaml_api.Exercise_status_index token) >|= fun statuses -> let map = - List.fold_left (fun m ex -> SMap.add ex.ES.id ex m) - SMap.empty statuses + List.fold_left (fun m ex -> SMap.add ex.ES.id ex m) SMap.empty statuses in status_map := map in @@ -1334,20 +1382,23 @@ let rec teacher_tab token _select _params () = retrieve (Learnocaml_api.Students_list token) >|= fun students -> students_map := - List.fold_left (fun m st -> Token.Map.add st.Student.token st m) + List.fold_left + (fun m st -> Token.Map.add st.Student.token st m) Token.Map.empty students in let content_div = find_component "learnocaml-main-content" in Manip.appendChild content_div div; - Lwt.join [fetch_exercises; fetch_stats; fetch_students] >>= fun () -> + Lwt.join [fetch_exercises; fetch_stats; fetch_students] + >>= fun () -> exercises_index := - Exercise.Index.map_exercises (fun id meta -> + Exercise.Index.map_exercises + (fun id meta -> let st = get_status id in - Exercise.Meta.{ meta with - requirements = - ES.skills_base ~current:meta.requirements st.ES.skills_prereq; - focus = - ES.skills_base ~current:meta.focus st.ES.skills_focus; }) + Exercise.Meta. + { meta with + requirements = + ES.skills_base ~current:meta.requirements st.ES.skills_prereq + ; focus = ES.skills_base ~current:meta.focus st.ES.skills_focus } ) !exercises_index; fill_exercises_pane (); (* fill_skills_pane (); *) diff --git a/src/app/learnocaml_teacher_tab.mli b/src/app/learnocaml_teacher_tab.mli index a373b9021..3d7857ba2 100644 --- a/src/app/learnocaml_teacher_tab.mli +++ b/src/app/learnocaml_teacher_tab.mli @@ -6,6 +6,9 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -val teacher_tab: - Learnocaml_data.Token.t -> (unit -> 'a Lwt.t) -> 'b -> unit -> - [> Html_types.div ] Tyxml_js.Html5.elt Lwt.t +val teacher_tab : + Learnocaml_data.Token.t + -> (unit -> 'a Lwt.t) + -> 'b + -> unit + -> [> Html_types.div] Tyxml_js.Html5.elt Lwt.t diff --git a/src/app/server_caller.ml b/src/app/server_caller.ml index 4b1837588..383b8150e 100644 --- a/src/app/server_caller.ml +++ b/src/app/server_caller.ml @@ -32,61 +32,58 @@ let fetch ?message filename = Lwt.fail (Cannot_fetch msg)) *) module Json_codec = struct - let decode enc s = - Js._JSON##(parse (Js.string s)) |> - Json_repr_browser.Json_encoding.destruct enc + Js._JSON ## (parse (Js.string s)) + |> Json_repr_browser.Json_encoding.destruct enc let encode ?minify:_ enc x = let json = Json_repr_browser.Json_encoding.construct enc x in - Js.to_string Js._JSON##(stringify json) - + Js.to_string Js._JSON ## (stringify json) end module Api_client = Learnocaml_api.Client (Json_codec) -type request_error = [ - | `Unreachable of string +type request_error = + [ `Unreachable of string | `Not_found of string | `Http_error of int * string | `Exception of exn - | `Invalid_response of exn -] + | `Invalid_response of exn ] let string_of_error = function | `Unreachable "" -> "Server unreachable" | `Unreachable s -> "Server unreachable: " ^ s | `Not_found s -> "URL not found: " ^ s - | `Http_error (code, s) -> - Printf.sprintf "HTTP error (%d): %s" code s - | `Exception e -> - "Server request failed: " ^ Printexc.to_string e + | `Http_error (code, s) -> Printf.sprintf "HTTP error (%d): %s" code s + | `Exception e -> "Server request failed: " ^ Printexc.to_string e | `Invalid_response e -> "Could not decode server response: " ^ Printexc.to_string e let () = - Printexc.register_printer @@ function + Printexc.register_printer + @@ function | Json_encoding.Cannot_destruct (path, e) -> - Some (Printf.sprintf "JSON error at %s: %s" - (Json_query.json_pointer_of_path path) - (Printexc.to_string e)) + Some + (Printf.sprintf "JSON error at %s: %s" + (Json_query.json_pointer_of_path path) + (Printexc.to_string e)) | _ -> None -let urlpath p = - String.concat "/" (Learnocaml_config.api_server :: p) +let urlpath p = String.concat "/" (Learnocaml_config.api_server :: p) let request req = let do_req = function - | { Learnocaml_api.meth = `GET; path; args } -> - Lwt_request.get ?headers:None ~url:(urlpath path) ~args:args - | { Learnocaml_api.meth = `POST body; path; args } -> + | {Learnocaml_api.meth = `GET; path; args} -> + Lwt_request.get ?headers:None ~url:(urlpath path) ~args + | {Learnocaml_api.meth = `POST body; path; args} -> let get_args = match args with [] -> None | a -> Some a in - Lwt_request.post ?headers:None ?get_args - ~url:(urlpath path) ~body:(Some body) + Lwt_request.post ?headers:None ?get_args ~url:(urlpath path) + ~body:(Some body) in Lwt.catch (fun () -> - Api_client.make_request (fun http_request -> - Lwt.catch (fun () -> do_req http_request >|= fun body -> Ok (body)) + Api_client.make_request + (fun http_request -> + Lwt.catch (fun () -> do_req http_request >|= fun body -> Ok body) @@ function | Lwt_request.Request_failed (0, s) -> Lwt.return (Error (`Unreachable s)) @@ -94,31 +91,24 @@ let request req = Lwt.return (Error (`Not_found s)) | Lwt_request.Request_failed (code, s) -> Lwt.return (Error (`Http_error (code, s))) - | e -> - Lwt.return (Error (`Exception e))) - req) - @@ fun e -> - Lwt.return (Error (`Invalid_response e)) + | e -> Lwt.return (Error (`Exception e)) ) + req ) + @@ fun e -> Lwt.return (Error (`Invalid_response e)) exception Cannot_fetch of string let request_exn req = - request req >>= function + request req + >>= function | Ok x -> Lwt.return x - | Error e -> - Lwt.fail (Cannot_fetch (string_of_error e)) + | Error e -> Lwt.fail (Cannot_fetch (string_of_error e)) -let fetch_lesson_index () = - request_exn (Learnocaml_api.Lesson_index ()) +let fetch_lesson_index () = request_exn (Learnocaml_api.Lesson_index ()) -let fetch_lesson id = - request_exn (Learnocaml_api.Lesson id) +let fetch_lesson id = request_exn (Learnocaml_api.Lesson id) -let fetch_exercise token id = - request_exn (Learnocaml_api.Exercise (token,id)) +let fetch_exercise token id = request_exn (Learnocaml_api.Exercise (token, id)) -let fetch_tutorial_index () = - request_exn (Learnocaml_api.Tutorial_index ()) +let fetch_tutorial_index () = request_exn (Learnocaml_api.Tutorial_index ()) -let fetch_tutorial id = - request_exn (Learnocaml_api.Tutorial id) +let fetch_tutorial id = request_exn (Learnocaml_api.Tutorial id) diff --git a/src/app/server_caller.mli b/src/app/server_caller.mli index ee9473af4..338f1b023 100644 --- a/src/app/server_caller.mli +++ b/src/app/server_caller.mli @@ -8,26 +8,31 @@ open Learnocaml_data -type request_error = [ - | `Unreachable of string +type request_error = + [ `Unreachable of string | `Not_found of string | `Http_error of int * string | `Exception of exn - | `Invalid_response of exn -] + | `Invalid_response of exn ] -val string_of_error: request_error -> string +val string_of_error : request_error -> string -val request: 'a Learnocaml_api.request -> ('a, request_error) result Lwt.t +val request : 'a Learnocaml_api.request -> ('a, request_error) result Lwt.t exception Cannot_fetch of string -val request_exn: 'a Learnocaml_api.request -> 'a Lwt.t -val[@deprecated] fetch_exercise: - Token.t option -> Exercise.id -> (Exercise.Meta.t * Exercise.t * float option) Lwt.t +val request_exn : 'a Learnocaml_api.request -> 'a Lwt.t -val[@deprecated] fetch_lesson_index: unit -> Lesson.Index.t Lwt.t -val[@deprecated] fetch_lesson : string -> Lesson.t Lwt.t +val fetch_exercise : + Token.t option + -> Exercise.id + -> (Exercise.Meta.t * Exercise.t * float option) Lwt.t + [@@deprecated] -val[@deprecated] fetch_tutorial_index : unit -> Tutorial.Index.t Lwt.t -val[@deprecated] fetch_tutorial : string -> Tutorial.t Lwt.t +val fetch_lesson_index : unit -> Lesson.Index.t Lwt.t [@@deprecated] + +val fetch_lesson : string -> Lesson.t Lwt.t [@@deprecated] + +val fetch_tutorial_index : unit -> Tutorial.Index.t Lwt.t [@@deprecated] + +val fetch_tutorial : string -> Tutorial.t Lwt.t [@@deprecated] diff --git a/src/grader/grader_cli.ml b/src/grader/grader_cli.ml index 709f1a641..528faf3f6 100644 --- a/src/grader/grader_cli.ml +++ b/src/grader/grader_cli.ml @@ -7,13 +7,21 @@ * included LICENSE file for details. *) let display_std_outputs = ref false + let dump_outputs = ref None + let dump_reports = ref None + let display_callback = ref false + let display_outcomes = ref false + let grade_student = ref None + let individual_timeout = ref None + let display_reports = ref false + let dump_dot = ref None open Lwt.Infix @@ -21,12 +29,12 @@ open Lwt.Infix let read_exercise exercise_dir = let read_field field = let fn = Filename.concat exercise_dir field in - Lwt_unix.file_exists fn >>= fun exists -> - if not exists then - Lwt.return None + Lwt_unix.file_exists fn + >>= fun exists -> + if not exists then Lwt.return None else - Lwt_io.with_file ~mode:Lwt_io.Input fn Lwt_io.read >>= fun content -> - Lwt.return (Some content) + Lwt_io.with_file ~mode:Lwt_io.Input fn Lwt_io.read + >>= fun content -> Lwt.return (Some content) in Learnocaml_exercise.read_lwt ~read_field ~id:(Filename.basename exercise_dir) @@ -34,153 +42,160 @@ let read_exercise exercise_dir = let remove_trailing_slash s = let len = String.length s in - if len <> 0 && s.[len-1] = '/' then String.sub s 0 (len-1) else s + if len <> 0 && s.[len - 1] = '/' then String.sub s 0 (len - 1) else s let read_student_file exercise_dir path = let fn = - if Filename.is_relative path - then Filename.concat exercise_dir path - else path in - Lwt_unix.file_exists fn >>= fun exists -> - if not exists - then (Format.eprintf "Cannot find '%s': No such file@." fn; exit 1) - else - Lwt_io.with_file ~mode:Lwt_io.Input fn Lwt_io.read + if Filename.is_relative path then Filename.concat exercise_dir path + else path + in + Lwt_unix.file_exists fn + >>= fun exists -> + if not exists then ( + Format.eprintf "Cannot find '%s': No such file@." fn; + exit 1 ) + else Lwt_io.with_file ~mode:Lwt_io.Input fn Lwt_io.read -let grade ?(print_result=false) ?dirname meta exercise output_json = +let grade ?(print_result = false) ?dirname meta exercise output_json = Lwt.catch (fun () -> - let code_to_grade = match !grade_student with - | Some path -> read_student_file (Sys.getcwd ()) path - | None -> - Lwt.return (Learnocaml_exercise.(decipher File.solution exercise)) in - let callback = - if !display_callback then Some (Printf.eprintf "[ %s ]%!\r\027[K") else None in - let timeout = !individual_timeout in - code_to_grade >>= fun code -> - Grading_cli.get_grade ?callback ?timeout ?dirname exercise code - >>= fun (result, stdout_contents, stderr_contents, outcomes) -> - flush stderr; - match result with - | Error exn -> - let dump_error ppf = - begin match Grading.string_of_exn exn with - | Some msg -> - Format.fprintf ppf "%s@." msg - | None -> - Format.fprintf ppf "%a@." Location.report_exception exn - end; - if stdout_contents <> "" then begin - Format.fprintf ppf "grader stdout:@.%s@." stdout_contents - end ; - if stderr_contents <> "" then begin - Format.fprintf ppf "grader stderr:@.%s@." stderr_contents - end ; - if outcomes <> "" then begin - Format.fprintf ppf "grader outcomes:@.%s@." outcomes - end in - begin match !dump_outputs with - | None -> () - | Some prefix -> - let oc = open_out (prefix ^ ".error") in - dump_error (Format.formatter_of_out_channel oc) ; - close_out oc - end ; - dump_error Format.err_formatter ; - Lwt.return (Error (-1)) - | Ok report -> - let (max, failure) = Learnocaml_report.result report in - if !display_reports then - Learnocaml_report.print (Format.formatter_of_out_channel stderr) report; - begin match !dump_reports with - | None -> () - | Some prefix -> - let oc = open_out (prefix ^ ".report.txt") in - Learnocaml_report.print (Format.formatter_of_out_channel oc) report ; - close_out oc ; - let oc = open_out (prefix ^ ".report.html") in - Learnocaml_report.output_html (Format.formatter_of_out_channel oc) report ; - close_out oc - end ; - if stderr_contents <> "" then begin - begin match !dump_outputs with - | None -> () - | Some prefix -> - let oc = open_out (prefix ^ ".stderr") in - output_string oc stderr_contents ; - close_out oc - end ; - if !display_std_outputs then - Format.eprintf "%s" stderr_contents - end ; - if stdout_contents <> "" then begin - begin match !dump_outputs with - | None -> () - | Some prefix -> - let oc = open_out (prefix ^ ".stdout") in - output_string oc stdout_contents ; - close_out oc - end ; - if !display_std_outputs then - Format.printf "%s" stdout_contents - end ; - if outcomes <> "" then begin - begin match !dump_outputs with - | None -> () - | Some prefix -> - let oc = open_out (prefix ^ ".outcomes") in - output_string oc outcomes ; - close_out oc - end ; - if !display_outcomes then - Format.printf "%s" outcomes - end ; - if failure then begin - if print_result then - Printf.eprintf "%-30s - Failure - %d points\n%!" - Learnocaml_exercise.(access File.id exercise) max; - Lwt.return (Error max) - end - else begin - if print_result then - Printf.eprintf "%-30s - Success - %d points\n%!" - Learnocaml_exercise.(access File.id exercise) max; - match output_json with - | None -> - Lwt.return (Ok ()) - | Some json_file -> - let json = - Json_encoding.(construct (tup3 Learnocaml_data.Exercise.Meta.enc Learnocaml_exercise.encoding (option float))) - (meta, Learnocaml_exercise.(update File.max_score max exercise), None) - in - let json = match json with - | `A _ | `O _ as d -> d - | v -> `A [ v ] in - let str = Ezjsonm.to_string ~minify:false (json :> Ezjsonm.t) in - Lwt_utils.mkdir_p (Filename.dirname json_file) >>= fun () -> - Lwt_io.with_file ~mode: Lwt_io.Output json_file @@ fun chan -> - Lwt_io.write chan str >>= fun () -> - Lwt.return (Ok ()) - end) + let code_to_grade = + match !grade_student with + | Some path -> read_student_file (Sys.getcwd ()) path + | None -> + Lwt.return Learnocaml_exercise.(decipher File.solution exercise) + in + let callback = + if !display_callback then Some (Printf.eprintf "[ %s ]%!\r\027[K") + else None + in + let timeout = !individual_timeout in + code_to_grade + >>= fun code -> + Grading_cli.get_grade ?callback ?timeout ?dirname exercise code + >>= fun (result, stdout_contents, stderr_contents, outcomes) -> + flush stderr; + match result with + | Error exn -> + let dump_error ppf = + ( match Grading.string_of_exn exn with + | Some msg -> Format.fprintf ppf "%s@." msg + | None -> Format.fprintf ppf "%a@." Location.report_exception exn + ); + if stdout_contents <> "" then + Format.fprintf ppf "grader stdout:@.%s@." stdout_contents; + if stderr_contents <> "" then + Format.fprintf ppf "grader stderr:@.%s@." stderr_contents; + if outcomes <> "" then + Format.fprintf ppf "grader outcomes:@.%s@." outcomes + in + ( match !dump_outputs with + | None -> () + | Some prefix -> + let oc = open_out (prefix ^ ".error") in + dump_error (Format.formatter_of_out_channel oc); + close_out oc ); + dump_error Format.err_formatter; + Lwt.return (Error (-1)) + | Ok report -> + let max, failure = Learnocaml_report.result report in + if !display_reports then + Learnocaml_report.print + (Format.formatter_of_out_channel stderr) + report; + ( match !dump_reports with + | None -> () + | Some prefix -> + let oc = open_out (prefix ^ ".report.txt") in + Learnocaml_report.print + (Format.formatter_of_out_channel oc) + report; + close_out oc; + let oc = open_out (prefix ^ ".report.html") in + Learnocaml_report.output_html + (Format.formatter_of_out_channel oc) + report; + close_out oc ); + if stderr_contents <> "" then ( + ( match !dump_outputs with + | None -> () + | Some prefix -> + let oc = open_out (prefix ^ ".stderr") in + output_string oc stderr_contents; + close_out oc ); + if !display_std_outputs then Format.eprintf "%s" stderr_contents ); + if stdout_contents <> "" then ( + ( match !dump_outputs with + | None -> () + | Some prefix -> + let oc = open_out (prefix ^ ".stdout") in + output_string oc stdout_contents; + close_out oc ); + if !display_std_outputs then Format.printf "%s" stdout_contents ); + if outcomes <> "" then ( + ( match !dump_outputs with + | None -> () + | Some prefix -> + let oc = open_out (prefix ^ ".outcomes") in + output_string oc outcomes; close_out oc ); + if !display_outcomes then Format.printf "%s" outcomes ); + if failure then ( + if print_result then + Printf.eprintf "%-30s - Failure - %d points\n%!" + Learnocaml_exercise.(access File.id exercise) + max; + Lwt.return (Error max) ) + else ( + if print_result then + Printf.eprintf "%-30s - Success - %d points\n%!" + Learnocaml_exercise.(access File.id exercise) + max; + match output_json with + | None -> Lwt.return (Ok ()) + | Some json_file -> + let json = + Json_encoding.( + construct + (tup3 Learnocaml_data.Exercise.Meta.enc + Learnocaml_exercise.encoding (option float))) + ( meta + , Learnocaml_exercise.(update File.max_score max exercise) + , None ) + in + let json = + match json with (`A _ | `O _) as d -> d | v -> `A [v] + in + let str = + Ezjsonm.to_string ~minify:false (json :> Ezjsonm.t) + in + Lwt_utils.mkdir_p (Filename.dirname json_file) + >>= fun () -> + Lwt_io.with_file ~mode:Lwt_io.Output json_file + @@ fun chan -> + Lwt_io.write chan str >>= fun () -> Lwt.return (Ok ()) ) ) (fun exn -> - begin match !dump_outputs with - | None -> () - | Some prefix -> - let oc = open_out (prefix ^ ".error") in - Format.fprintf - (Format.formatter_of_out_channel oc) - "%a@!" Location.report_exception exn ; - close_out oc - end ; - Format.eprintf "%a" Location.report_exception exn ; - Lwt.return (Error (-1))) + ( match !dump_outputs with + | None -> () + | Some prefix -> + let oc = open_out (prefix ^ ".error") in + Format.fprintf + (Format.formatter_of_out_channel oc) + "%a@!" Location.report_exception exn; + close_out oc ); + Format.eprintf "%a" Location.report_exception exn; + Lwt.return (Error (-1)) ) -let grade_from_dir ?(print_result=false) exercise_dir output_json = +let grade_from_dir ?(print_result = false) exercise_dir output_json = let exercise_dir = remove_trailing_slash exercise_dir in - read_exercise exercise_dir >>= fun exo -> - Lwt_io.(with_file ~mode:Input (String.concat Filename.dir_sep [exercise_dir; "meta.json"]) read) >>= fun content -> - let meta = (match content with - | "" -> `O [] - | s -> Ezjsonm.from_string s) - |> Json_encoding.destruct Learnocaml_data.Exercise.Meta.enc in + read_exercise exercise_dir + >>= fun exo -> + Lwt_io.( + with_file ~mode:Input + (String.concat Filename.dir_sep [exercise_dir; "meta.json"]) + read) + >>= fun content -> + let meta = + (match content with "" -> `O [] | s -> Ezjsonm.from_string s) + |> Json_encoding.destruct Learnocaml_data.Exercise.Meta.enc + in grade ~print_result ~dirname:exercise_dir meta exo output_json diff --git a/src/grader/grader_cli.mli b/src/grader/grader_cli.mli index e66095ef7..8d9ed634e 100644 --- a/src/grader/grader_cli.mli +++ b/src/grader/grader_cli.mli @@ -8,40 +8,43 @@ (** {2 Configuration options} *) +val display_std_outputs : bool ref (** Should stdout / stderr of the grader be echoed *) -val display_std_outputs: bool ref +val dump_outputs : string option ref (** Should outputs of the grader be saved and where *) -val dump_outputs: string option ref +val dump_reports : string option ref (** Should the reports be saved and where *) -val dump_reports: string option ref +val display_callback : bool ref (** Should the message from 'test.ml' be displayed on stdout ? *) -val display_callback: bool ref +val display_outcomes : bool ref (** Should compiler outcome be printed ? *) -val display_outcomes: bool ref +val grade_student : string option ref (** Should the tool grade a student file instead of 'solution.ml' ? *) -val grade_student: string option ref +val individual_timeout : int option ref (** Should each test be run with a specific timeout (in secs) ? *) -val individual_timeout: int option ref +val display_reports : bool ref (** Display reports to stderr *) -val display_reports: bool ref +val dump_dot : string option ref (** Should the tool generate and dump a dependency graph of the exercises and where *) -val dump_dot: string option ref (** {2 Functions} *) +val grade : + ?print_result:bool + -> ?dirname:string + -> Learnocaml_data.Exercise.Meta.t + -> Learnocaml_exercise.t + -> string option + -> (unit, int) result Lwt.t (** Runs the grading process *) -val grade: - ?print_result:bool -> ?dirname:string -> Learnocaml_data.Exercise.Meta.t -> Learnocaml_exercise.t -> string option -> - (unit, int) result Lwt.t -val grade_from_dir: - ?print_result:bool -> string -> string option -> - (unit, int) result Lwt.t +val grade_from_dir : + ?print_result:bool -> string -> string option -> (unit, int) result Lwt.t diff --git a/src/grader/grader_jsoo_messages.ml b/src/grader/grader_jsoo_messages.ml index f6b71160a..2afbfe19d 100644 --- a/src/grader/grader_jsoo_messages.ml +++ b/src/grader/grader_jsoo_messages.ml @@ -6,9 +6,8 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -type to_worker = - { exercise : Learnocaml_exercise.t ; - solution : string } +type to_worker = {exercise : Learnocaml_exercise.t; solution : string} + type from_worker = | Callback of string | Answer of Learnocaml_report.t * string * string * string @@ -17,26 +16,21 @@ open Json_encoding let to_worker_enc = conv - (fun { solution ; exercise } -> (solution, exercise)) - (fun (solution, exercise) -> { solution ; exercise }) - (obj2 - (req "solution" string) - (req "exercise" Learnocaml_exercise.encoding)) + (fun {solution; exercise} -> (solution, exercise)) + (fun (solution, exercise) -> {solution; exercise}) + (obj2 (req "solution" string) (req "exercise" Learnocaml_exercise.encoding)) let from_worker_enc = union [ case (obj4 (req "report" Learnocaml_report.enc) - (dft "stdout" string "") - (dft "stderr" string "") + (dft "stdout" string "") (dft "stderr" string "") (dft "outcomes" string "")) (function | Answer (rep, out, err, msgs) -> Some (rep, out, err, msgs) | Callback _ -> None) - (fun (rep, out, err, msgs) -> Answer (rep, out, err, msgs)) ; - case string - (function - | Answer _ -> None - | Callback msg -> Some msg) + (fun (rep, out, err, msgs) -> Answer (rep, out, err, msgs)) + ; case string + (function Answer _ -> None | Callback msg -> Some msg) (fun msg -> Callback msg) ] diff --git a/src/grader/grader_jsoo_messages.mli b/src/grader/grader_jsoo_messages.mli index c451a2e8f..42415d75f 100644 --- a/src/grader/grader_jsoo_messages.mli +++ b/src/grader/grader_jsoo_messages.mli @@ -6,12 +6,12 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -type to_worker = - { exercise : Learnocaml_exercise.t ; - solution : string } +type to_worker = {exercise : Learnocaml_exercise.t; solution : string} + type from_worker = | Callback of string | Answer of Learnocaml_report.t * string * string * string val to_worker_enc : to_worker Json_encoding.encoding + val from_worker_enc : from_worker Json_encoding.encoding diff --git a/src/grader/grader_jsoo_worker.ml b/src/grader/grader_jsoo_worker.ml index cfcbc8379..216c9dd6f 100644 --- a/src/grader/grader_jsoo_worker.ml +++ b/src/grader/grader_jsoo_worker.ml @@ -11,51 +11,58 @@ open Js_of_ocaml let get_grade ?callback exo solution = let path = "/grading_cmis" in let root = - OCamlRes.Res.merge - Embedded_cmis.root - Embedded_grading_cmis.root in - Sys_js.mount ~path - (fun ~prefix:_ ~path -> - match OCamlRes.Res.find (OCamlRes.Path.of_string path) root with - | cmi -> - Js.Unsafe.set cmi (Js.string "t") 9 ; (* XXX hack *) - Some cmi - | exception Not_found -> None) ; - Config.load_path := [ path ] ; - Toploop_jsoo.initialize () ; + OCamlRes.Res.merge Embedded_cmis.root Embedded_grading_cmis.root + in + Sys_js.mount ~path (fun ~prefix:_ ~path -> + match OCamlRes.Res.find (OCamlRes.Path.of_string path) root with + | cmi -> + Js.Unsafe.set cmi (Js.string "t") 9; + (* XXX hack *) + Some cmi + | exception Not_found -> None ); + Config.load_path := [path]; + Toploop_jsoo.initialize (); let divert name chan cb = let redirection = Toploop_jsoo.redirect_channel name chan cb in - fun () -> Toploop_jsoo.stop_channel_redirection redirection in + fun () -> Toploop_jsoo.stop_channel_redirection redirection + in Grading.get_grade ?callback ~divert exo solution open Grader_jsoo_messages let () = - (match Js_utils.get_lang() with Some l -> Ocplib_i18n.set_lang l | None -> ()); - Worker.set_onmessage @@ fun (json : Json_repr_browser.Repr.value) -> - let { exercise ; solution } = - Json_repr_browser.Json_encoding.destruct to_worker_enc json in + ( match Js_utils.get_lang () with + | Some l -> Ocplib_i18n.set_lang l + | None -> () ); + Worker.set_onmessage + @@ fun (json : Json_repr_browser.Repr.value) -> + let {exercise; solution} = + Json_repr_browser.Json_encoding.destruct to_worker_enc json + in let callback msg = let msg = Callback msg in let json = Json_repr_browser.Json_encoding.construct from_worker_enc msg in - Worker.post_message json in + Worker.post_message json + in let ans = let result, stdout, stderr, outcomes = - get_grade ~callback exercise solution in + get_grade ~callback exercise solution + in match result with - | Ok report -> - Answer (report, stdout, stderr, outcomes) + | Ok report -> Answer (report, stdout, stderr, outcomes) | Error exn -> - let msg = match exn with - | Grading.User_code_error { Toploop_results.msg ; _ } -> - [%i"Error in your solution:\n"] ^ msg - | Grading.Internal_error (step, { Toploop_results.msg ; _ }) -> - [%i"Error in the exercise "] ^ step ^ "\n" ^ msg + let msg = + match exn with + | Grading.User_code_error {Toploop_results.msg; _} -> + [%i "Error in your solution:\n"] ^ msg + | Grading.Internal_error (step, {Toploop_results.msg; _}) -> + [%i "Error in the exercise "] ^ step ^ "\n" ^ msg | Grading.Invalid_grader -> - [%i"Internal error:\nThe grader did not return a report."] - | exn -> - [%i"Unexpected error:\n"] ^ Printexc.to_string exn in - let report = Learnocaml_report.[ Message ([ Code msg ], Failure) ] in - Answer (report, stdout, stderr, outcomes) in + [%i "Internal error:\nThe grader did not return a report."] + | exn -> [%i "Unexpected error:\n"] ^ Printexc.to_string exn + in + let report = Learnocaml_report.[Message ([Code msg], Failure)] in + Answer (report, stdout, stderr, outcomes) + in let json = Json_repr_browser.Json_encoding.construct from_worker_enc ans in Worker.post_message json diff --git a/src/grader/grading.ml b/src/grader/grading.ml index dce50a237..b8bbf981d 100644 --- a/src/grader/grading.ml +++ b/src/grader/grading.ml @@ -7,19 +7,21 @@ * included LICENSE file for details. *) exception Internal_error of string * Toploop_ext.error + exception User_code_error of Toploop_ext.error + exception Invalid_grader let string_of_exn = function | Internal_error (msg, error) -> let msg = - Printf.sprintf [%if"Exercise definition error %s:\n%s\n%!"] - msg error.Toploop_ext.msg + Printf.sprintf [%if "Exercise definition error %s:\n%s\n%!"] msg + error.Toploop_ext.msg in - Some msg + Some msg | User_code_error error -> let msg = - Printf.sprintf [%if"Error in user code:\n\n%s\n%!"] + Printf.sprintf [%if "Error in user code:\n\n%s\n%!"] error.Toploop_ext.msg in Some msg @@ -29,179 +31,175 @@ let () = Location.register_error_of_exn (fun exn -> match string_of_exn exn with | Some msg -> - Some {Location.loc = Location.none ; sub = [] ; - msg ; if_highlight = msg } - | None -> None) - + Some {Location.loc = Location.none; sub = []; msg; if_highlight = msg} + | None -> None ) -let internal_error name err = - raise (Internal_error (name, err)) +let internal_error name err = raise (Internal_error (name, err)) -let user_code_error err = - raise (User_code_error err) +let user_code_error err = raise (User_code_error err) -let get_grade - ?callback ?timeout ?(dirname="") ~divert +let get_grade ?callback ?timeout ?(dirname = "") ~divert (exo : Learnocaml_exercise.t) code = - let file f = String.concat Filename.dir_sep [dirname; f] in - let print_outcome = true in let outcomes_buffer = Buffer.create 503 in - let ppf_answer = - Format.formatter_of_buffer outcomes_buffer in - + let ppf_answer = Format.formatter_of_buffer outcomes_buffer in let stderr_buffer = Buffer.create 503 in let stdout_buffer = Buffer.create 503 in - let flush_stderr = ref - (divert "ERR" stderr (Buffer.add_string stderr_buffer)) in - let flush_stdout = ref - (divert "OUT" stdout (Buffer.add_string stdout_buffer)) in - + let flush_stderr = + ref (divert "ERR" stderr (Buffer.add_string stderr_buffer)) + in + let flush_stdout = + ref (divert "OUT" stdout (Buffer.add_string stdout_buffer)) + in let callback = match callback with | None -> None | Some callback -> - Some (fun msg -> - !flush_stderr () ; - !flush_stdout () ; - callback msg ; + Some + (fun msg -> + !flush_stderr (); + !flush_stdout (); + callback msg; flush_stderr := - divert "ERR" stderr (Buffer.add_string stderr_buffer) ; + divert "ERR" stderr (Buffer.add_string stderr_buffer); flush_stdout := - divert "OUT" stdout (Buffer.add_string stdout_buffer)) in - + divert "OUT" stdout (Buffer.add_string stdout_buffer) ) + in let set_progress = match callback with - | None -> (fun _ -> ()) - | Some set_progress -> set_progress in - - let handle_error ?(warn = fun _ -> ()) fail = - function + | None -> fun _ -> () + | Some set_progress -> set_progress + in + let handle_error ?(warn = fun _ -> ()) fail = function | Toploop_ext.Ok (s, w) -> - warn w ; - if not s then begin - !flush_stderr () ; - !flush_stdout () ; + warn w; + if not s then ( + !flush_stderr (); + !flush_stdout (); let msg = String.concat "\n" - (List.map Buffer.contents [stderr_buffer; stdout_buffer; outcomes_buffer]) - in fail { Toploop_ext.msg ; locs = [] ; if_highlight = msg } - end + (List.map Buffer.contents + [stderr_buffer; stdout_buffer; outcomes_buffer]) + in + fail {Toploop_ext.msg; locs = []; if_highlight = msg} ) | Toploop_ext.Error (err, w) -> - warn w ; - !flush_stderr () ; - !flush_stdout () ; - fail err in - - let result = try - handle_error (internal_error [%i"while preparing the tests"]) @@ - Toploop_ext.use_string ~print_outcome ~ppf_answer - {|let print_html _ = assert false|}; - - set_progress [%i"Loading the prelude."] ; - handle_error (internal_error [%i"while loading the prelude"]) @@ - Toploop_ext.use_string ~print_outcome ~ppf_answer ~filename:(file "prelude.ml") - (Learnocaml_exercise.(decipher File.prelude exo)) ; - - set_progress [%i"Preparing the test environment."] ; - handle_error (internal_error [%i"while preparing the tests"]) @@ - Toploop_ext.use_string ~print_outcome ~ppf_answer ~filename:(file "prepare.ml") - (Learnocaml_exercise.(decipher File.prepare exo)) ; - - set_progress [%i"Loading your code."] ; - handle_error user_code_error @@ - Toploop_ext.use_mod_string ~print_outcome ~ppf_answer ~modname:"Code" - ~filename:(file "solution.ml") code ; - - set_progress [%i"Loading the solution."] ; - handle_error (internal_error [%i"while loading the solution"]) @@ - Toploop_ext.use_mod_string ~print_outcome ~ppf_answer ~modname:"Solution" - (Learnocaml_exercise.(decipher File.solution exo)) ; - - set_progress [%i"Preparing to launch the tests."] ; - Introspection.allow_introspection ~divert ; - Introspection.insert_mod_ast_in_env ~var_name: "code_ast" code ; + warn w; !flush_stderr (); !flush_stdout (); fail err + in + let result = + try + handle_error (internal_error [%i "while preparing the tests"]) + @@ Toploop_ext.use_string ~print_outcome ~ppf_answer + {|let print_html _ = assert false|}; + set_progress [%i "Loading the prelude."]; + handle_error (internal_error [%i "while loading the prelude"]) + @@ Toploop_ext.use_string ~print_outcome ~ppf_answer + ~filename:(file "prelude.ml") + Learnocaml_exercise.(decipher File.prelude exo); + set_progress [%i "Preparing the test environment."]; + handle_error (internal_error [%i "while preparing the tests"]) + @@ Toploop_ext.use_string ~print_outcome ~ppf_answer + ~filename:(file "prepare.ml") + Learnocaml_exercise.(decipher File.prepare exo); + set_progress [%i "Loading your code."]; + handle_error user_code_error + @@ Toploop_ext.use_mod_string ~print_outcome ~ppf_answer ~modname:"Code" + ~filename:(file "solution.ml") code; + set_progress [%i "Loading the solution."]; + handle_error (internal_error [%i "while loading the solution"]) + @@ Toploop_ext.use_mod_string ~print_outcome ~ppf_answer + ~modname:"Solution" + Learnocaml_exercise.(decipher File.solution exo); + set_progress [%i "Preparing to launch the tests."]; + Introspection.allow_introspection ~divert; + Introspection.insert_mod_ast_in_env ~var_name:"code_ast" code; let get_result = - Introspection.create_ref "results" - [%ty: Learnocaml_report.t option] - None in - Introspection.register_callback "set_progress" - [%ty: string] - set_progress ; - Introspection.insert_in_env "timeout" [%ty: int option] timeout ; - handle_error (internal_error [%i"while preparing the tests"]) @@ - Toploop_ext.use_string ~print_outcome ~ppf_answer - "module Test_lib = Test_lib.Make(struct\n\ - \ let results = results\n\ - \ let set_progress = set_progress\n\ - \ let timeout = timeout\n\ - \ module Introspection = Introspection\n\ - end)" ; - handle_error (internal_error [%i"while preparing the tests"]) @@ - Toploop_ext.use_string ~print_outcome ~ppf_answer - "module Report = Learnocaml_report" ; - set_progress [%i"Launching the test bench."] ; - + Introspection.create_ref "results" [%ty: Learnocaml_report.t option] + None + in + Introspection.register_callback "set_progress" [%ty: string] set_progress; + Introspection.insert_in_env "timeout" [%ty: int option] timeout; + handle_error (internal_error [%i "while preparing the tests"]) + @@ Toploop_ext.use_string ~print_outcome ~ppf_answer + "module Test_lib = Test_lib.Make(struct\n\ + \ let results = results\n\ + \ let set_progress = set_progress\n\ + \ let timeout = timeout\n\ + \ module Introspection = Introspection\n\ + end)"; + handle_error (internal_error [%i "while preparing the tests"]) + @@ Toploop_ext.use_string ~print_outcome ~ppf_answer + "module Report = Learnocaml_report"; + set_progress [%i "Launching the test bench."]; let () = let open Learnocaml_exercise in let files = File.dependencies (access File.depend exo) in let rec load_dependencies signatures = function - | [] -> () (* signatures without implementation are ignored *) - | file::fs -> - let path = File.key file - and content = decipher file exo in - let modname = String.capitalize_ascii @@ - Filename.remove_extension @@ Filename.basename path in - match Filename.extension path with - | ".mli" -> load_dependencies ((modname,content) :: signatures) fs - | ".ml" -> - let included,content = - (* the first line of an .ml file can contain an annotation *) - (* [@@@included] which denotes that this file has to be included *) - (* directly in the toplevel environment, and not in an module. *) - match String.index_opt content '\n' with - | None -> (false,content) - | Some i -> - (match String.trim (String.sub content 0 i) with - | "[@@@included]" -> - let content' = String.sub content i @@ - (String.length content - i) - in (true,content') - | _ -> (false,content)) - in - (handle_error (internal_error [%i"while loading user dependencies"]) @@ - match included with - | true -> Toploop_ext.use_string ~print_outcome ~ppf_answer - ~filename:(Filename.basename path) content - | false -> - let use_mod = - Toploop_ext.use_mod_string ~print_outcome ~ppf_answer ~modname in - match List.assoc_opt modname signatures with - | Some sig_code -> use_mod ~sig_code content - | None -> use_mod content); - load_dependencies signatures fs - | _ -> failwith ("uninterpreted dependency \"" ^ path ^ - "\", file extension expected : .ml or .mli") in - load_dependencies [] files + | [] -> () (* signatures without implementation are ignored *) + | file :: fs -> ( + let path = File.key file and content = decipher file exo in + let modname = + String.capitalize_ascii @@ Filename.remove_extension + @@ Filename.basename path + in + match Filename.extension path with + | ".mli" -> + load_dependencies ((modname, content) :: signatures) fs + | ".ml" -> + let included, content = + (* the first line of an .ml file can contain an annotation *) + (* [@@@included] which denotes that this file has to be included *) + (* directly in the toplevel environment, and not in an module. *) + match String.index_opt content '\n' with + | None -> (false, content) + | Some i -> ( + match String.trim (String.sub content 0 i) with + | "[@@@included]" -> + let content' = + String.sub content i @@ (String.length content - i) + in + (true, content') + | _ -> (false, content) ) + in + ( handle_error + (internal_error [%i "while loading user dependencies"]) + @@ + match included with + | true -> + Toploop_ext.use_string ~print_outcome ~ppf_answer + ~filename:(Filename.basename path) content + | false -> ( + let use_mod = + Toploop_ext.use_mod_string ~print_outcome ~ppf_answer + ~modname + in + match List.assoc_opt modname signatures with + | Some sig_code -> use_mod ~sig_code content + | None -> use_mod content ) ); + load_dependencies signatures fs + | _ -> + failwith + ( "uninterpreted dependency \"" ^ path + ^ "\", file extension expected : .ml or .mli" ) ) + in + load_dependencies [] files in - - handle_error (internal_error [%i"while testing your solution"]) @@ - Toploop_ext.use_string ~print_outcome ~ppf_answer ~filename:(file "test.ml") - (Learnocaml_exercise.(decipher File.test exo)) ; - + handle_error (internal_error [%i "while testing your solution"]) + @@ Toploop_ext.use_string ~print_outcome ~ppf_answer + ~filename:(file "test.ml") + Learnocaml_exercise.(decipher File.test exo); (* Memory cleanup... *) - Toploop.initialize_toplevel_env () ; + Toploop.initialize_toplevel_env (); (* TODO: Also clear the object table, once the OCaml's Toploop allows to. *) - !flush_stderr () ; - !flush_stdout () ; + !flush_stderr (); + !flush_stdout (); match get_result () with | Some report -> Ok report | None -> Error Invalid_grader - with exn -> - Error exn in - Format.fprintf ppf_answer "@." ; - (result, - Buffer.contents stdout_buffer, - Buffer.contents stderr_buffer, - Buffer.contents outcomes_buffer) + with exn -> Error exn + in + Format.fprintf ppf_answer "@."; + ( result + , Buffer.contents stdout_buffer + , Buffer.contents stderr_buffer + , Buffer.contents outcomes_buffer ) diff --git a/src/grader/grading.mli b/src/grader/grading.mli index a918a6544..f181f8cfc 100644 --- a/src/grader/grading.mli +++ b/src/grader/grading.mli @@ -9,21 +9,25 @@ open Toploop_ext exception Internal_error of string * error + exception User_code_error of error + exception Invalid_grader +val get_grade : + ?callback:(string -> unit) + -> ?timeout:int + -> ?dirname:string + -> divert:(string -> out_channel -> (string -> unit) -> unit -> unit) + -> Learnocaml_exercise.t + -> string + -> (Learnocaml_report.t, exn) result * string * string * string (** Take an exercise, a solution, and return the report, stdout, stderr and outcomes of the toplevel, or raise ont of the exceptions above. The divert mechanism is a platform dependent way of rerouting the standard channel descriptors, as implemented by {!Toploop_unix} and {!Toploop_jsoo}. *) -val get_grade: - ?callback:(string -> unit) -> - ?timeout:int -> - ?dirname:string -> - divert:(string -> out_channel -> (string -> unit) -> (unit -> unit)) -> - Learnocaml_exercise.t -> string -> (Learnocaml_report.t, exn) result * string * string * string +val string_of_exn : exn -> string option (** Returns user-friendly messages when called on [Internal_error] or [User_code_error] *) -val string_of_exn: exn -> string option diff --git a/src/grader/grading_cli.ml b/src/grader/grading_cli.ml index 47e0fcf8f..8b844a096 100644 --- a/src/grader/grading_cli.ml +++ b/src/grader/grading_cli.ml @@ -9,11 +9,11 @@ open Lwt let rec remove_dir dir = - Lwt_stream.iter_p (remove dir) (Lwt_unix.files_of_directory dir) >>= fun () -> - Lwt_unix.rmdir dir + Lwt_stream.iter_p (remove dir) (Lwt_unix.files_of_directory dir) + >>= fun () -> Lwt_unix.rmdir dir + and remove dir name = - if name = "." || name = ".." then - Lwt.return_unit + if name = "." || name = ".." then Lwt.return_unit else let file = Filename.concat dir name in if Sys.is_directory file then remove_dir file else Lwt_unix.unlink file @@ -27,26 +27,29 @@ let with_temp_dir f = in Lwt.catch (fun () -> Lwt_unix.mkdir d 0o700 >>= fun () -> Lwt.return d) @@ function - | Unix.Unix_error(Unix.EEXIST, _, _) -> get_dir () - | e -> raise e + | Unix.Unix_error (Unix.EEXIST, _, _) -> get_dir () | e -> raise e in - get_dir () >>= fun dir -> + get_dir () + >>= fun dir -> Lwt.catch (fun () -> f dir >>= fun res -> remove_dir dir >>= fun () -> Lwt.return res) (fun e -> remove_dir dir >>= fun () -> Lwt.fail e) let get_grade ?callback ?timeout ?dirname exo solution = - with_temp_dir @@ fun cmis_dir -> - let module ResDump = - OCamlResFormats.Files (OCamlResSubFormats.Raw) in + with_temp_dir + @@ fun cmis_dir -> + let module ResDump = OCamlResFormats.Files (OCamlResSubFormats.Raw) in let dump_cmis = - ResDump.output { OCamlResFormats.base_output_dir = cmis_dir } in - dump_cmis Embedded_cmis.root ; - dump_cmis Embedded_grading_cmis.root ; - Config.load_path := [ cmis_dir ] ; - Toploop_unix.initialize () ; + ResDump.output {OCamlResFormats.base_output_dir = cmis_dir} + in + dump_cmis Embedded_cmis.root; + dump_cmis Embedded_grading_cmis.root; + Config.load_path := [cmis_dir]; + Toploop_unix.initialize (); let divert name chan cb = let redirection = Toploop_unix.redirect_channel name chan cb in - fun () -> Toploop_unix.stop_channel_redirection redirection in - Lwt.wrap @@ fun () -> + fun () -> Toploop_unix.stop_channel_redirection redirection + in + Lwt.wrap + @@ fun () -> Grading.get_grade ?callback ?timeout ?dirname ~divert exo solution diff --git a/src/grader/grading_cli.mli b/src/grader/grading_cli.mli index 227f3ac94..51f6abade 100644 --- a/src/grader/grading_cli.mli +++ b/src/grader/grading_cli.mli @@ -6,12 +6,13 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) +val get_grade : + ?callback:(string -> unit) + -> ?timeout:int + -> ?dirname:string + -> Learnocaml_exercise.t + -> string + -> ((Learnocaml_report.t, exn) result * string * string * string) Lwt.t (** Take an exercise, a solution, and return the report, stdout, stderr and outcomes of the toplevel, or raise ont of the exceptions defined in module {!Grading}. *) -val get_grade: - ?callback:(string -> unit) -> - ?timeout:int -> - ?dirname:string -> - Learnocaml_exercise.t -> string -> - ((Learnocaml_report.t, exn) result * string * string * string) Lwt.t diff --git a/src/grader/grading_jsoo.ml b/src/grader/grading_jsoo.ml index e2abda7b5..bb674ee39 100644 --- a/src/grader/grading_jsoo.ml +++ b/src/grader/grading_jsoo.ml @@ -12,32 +12,27 @@ open Grader_jsoo_messages open Lwt.Infix open Js_of_ocaml -let get_grade - ?(worker_js_file = "/js/learnocaml-grader-worker.js") - ?(callback = (fun _ -> ())) - ?(timeout = infinity) - exercise = +let get_grade ?(worker_js_file = "/js/learnocaml-grader-worker.js") + ?(callback = fun _ -> ()) ?(timeout = infinity) exercise = let t, u = Lwt.task () in let worker = Worker.create worker_js_file in - Lwt.on_cancel t (fun () -> worker##terminate) ; + Lwt.on_cancel t (fun () -> worker##terminate); let onmessage (ev : Json_repr_browser.Repr.value Worker.messageEvent Js.t) = let json = ev##.data in - begin match Json_repr_browser.Json_encoding.destruct from_worker_enc json with - | Callback text -> callback text - | Answer (report, stdout, stderr, outcomes) -> - worker##terminate ; - Lwt.wakeup u (report, stdout, stderr, outcomes) - end ; + ( match Json_repr_browser.Json_encoding.destruct from_worker_enc json with + | Callback text -> callback text + | Answer (report, stdout, stderr, outcomes) -> + worker##terminate; + Lwt.wakeup u (report, stdout, stderr, outcomes) ); Js._true in - worker##.onmessage := Dom.handler onmessage ; - Lwt.return @@ - fun solution -> - let req = { exercise ; solution } in - let json = Json_repr_browser.Json_encoding.construct to_worker_enc req in - worker##(postMessage json) ; - let timer = - Lwt_js.sleep timeout >>= fun () -> - worker##terminate ; - Lwt.fail Timeout in - Lwt.pick [ timer ; t ] + worker##.onmessage := Dom.handler onmessage; + Lwt.return + @@ fun solution -> + let req = {exercise; solution} in + let json = Json_repr_browser.Json_encoding.construct to_worker_enc req in + worker ## (postMessage json); + let timer = + Lwt_js.sleep timeout >>= fun () -> worker##terminate; Lwt.fail Timeout + in + Lwt.pick [timer; t] diff --git a/src/grader/grading_jsoo.mli b/src/grader/grading_jsoo.mli index 1c50b5bfe..3c99cf209 100644 --- a/src/grader/grading_jsoo.mli +++ b/src/grader/grading_jsoo.mli @@ -1,18 +1,18 @@ -(* This file is part of Learn-OCaml. +exception + (* This file is part of Learn-OCaml. * * Copyright (C) 2019 OCaml Software Foundation. * Copyright (C) 2016-2018 OCamlPro. * * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) + Timeout -exception Timeout - +val get_grade : + ?worker_js_file:string + -> ?callback:(string -> unit) + -> ?timeout:float + -> Learnocaml_exercise.t + -> (string -> (Learnocaml_report.t * string * string * string) Lwt.t) Lwt.t (** Launch a worker, send it the exercise, grade it, return the result and kill the worker. Fail with {!Timeout} after [timeout] seconds. *) -val get_grade : - ?worker_js_file: string -> - ?callback:(string -> unit) -> - ?timeout: float -> - Learnocaml_exercise.t -> - (string -> (Learnocaml_report.t * string * string * string) Lwt.t) Lwt.t diff --git a/src/grader/introspection.ml b/src/grader/introspection.ml index 662c06288..53591435e 100644 --- a/src/grader/introspection.ml +++ b/src/grader/introspection.ml @@ -14,7 +14,8 @@ let split s c = | exception Not_found when i <> 0 -> if i = 0 then [] else [String.sub s i (String.length s - i)] | exception _ -> [] - | j -> String.sub s i (j - i) :: loop (j + 1) in + | j -> String.sub s i (j - i) :: loop (j + 1) + in loop 0 let parse_lid name = @@ -23,269 +24,287 @@ let parse_lid name = | id :: args -> List.fold_left (fun lid name -> Longident.Ldot (lid, name)) - (Longident.Lident id) - args + (Longident.Lident id) args -type 'a value = - | Absent - | Present of 'a - | Incompatible of string +type 'a value = Absent | Present of 'a | Incompatible of string let insert_in_env (type t) name (ty : t Ty.ty) (value : t) = if name = "" then invalid_arg "Learnocaml_toplevel_toploop.insert_in_env (1)"; - let ty = - Typetexp.transl_type_scheme !Toploop.toplevel_env (Ty.obj ty) in - Toploop.toplevel_env := begin - if String.uncapitalize_ascii name = name then - Env.add_value - (Ident.create name) - { Types. - val_type = ty.Typedtree.ctyp_type; - val_kind = Types.Val_reg; - val_attributes = []; - val_loc = Location.none } - !Toploop.toplevel_env - else - let open Typedtree in - match ty.ctyp_desc with - | Ttyp_package { pack_type; _ } -> - Env.add_module - (Ident.create name) - pack_type - !Toploop.toplevel_env - | _ -> invalid_arg "Learnocaml_toplevel_toploop.insert_in_env (2)" - end; + let ty = Typetexp.transl_type_scheme !Toploop.toplevel_env (Ty.obj ty) in + (Toploop.toplevel_env := + if String.uncapitalize_ascii name = name then + Env.add_value (Ident.create name) + { Types.val_type = ty.Typedtree.ctyp_type + ; val_kind = Types.Val_reg + ; val_attributes = [] + ; val_loc = Location.none } + !Toploop.toplevel_env + else + let open Typedtree in + match ty.ctyp_desc with + | Ttyp_package {pack_type; _} -> + Env.add_module (Ident.create name) pack_type !Toploop.toplevel_env + | _ -> invalid_arg "Learnocaml_toplevel_toploop.insert_in_env (2)"); Toploop.setvalue name (Obj.repr value) let insert_mod_ast_in_env ~var_name impl_code = let init_loc lb filename = Location.input_name := filename; Location.input_lexbuf := Some lb; - Location.init lb filename in + Location.init lb filename + in let parse_mod_string modname sig_code impl_code = let open Parsetree in let open Ast_helper in let str = let impl_lb = Lexing.from_string impl_code in init_loc impl_lb (String.uncapitalize_ascii modname ^ ".ml"); - Parse.implementation impl_lb in + Parse.implementation impl_lb + in let m = match sig_code with - | None -> (Mod.structure str) + | None -> Mod.structure str | Some sig_code -> - let sig_lb = Lexing.from_string sig_code in - init_loc sig_lb (String.uncapitalize_ascii modname ^ ".mli"); - let s = Parse.interface sig_lb in - Mod.constraint_ (Mod.structure str) (Mty.signature s) in - Ptop_def [ Str.module_ (Mb.mk (Location.mknoloc modname) m) ] in + let sig_lb = Lexing.from_string sig_code in + init_loc sig_lb (String.uncapitalize_ascii modname ^ ".mli"); + let s = Parse.interface sig_lb in + Mod.constraint_ (Mod.structure str) (Mty.signature s) + in + Ptop_def [Str.module_ (Mb.mk (Location.mknoloc modname) m)] + in let phr = - Toploop_ext.Ppx.preprocess_phrase @@ - parse_mod_string (String.capitalize_ascii var_name) None impl_code in + Toploop_ext.Ppx.preprocess_phrase + @@ parse_mod_string (String.capitalize_ascii var_name) None impl_code + in let open Parsetree in - (match phr with - | Ptop_def [ { pstr_desc = - Pstr_module { pmb_expr = { pmod_desc = - Pmod_structure s; _ }; _ }; _}] - | Ptop_def [ { pstr_desc = - Pstr_module { pmb_expr = { pmod_desc = - Pmod_constraint ({ pmod_desc = - Pmod_structure s; _ }, _); _ }; _ }; _}] -> - let ty = Ty.repr (Ast_helper.(Typ.constr (Location.mknoloc (parse_lid "Parsetree.structure")) [])) in - insert_in_env var_name (ty : Parsetree.structure Ty.ty) s - | _ (* should not happen *) -> assert false) - -let treat_lookup_errors fn = match fn () with + match phr with + | Ptop_def + [ { pstr_desc = + Pstr_module {pmb_expr = {pmod_desc = Pmod_structure s; _}; _}; _ } + ] + |Ptop_def + [ { pstr_desc = + Pstr_module + { pmb_expr = + { pmod_desc = + Pmod_constraint ({pmod_desc = Pmod_structure s; _}, _); _ + }; _ }; _ } ] -> + let ty = + Ty.repr + Ast_helper.( + Typ.constr (Location.mknoloc (parse_lid "Parsetree.structure")) []) + in + insert_in_env var_name (ty : Parsetree.structure Ty.ty) s + | _ (* should not happen *) -> assert false + +let treat_lookup_errors fn = + match fn () with | result -> result - | exception Not_found -> - Absent - | exception Failure msg -> - Incompatible msg + | exception Not_found -> Absent + | exception Failure msg -> Incompatible msg | exception Ctype.Unify args -> Incompatible (Format.asprintf "%a@." (Typetexp.report_error !Toploop.toplevel_env) (Typetexp.Type_mismatch args)) - | exception exn -> - match Location.error_of_exn exn with - | None -> Incompatible (Format.asprintf "%a@." Toploop.print_untyped_exception (Obj.repr exn)) - | Some { Location.msg; _ } -> Incompatible msg + | exception exn -> ( + match Location.error_of_exn exn with + | None -> + Incompatible + (Format.asprintf "%a@." Toploop.print_untyped_exception + (Obj.repr exn)) + | Some {Location.msg; _} -> Incompatible msg ) let compatible_type nexp ngot = - treat_lookup_errors @@ fun () -> + treat_lookup_errors + @@ fun () -> let path_exp = Env.lookup_type nexp !Toploop.toplevel_env in let decl_exp = Env.find_type path_exp !Toploop.toplevel_env in let path_got = Env.lookup_type ngot !Toploop.toplevel_env in let decl_got = Env.find_type path_got !Toploop.toplevel_env in - let texp = Ctype.newconstr path_exp (List.map (fun _ -> Ctype.newvar ()) decl_exp.Types.type_params) in - let tgot = Ctype.newconstr path_got (List.map (fun _ -> Ctype.newvar ()) decl_got.Types.type_params) in - Ctype.unify !Toploop.toplevel_env tgot texp ; + let texp = + Ctype.newconstr path_exp + (List.map (fun _ -> Ctype.newvar ()) decl_exp.Types.type_params) + in + let tgot = + Ctype.newconstr path_got + (List.map (fun _ -> Ctype.newvar ()) decl_got.Types.type_params) + in + Ctype.unify !Toploop.toplevel_env tgot texp; Present () let get_value lid ty = - treat_lookup_errors @@ fun () -> - match Ty.obj ty, String.get (Longident.last lid) 0 with - | { Parsetree.ptyp_desc = Parsetree.Ptyp_package (n, rews); _ }, 'A'.. 'Z' -> - begin match Env.lookup_module ~load:false lid !Toploop.toplevel_env with - | exception Not_found -> Absent - | path -> - let { Types.md_loc; _ } = Env.find_module path !Toploop.toplevel_env in - let phrase = - let open Ast_helper in - with_default_loc md_loc @@ fun () -> - let pack_expr = - Exp.constraint_ - (Exp.pack (Mod.ident (Location.mkloc lid md_loc))) - (Typ.package n rews) in - Parsetree.Ptop_def - [Str.value Asttypes.Nonrecursive - [Vb.mk (Pat.var (Location.mkloc "%fake%" md_loc)) pack_expr ]] in - let buf = Buffer.create 300 in - let ppf = Format.formatter_of_buffer buf in - if Toploop.execute_phrase false ppf phrase then - let fake_path, _ = Env.lookup_value (Longident.Lident "%fake%") !Toploop.toplevel_env in - Present (Obj.obj @@ Toploop.eval_path !Toploop.toplevel_env fake_path) - else - let msg = Format.fprintf ppf "@." ; Buffer.contents buf in - failwith msg - end + treat_lookup_errors + @@ fun () -> + match (Ty.obj ty, (Longident.last lid).[0]) with + | {Parsetree.ptyp_desc = Parsetree.Ptyp_package (n, rews); _}, 'A' .. 'Z' -> ( + match Env.lookup_module ~load:false lid !Toploop.toplevel_env with + | exception Not_found -> Absent + | path -> + let {Types.md_loc; _} = Env.find_module path !Toploop.toplevel_env in + let phrase = + let open Ast_helper in + with_default_loc md_loc + @@ fun () -> + let pack_expr = + Exp.constraint_ + (Exp.pack (Mod.ident (Location.mkloc lid md_loc))) + (Typ.package n rews) + in + Parsetree.Ptop_def + [ Str.value Asttypes.Nonrecursive + [Vb.mk (Pat.var (Location.mkloc "%fake%" md_loc)) pack_expr] ] + in + let buf = Buffer.create 300 in + let ppf = Format.formatter_of_buffer buf in + if Toploop.execute_phrase false ppf phrase then + let fake_path, _ = + Env.lookup_value (Longident.Lident "%fake%") !Toploop.toplevel_env + in + Present (Obj.obj @@ Toploop.eval_path !Toploop.toplevel_env fake_path) + else + let msg = Format.fprintf ppf "@."; Buffer.contents buf in + failwith msg ) | _ -> - let { Typedtree.ctyp_type = exp_type; _ } = - Typetexp.transl_type_scheme !Toploop.toplevel_env (Ty.obj ty) in - let path, { Types.val_type; _ } = - Env.lookup_value lid !Toploop.toplevel_env in + let {Typedtree.ctyp_type = exp_type; _} = + Typetexp.transl_type_scheme !Toploop.toplevel_env (Ty.obj ty) + in + let path, {Types.val_type; _} = + Env.lookup_value lid !Toploop.toplevel_env + in if Ctype.moregeneral !Toploop.toplevel_env true val_type exp_type then Present (Obj.obj @@ Toploop.eval_path !Toploop.toplevel_env path) else failwith (Format.asprintf "Wrong type %a." Printtyp.type_sch val_type) let print_value ppf v ty = - let { Typedtree.ctyp_type = ty; _ } = - Typetexp.transl_type_scheme !Toploop.toplevel_env (Ty.obj ty) in + let {Typedtree.ctyp_type = ty; _} = + Typetexp.transl_type_scheme !Toploop.toplevel_env (Ty.obj ty) + in let needs_parentheses = let state = ref `Start in let tmp_ppf = Format.make_formatter (fun s ofs len -> - if len = 0 then () else - match !state, String.get s ofs with - | `Decided _, _ -> () - | `Start, ('(' | '{' | '[' | '<' | '\'' | '"') -> - state := `Decided false ; - raise Exit - | (`Start | `Undecided), _ -> - state := `Undecided ; - for i = ofs to ofs + len - 1 do - match String.get s i with - | ' ' | '\n' | '\r' | '\t' -> - state := `Decided true ; - raise Exit - | _ -> () - done) - (fun () -> ()) in - begin try - Toploop.print_value !Toploop.toplevel_env (Obj.repr v) tmp_ppf ty ; + if len = 0 then () + else + match (!state, s.[ofs]) with + | `Decided _, _ -> () + | `Start, ('(' | '{' | '[' | '<' | '\'' | '"') -> + state := `Decided false; + raise Exit + | (`Start | `Undecided), _ -> + state := `Undecided; + for i = ofs to ofs + len - 1 do + match s.[i] with + | ' ' | '\n' | '\r' | '\t' -> + state := `Decided true; + raise Exit + | _ -> () + done ) + (fun () -> ()) + in + ( try + Toploop.print_value !Toploop.toplevel_env (Obj.repr v) tmp_ppf ty; Format.pp_print_flush tmp_ppf () - with Exit -> () end ; - match !state with `Start | `Decided false | `Undecided -> false | `Decided true -> true in - if needs_parentheses then begin - Format.fprintf ppf "@[(" ; - Toploop.print_value !Toploop.toplevel_env (Obj.repr v) ppf ty ; - Format.fprintf ppf ")@]" - end else begin - Format.fprintf ppf "@[" ; - Toploop.print_value !Toploop.toplevel_env (Obj.repr v) ppf ty ; - Format.fprintf ppf "@]" - end + with Exit -> () ); + match !state with + | `Start | `Decided false | `Undecided -> false + | `Decided true -> true + in + if needs_parentheses then ( + Format.fprintf ppf "@[("; + Toploop.print_value !Toploop.toplevel_env (Obj.repr v) ppf ty; + Format.fprintf ppf ")@]" ) + else ( + Format.fprintf ppf "@["; + Toploop.print_value !Toploop.toplevel_env (Obj.repr v) ppf ty; + Format.fprintf ppf "@]" ) let sample_value ty = - let { Typedtree.ctyp_type = ty; _ } = - Typetexp.transl_type_scheme !Toploop.toplevel_env (Ty.obj ty) in + let {Typedtree.ctyp_type = ty; _} = + Typetexp.transl_type_scheme !Toploop.toplevel_env (Ty.obj ty) + in let lid = Format.asprintf "sample_%04X" (Random.int 0xFFFF) in let phrase = let open Asttypes in let open Types in let open Ast_helper in let sampler_id suffix = - Exp.ident (Location.mknoloc (Longident.Lident ("sample_" ^ suffix))) in - let rec phrase ty = match ty.desc with - | Tconstr (path, [], _) -> - sampler_id (Path.name path) + Exp.ident (Location.mknoloc (Longident.Lident ("sample_" ^ suffix))) + in + let rec phrase ty = + match ty.desc with + | Tconstr (path, [], _) -> sampler_id (Path.name path) | Tconstr (path, tl, _) -> - Exp.apply (sampler_id (Path.name path)) - (List.map (fun arg -> Asttypes.Nolabel, phrase arg) tl) - | Ttuple tys -> - begin match tys with - | [_; _] -> - Exp.apply (sampler_id "pair") - (List.map (fun arg -> Asttypes.Nolabel, phrase arg) tys) - | _ -> failwith "sample_value: unsupported tuple arity" - end + Exp.apply + (sampler_id (Path.name path)) + (List.map (fun arg -> (Asttypes.Nolabel, phrase arg)) tl) + | Ttuple tys -> ( + match tys with + | [_; _] -> + Exp.apply (sampler_id "pair") + (List.map (fun arg -> (Asttypes.Nolabel, phrase arg)) tys) + | _ -> failwith "sample_value: unsupported tuple arity" ) | _ -> failwith "unsamplable type" in let lid = Location.mknoloc lid in Parsetree.Ptop_def - [Str.value Nonrecursive - [Vb.mk (Pat.var lid) (phrase ty)]] + [Str.value Nonrecursive [Vb.mk (Pat.var lid) (phrase ty)]] in let buf = Buffer.create 100 in let ppf = Format.formatter_of_buffer buf in match Toploop.execute_phrase false ppf phrase with | true -> - let path, { Types.val_type; _ } = - Env.lookup_value (Longident.Lident lid) !Toploop.toplevel_env in - let gty = Types.{ty with desc = Tarrow (Asttypes.Nolabel, Predef.type_unit, ty, Cok) } in - if Ctype.moregeneral !Toploop.toplevel_env true val_type gty then - (Obj.obj @@ Toploop.eval_path !Toploop.toplevel_env path) - else (failwith "sampler has the wrong type !") - | false -> - failwith ("sampler could not be defined, " ^ Buffer.contents buf) + let path, {Types.val_type; _} = + Env.lookup_value (Longident.Lident lid) !Toploop.toplevel_env + in + let gty = + Types. + {ty with desc = Tarrow (Asttypes.Nolabel, Predef.type_unit, ty, Cok)} + in + if Ctype.moregeneral !Toploop.toplevel_env true val_type gty then + Obj.obj @@ Toploop.eval_path !Toploop.toplevel_env path + else failwith "sampler has the wrong type !" + | false -> failwith ("sampler could not be defined, " ^ Buffer.contents buf) | exception Typetexp.Error (_loc, env, err) -> Typetexp.report_error env ppf err; failwith ("type error while defining sampler: " ^ Buffer.contents buf) | exception e -> - failwith ("error while defining sampler: " ^ Buffer.contents buf ^ Printexc.to_string e) + failwith + ( "error while defining sampler: " ^ Buffer.contents buf + ^ Printexc.to_string e ) let register_callback name ty f = let unit = - Ast_helper.(Typ.constr (Location.mknoloc (Longident.Lident "unit")) []) in + Ast_helper.(Typ.constr (Location.mknoloc (Longident.Lident "unit")) []) + in let ty = Ty.curry ty (Ty.repr unit) in insert_in_env name ty f +let ref_lid = Location.mknoloc Longident.(Ldot (Lident "Pervasives", "ref")) -let ref_lid = - Location.mknoloc Longident.(Ldot(Lident "Pervasives", "ref")) - -let create_ref name (ty: 'a Ty.ty) (v: 'a) = +let create_ref name (ty : 'a Ty.ty) (v : 'a) = let ty = Ty.repr @@ Ast_helper.Typ.constr ref_lid [Ty.obj ty] in let r = ref v in insert_in_env name ty r; - (fun () -> !r) + fun () -> !r let setup = lazy (Ast_mapper.register "ppx_metaquot" Ppx_metaquot.expander) let allow_introspection ~divert = - - Lazy.force setup ; - + Lazy.force setup; let module Introspection = struct + type 'a t = 'a value = Absent | Present of 'a | Incompatible of string - type 'a t = 'a value = - | Absent - | Present of 'a - | Incompatible of string - type 'a value = 'a t = - | Absent - | Present of 'a - | Incompatible of string + type 'a value = 'a t = Absent | Present of 'a | Incompatible of string let get_value name ty = let lid = parse_lid name in get_value lid ty let compatible_type name_exp name_got = - compatible_type - (parse_lid name_exp) - (parse_lid name_got) + compatible_type (parse_lid name_exp) (parse_lid name_got) let print_value = print_value @@ -297,48 +316,52 @@ let allow_introspection ~divert = let append s = if Buffer.length buffer + String.length s > 32_768 - 9 then excess := true - else - Buffer.add_string buffer s in + else Buffer.add_string buffer s + in let flush = divert name ch append in fun () -> - flush () ; + flush (); let res = Buffer.contents buffer in Buffer.clear buffer; let fail = !excess in - excess := false ; - if fail then raise Excess ; + excess := false; + if fail then raise Excess; res let bad_stdout_cb () = invalid_arg "Introspection.release_stdout" + let stdout_cb = ref bad_stdout_cb + let grab_stdout () = if !stdout_cb != bad_stdout_cb then - invalid_arg "Introspection.grab_stdout" ; + invalid_arg "Introspection.grab_stdout"; stdout_cb := divert "stdout" stdout + let release_stdout () = let res = !stdout_cb () in - stdout_cb := bad_stdout_cb ; + stdout_cb := bad_stdout_cb; res let bad_stderr_cb () = invalid_arg "Introspection.release_stderr" + let stderr_cb = ref bad_stderr_cb + let grab_stderr () = if !stderr_cb != bad_stderr_cb then - invalid_arg "Introspection.grab_stderr" ; + invalid_arg "Introspection.grab_stderr"; stderr_cb := divert "stderr" stderr + let release_stderr () = let res = !stderr_cb () in - stderr_cb := bad_stderr_cb ; + stderr_cb := bad_stderr_cb; res - let get_printer ty = fun ppf v -> print_value ppf v ty + let get_printer ty ppf v = print_value ppf v ty + let get_sampler ty = sample_value ty let parse_lid name = parse_lid name - end in - - insert_in_env - "Introspection" + insert_in_env "Introspection" [%ty: (module Introspection_intf.INTROSPECTION)] (module Introspection : Introspection_intf.INTROSPECTION) diff --git a/src/grader/introspection.mli b/src/grader/introspection.mli index 842fc9d9a..c80099ce7 100644 --- a/src/grader/introspection.mli +++ b/src/grader/introspection.mli @@ -6,21 +6,21 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -type 'a value = - | Absent - | Present of 'a - | Incompatible of string -val get_value: Longident.t -> 'a Ty.ty -> 'a value +type 'a value = Absent | Present of 'a | Incompatible of string -val print_value: Format.formatter -> 'a -> 'a Ty.ty -> unit -val sample_value: 'a Ty.ty -> 'a +val get_value : Longident.t -> 'a Ty.ty -> 'a value -val insert_in_env: string -> 'a Ty.ty -> 'a -> unit +val print_value : Format.formatter -> 'a -> 'a Ty.ty -> unit -val insert_mod_ast_in_env: var_name:string -> string -> unit -val create_ref: string -> 'a Ty.ty -> 'a -> unit -> 'a -val register_callback: string -> 'a Ty.ty -> ('a -> unit) -> unit +val sample_value : 'a Ty.ty -> 'a -val allow_introspection: - divert:(string -> out_channel -> (string -> unit) -> (unit -> unit)) -> - unit +val insert_in_env : string -> 'a Ty.ty -> 'a -> unit + +val insert_mod_ast_in_env : var_name:string -> string -> unit + +val create_ref : string -> 'a Ty.ty -> 'a -> unit -> 'a + +val register_callback : string -> 'a Ty.ty -> ('a -> unit) -> unit + +val allow_introspection : + divert:(string -> out_channel -> (string -> unit) -> unit -> unit) -> unit diff --git a/src/grader/introspection_intf.mli b/src/grader/introspection_intf.mli index b1afabf59..d6826edec 100644 --- a/src/grader/introspection_intf.mli +++ b/src/grader/introspection_intf.mli @@ -15,28 +15,28 @@ *) module type INTROSPECTION = sig + type 'a value = Absent | Present of 'a | Incompatible of string - type 'a value = - | Absent - | Present of 'a - | Incompatible of string + val get_value : string -> 'a Ty.ty -> 'a value + + val print_value : Format.formatter -> 'a -> 'a Ty.ty -> unit - val get_value: string -> 'a Ty.ty -> 'a value - val print_value: Format.formatter -> 'a -> 'a Ty.ty -> unit (* expected first *) - val compatible_type: string -> string -> unit value + val compatible_type : string -> string -> unit value exception Excess - val grab_stdout: unit -> unit - val release_stdout: unit -> string + val grab_stdout : unit -> unit + + val release_stdout : unit -> string + + val grab_stderr : unit -> unit - val grab_stderr: unit -> unit - val release_stderr: unit -> string + val release_stderr : unit -> string - val get_sampler: 'a Ty.ty -> (unit -> 'a) - val get_printer: 'a Ty.ty -> (Format.formatter -> 'a -> unit) + val get_sampler : 'a Ty.ty -> unit -> 'a - val parse_lid: string -> Longident.t + val get_printer : 'a Ty.ty -> Format.formatter -> 'a -> unit + val parse_lid : string -> Longident.t end diff --git a/src/grader/learnocaml_report.ml b/src/grader/learnocaml_report.ml index 95b979340..5c17bfec0 100644 --- a/src/grader/learnocaml_report.ml +++ b/src/grader/learnocaml_report.ml @@ -9,29 +9,31 @@ (* -- minimal HTML producer ------------------------------------------------- *) type html = elt list + and elt = | C of string | T of string | I of string | E of string * attr list * html | S of string * attr list + and attr = string * string let rec output_html ppf = function | elt :: elts -> - output_elt ppf elt ; - begin match elt, elts with + output_elt ppf elt; + ( match (elt, elts) with | (E _ | S _ | C _), (T _ | I _) :: _ - | (T _ | I _), (E _ | S _ | C _) :: _ - | (T _ | I _), (T _ | I _) :: _ -> - Format.fprintf ppf " " - | _ -> () end ; - output_html ppf elts + |(T _ | I _), (E _ | S _ | C _) :: _ + |(T _ | I _), (T _ | I _) :: _ -> + Format.fprintf ppf " " + | _ -> () ); + output_html ppf elts | [] -> () and output_text ppf text = for i = 0 to String.length text - 1 do - match String.get text i with + match text.[i] with | '"' -> Format.fprintf ppf """ | '<' -> Format.fprintf ppf "<" | '>' -> Format.fprintf ppf ">" @@ -40,31 +42,27 @@ and output_text ppf text = done and output_elt ppf = function - | T text -> - Format.fprintf ppf "%a" output_text text - | I text -> - Format.fprintf ppf "&%s;" text - | C text -> - Format.fprintf ppf "" text - | S (name, attrs) -> - Format.fprintf ppf "<%s%a/>" - name output_attrs attrs - | E ("script", attrs, [ C text ]) -> - Format.fprintf ppf "//" - output_attrs attrs text - | E ("style", attrs, [ C text ]) -> - Format.fprintf ppf "/**/" - output_attrs attrs text + | T text -> Format.fprintf ppf "%a" output_text text + | I text -> Format.fprintf ppf "&%s;" text + | C text -> Format.fprintf ppf "" text + | S (name, attrs) -> Format.fprintf ppf "<%s%a/>" name output_attrs attrs + | E ("script", attrs, [C text]) -> + Format.fprintf ppf "//" output_attrs + attrs text + | E ("style", attrs, [C text]) -> + Format.fprintf ppf + "/**/" + output_attrs attrs text | E (name, attrs, html) -> - Format.fprintf ppf "<%s%a>%a" - name output_attrs attrs output_html html name + Format.fprintf ppf "<%s%a>%a" name output_attrs attrs output_html + html name and output_attrs ppf attrs = - List.iter (fun (n, v) -> + List.iter + (fun (n, v) -> if String.contains v '"' then Printf.eprintf "Error: double quote in attribute value.\n%!" - else - Format.fprintf ppf " %s=\"%a\"" n output_text v) + else Format.fprintf ppf " %s=\"%a\"" n output_text v ) attrs (* -- report format --------------------------------------------------------- *) @@ -77,49 +75,50 @@ and item = | Message of text * status and status = - | Success of int | Penalty of int | Failure - | Warning | Informative | Important + | Success of int + | Penalty of int + | Failure + | Warning + | Informative + | Important and text = inline list -and inline = - | Text of string - | Break - | Code of string - | Output of string +and inline = Text of string | Break | Code of string | Output of string let result items = let rec do_report items = - List.fold_left (fun (successes, failures) item -> - let (isuccesses, ifailures) = do_item item in - (successes + isuccesses, failures || ifailures)) + List.fold_left + (fun (successes, failures) item -> + let isuccesses, ifailures = do_item item in + (successes + isuccesses, failures || ifailures) ) (0, false) items and do_item = function - | Message (_text, status) -> - begin match status with - | Success n -> (n, false) - | Penalty n -> (-n, true) - | Failure -> (0, true) - | Warning | Informative | Important -> (0, false) end - | Section (_title, contents) -> - do_report contents + | Message (_text, status) -> ( + match status with + | Success n -> (n, false) + | Penalty n -> (-n, true) + | Failure -> (0, true) + | Warning | Informative | Important -> (0, false) ) + | Section (_title, contents) -> do_report contents | SectionMin (_title, contents, min) -> - let (n, b) = do_report contents in - (max n min, b) in - let (n, b) = do_report items in + let n, b = do_report contents in + (max n min, b) + in + let n, b = do_report items in (max n 0, b) let rec scale ?(penalties = true) factor items = List.map (scale_item penalties factor) items + and scale_item penalties factor = function - | Section (text, report) -> - Section (text, scale ~penalties factor report) + | Section (text, report) -> Section (text, scale ~penalties factor report) | SectionMin (text, report, min) -> - SectionMin (text, - scale ~penalties factor report, - if penalties then factor * min else min) - | Message (text, Success n) -> - Message (text, Success (factor * n)) + SectionMin + ( text + , scale ~penalties factor report + , if penalties then factor * min else min ) + | Message (text, Success n) -> Message (text, Success (factor * n)) | Message (text, Penalty n) when penalties -> Message (text, Penalty (factor * n)) | item -> item @@ -127,49 +126,52 @@ and scale_item penalties factor = function let enc = let open Json_encoding in let text_enc = - list @@ union - [ case - (obj2 - (req "text" string) - (dft "display" - (string_enum [ "normal", `Normal ; - "code", `Code ; - "output", `Output ]) - `Normal)) - (function - | Text text -> Some (text, `Normal) - | Code text -> Some (text, `Code) - | Output text -> Some (text, `Output) - | _ -> None) - (function - | (text, `Normal) -> Text text - | (text, `Code) -> Code text - | (text, `Output) -> Output text) ; - case - empty - (function Break -> Some () | _ -> None) - (function () -> Break) ] in + list + @@ union + [ case + (obj2 (req "text" string) + (dft "display" + (string_enum + [ ("normal", `Normal) + ; ("code", `Code) + ; ("output", `Output) ]) + `Normal)) + (function + | Text text -> Some (text, `Normal) + | Code text -> Some (text, `Code) + | Output text -> Some (text, `Output) + | _ -> None) + (function + | text, `Normal -> Text text + | text, `Code -> Code text + | text, `Output -> Output text) + ; case empty + (function Break -> Some () | _ -> None) + (function () -> Break) ] + in let status_enc = union - [ case - int + [ case int (function Success n -> Some n | Penalty n -> Some (-n) | _ -> None) - (fun n -> if n > 0 then Success n else if n < 0 then Penalty (-n) - else Failure) ; - case - (string_enum [ "failure", Failure ; - "warning", Warning ; - "informative", Informative ; - "important", Important ]) + (fun n -> + if n > 0 then Success n + else if n < 0 then Penalty (-n) + else Failure ) + ; case + (string_enum + [ ("failure", Failure) + ; ("warning", Warning) + ; ("informative", Informative) + ; ("important", Important) ]) (function Success _ | Penalty _ -> None | v -> Some v) - (function v -> v) - ] + (function v -> v) ] in - let item_enc = mu "reportItem" @@ fun item_enc -> + let item_enc = + mu "reportItem" + @@ fun item_enc -> union [ case - (obj3 - (req "section" text_enc) + (obj3 (req "section" text_enc) (req "contents" (list item_enc)) (opt "minscore" int)) (function @@ -177,12 +179,10 @@ let enc = | SectionMin (text, report, min) -> Some (text, report, Some min) | Message _ -> None) (function - | (text, report, None) -> Section (text, report) - | (text, report, Some min) -> SectionMin (text, report, min)) ; - case - (obj2 - (req "message" text_enc) - (req "result" status_enc)) + | text, report, None -> Section (text, report) + | text, report, Some min -> SectionMin (text, report, min)) + ; case + (obj2 (req "message" text_enc) (req "result" status_enc)) (function | Message (text, status) -> Some (text, status) | Section _ | SectionMin _ -> None) @@ -193,42 +193,51 @@ let enc = (* -- report HTML output ---------------------------------------------------- *) let folder, unfolder = - let js = "var div = this.parentElement.parentElement;\ - if (div.classList.contains ('folded')) {\ - div.classList.remove ('folded') ;\ - this.innerHTML = 'v'\ - } else {\ - div.classList.add ('folded') ;\ - this.innerHTML = '>'\ - }" in - E ("span", [ "onclick", js ; "class", "folder-icon clickable" ], [ T "v" ]), - E ("span", [ "onclick", js ; "class", "folder-icon clickable" ], [ T ">" ]) + let js = + "var div = this.parentElement.parentElement;if (div.classList.contains \ + ('folded')) {div.classList.remove ('folded') ;this.innerHTML = 'v'} else \ + {div.classList.add ('folded') ;this.innerHTML = '>'}" + in + ( E ("span", [("onclick", js); ("class", "folder-icon clickable")], [T "v"]) + , E ("span", [("onclick", js); ("class", "folder-icon clickable")], [T ">"]) + ) let format items = let rec format_report items = - List.fold_left (fun ((successes, failures), items) item -> + List.fold_left + (fun ((successes, failures), items) item -> let (isuccesses, ifailures), item = format_item item in - (successes + isuccesses, failures || ifailures), item :: items) - ((0, false), []) items |> fun (result, items) -> - (result, List.rev items) + ((successes + isuccesses, failures || ifailures), item :: items) ) + ((0, false), []) + items + |> fun (result, items) -> (result, List.rev items) and format_item = function | Message (text, status) -> - let result, result_class, score = match status with - | Success 1 -> (1, false), "success", Some "1 pt" - | Success n -> (n, false), "success", Some (string_of_int n ^ " pts") - | Penalty 1 -> (-1, true), "failure", Some "-1 pt" + let result, result_class, score = + match status with + | Success 1 -> ((1, false), "success", Some "1 pt") + | Success n -> + ((n, false), "success", Some (string_of_int n ^ " pts")) + | Penalty 1 -> ((-1, true), "failure", Some "-1 pt") | Penalty n -> - (-n, true), "warning", Some ("-" ^ string_of_int n ^ " pts") - | Failure -> (0, true), "failure", Some "0 pt" - | Warning -> (0, false), "warning", None - | Informative -> (0, false), "informative", None - | Important -> (0, false), "important", None in - result, - E ("p", [ "class", "message " ^ result_class ], - [ E ("span", [ "class", "text" ], - match score with - | None -> format_text text - | Some score -> E ("span", [ "class", "score" ], [ T score ]) :: format_text text) ]) + ((-n, true), "warning", Some ("-" ^ string_of_int n ^ " pts")) + | Failure -> ((0, true), "failure", Some "0 pt") + | Warning -> ((0, false), "warning", None) + | Informative -> ((0, false), "informative", None) + | Important -> ((0, false), "important", None) + in + ( result + , E + ( "p" + , [("class", "message " ^ result_class)] + , [ E + ( "span" + , [("class", "text")] + , match score with + | None -> format_text text + | Some score -> + E ("span", [("class", "score")], [T score]) + :: format_text text ) ] ) ) | Section (title, contents) -> format_section title (format_report contents) | SectionMin (title, contents, min) -> @@ -236,78 +245,91 @@ let format items = format_section ~min title ((max n min, b), formatted_report) and format_section ?min title (result, formatted_report) = let result_class, score, folder = - let min_str = match min with + let min_str = + match min with | Some m when m = fst result -> " " ^ [%i "(minimum mark)"] - | _ -> "" in + | _ -> "" + in let format_section_html result_str = - [ E ("span", [ "class", "score" ], - [ T (result_str ^ min_str)])] in + [E ("span", [("class", "score")], [T (result_str ^ min_str)])] + in match result with - | (0, false) -> - "informative folded", [], unfolder - | (n, false) -> - "success folded", - format_section_html @@ Format.asprintf [%if"Completed, %d pts"] n, - unfolder - | (0, true) -> - "failure", - format_section_html @@ [%i"Failed"], - folder - | (s, true) when s < 0 || min_str <> "" -> - "failure", - format_section_html @@ Format.asprintf "%s, %d pts" [%i"Failed"] s, - folder - | (s, true) -> - "warning", - format_section_html @@ Format.asprintf [%if"Incomplete, %d pts"] s, - folder in - result, - E ("div", [ "class", "section " ^ result_class ], - [ E ("span", [ "class", "title" ], - folder :: format_text title @ score) ; - E ("div", [ "class", "report" ], formatted_report) ]) + | 0, false -> ("informative folded", [], unfolder) + | n, false -> + ( "success folded" + , format_section_html @@ Format.asprintf [%if "Completed, %d pts"] n + , unfolder ) + | 0, true -> ("failure", format_section_html @@ [%i "Failed"], folder) + | s, true when s < 0 || min_str <> "" -> + ( "failure" + , format_section_html @@ Format.asprintf "%s, %d pts" [%i "Failed"] s + , folder ) + | s, true -> + ( "warning" + , format_section_html @@ Format.asprintf [%if "Incomplete, %d pts"] s + , folder ) + in + ( result + , E + ( "div" + , [("class", "section " ^ result_class)] + , [ E + ( "span" + , [("class", "title")] + , (folder :: format_text title) @ score ) + ; E ("div", [("class", "report")], formatted_report) ] ) ) and format_text text = let format = function - | Text w -> - T w - | Break -> - S ("br", []) + | Text w -> T w + | Break -> S ("br", []) | Code s when String.contains s '\n' -> - E ("code", ["class", "code-block" ], [ T s ]) - | Output s -> - E ("code", ["class", "output-block" ], [ T s ]) - | Code s -> - E ("code", [ "class", "code" ], [ T s ]) in - List.map format text in + E ("code", [("class", "code-block")], [T s]) + | Output s -> E ("code", [("class", "output-block")], [T s]) + | Code s -> E ("code", [("class", "code")], [T s]) + in + List.map format text + in let (n, b), report = format_report items in let result = (max n 0, b) in - let result_class, score = match result with - | (0, false) -> "informative", [] - | (0, true) -> - "failure", [ T [%i"Exercise failed"] ; - E ("span", [ "class", "score" ], - [ T [%i"0 pt"] ]) ] - | (n, false) -> - "success", [ T [%i"Exercise complete"] ; - E ("span", [ "class", "score" ], - [ T (Format.asprintf [%if"%d pts"] n) ]) ] - | (s, true) -> - "warning", [ T [%i"Exercise incomplete"] ; - E ("span", [ "class", "score" ], - [ T (Format.asprintf [%if"%d pts"] s) ]) ] in - - let js = "var div = this.parentElement.parentElement;\ - if (div.classList.contains ('folded')) {\ - div.classList.remove ('folded') ;\ - } else {\ - div.classList.add ('folded') ;\ - }" in - E ("div", [ "id", "learnocaml-report" ], - [E ("div", [ "class", " section " ^ result_class ], - [ E ("span", [ "class", "title clickable" ; "onclick", js], score) ]) ; - E ("div", [ "class", "main" ], report) ]) - -let css = {| + let result_class, score = + match result with + | 0, false -> ("informative", []) + | 0, true -> + ( "failure" + , [ T [%i "Exercise failed"] + ; E ("span", [("class", "score")], [T [%i "0 pt"]]) ] ) + | n, false -> + ( "success" + , [ T [%i "Exercise complete"] + ; E + ( "span" + , [("class", "score")] + , [T (Format.asprintf [%if "%d pts"] n)] ) ] ) + | s, true -> + ( "warning" + , [ T [%i "Exercise incomplete"] + ; E + ( "span" + , [("class", "score")] + , [T (Format.asprintf [%if "%d pts"] s)] ) ] ) + in + let js = + "var div = this.parentElement.parentElement;if (div.classList.contains \ + ('folded')) {div.classList.remove ('folded') ;} else {div.classList.add \ + ('folded') ;}" + in + E + ( "div" + , [("id", "learnocaml-report")] + , [ E + ( "div" + , [("class", " section " ^ result_class)] + , [E ("span", [("class", "title clickable"); ("onclick", js)], score)] + ) + ; E ("div", [("class", "main")], report) ] ) + +let css = + {| #ocaml_fun_report > div { animation: fade_in_report 1s ease-in; opacity: 1; @@ -540,43 +562,53 @@ let css = {| } |} - let output_html ?(bare = false) ppf report = let html_report = - if bare then - format report + if bare then format report else - E ("div", ["id", "ocaml_fun_report" ], - [ E ("style", [], [ C css ]) ; - format report ]) in + E + ( "div" + , [("id", "ocaml_fun_report")] + , [E ("style", [], [C css]); format report] ) + in output_html ppf [html_report] -let to_html ?bare report = - Format.asprintf "%a" (output_html ?bare) report +let to_html ?bare report = Format.asprintf "%a" (output_html ?bare) report let print ppf items = - let rec print_report ppf items = - Format.pp_print_list format_item ppf items + let rec print_report ppf items = Format.pp_print_list format_item ppf items and format_item ppf = function - | Section (text, contents) -> Format.fprintf ppf "@[@[%a@]@,%a@]" print_text text print_report contents - | SectionMin (text, contents, min) -> Format.fprintf ppf "@[@[%a@ %a@]@,%a@]" print_text text print_min min print_report contents - | Message (text, Failure) -> Format.fprintf ppf [%if"@[Failure: %a@]"] print_text text - | Message (text, Warning) -> Format.fprintf ppf [%if"@[Warning: %a@]"] print_text text - | Message (text, Informative) -> Format.fprintf ppf "@[%a@]" print_text text - | Message (text, Important) -> Format.fprintf ppf [%if"@[Important: %a@]"] print_text text - | Message (text, Success n) -> Format.fprintf ppf [%if"@[Success %d: %a@]"] n print_text text - | Message (text, Penalty n) -> Format.fprintf ppf [%if"@[Penalty %d: %a@]"] (-n) print_text text + | Section (text, contents) -> + Format.fprintf ppf "@[@[%a@]@,%a@]" print_text text + print_report contents + | SectionMin (text, contents, min) -> + Format.fprintf ppf "@[@[%a@ %a@]@,%a@]" print_text text + print_min min print_report contents + | Message (text, Failure) -> + Format.fprintf ppf [%if "@[Failure: %a@]"] print_text text + | Message (text, Warning) -> + Format.fprintf ppf [%if "@[Warning: %a@]"] print_text text + | Message (text, Informative) -> + Format.fprintf ppf "@[%a@]" print_text text + | Message (text, Important) -> + Format.fprintf ppf [%if "@[Important: %a@]"] print_text text + | Message (text, Success n) -> + Format.fprintf ppf [%if "@[Success %d: %a@]"] n print_text text + | Message (text, Penalty n) -> + Format.fprintf ppf [%if "@[Penalty %d: %a@]"] (-n) print_text text and print_text ppf = function - | (Code wa | Output wa) :: Text wb :: rest when not (String.contains (String.trim wa) '\n') -> + | (Code wa | Output wa) :: Text wb :: rest + when not (String.contains (String.trim wa) '\n') -> print_text ppf (Text ("[" ^ String.trim wa ^ "] " ^ wb) :: rest) - | Text wa :: (Code wb | Output wb) :: rest when not (String.contains (String.trim wb) '\n') -> + | Text wa :: (Code wb | Output wb) :: rest + when not (String.contains (String.trim wb) '\n') -> print_text ppf (Text (wa ^ " [" ^ String.trim wb ^ "]") :: rest) | Text wa :: Text wb :: rest -> print_text ppf (Text (wa ^ " " ^ wb) :: rest) | Text w :: rest -> - Format.fprintf ppf "@[%a@]%a" Format.pp_print_text w print_text rest - | Break :: rest -> - Format.fprintf ppf "@\n%a" print_text rest + Format.fprintf ppf "@[%a@]%a" Format.pp_print_text w print_text + rest + | Break :: rest -> Format.fprintf ppf "@\n%a" print_text rest | Code s :: rest -> Format.fprintf ppf "@,%a%a" print_code s print_text rest | Output s :: rest -> @@ -584,16 +616,17 @@ let print ppf items = | [] -> () and print_code ppf s = let s = String.trim s in - Format.fprintf ppf "@[ | " ; + Format.fprintf ppf "@[ | "; for i = 0 to String.length s - 1 do - match String.get s i with + match s.[i] with | '\n' -> Format.fprintf ppf "@, | " | c -> Format.fprintf ppf "%c" c - done ; + done; Format.fprintf ppf "@]" and print_min ppf min = Format.fprintf ppf "%a@ %a" Format.pp_print_string [%i "(minimum mark)"] - Format.pp_print_int min in + Format.pp_print_int min + in Format.fprintf ppf "@[%a@]@." print_report items (* -- report building combinators ------------------------------------------- *) @@ -601,7 +634,7 @@ let print ppf items = let split_text str = let rec spaces acc i = if i < String.length str then - match String.get str i, acc with + match (str.[i], acc) with | (' ' | '\t'), acc -> spaces acc (succ i) | ('\r' | '\n'), ([] | Break :: _) -> spaces acc (succ i) | ('\r' | '\n'), acc -> spaces (Break :: acc) (succ i) @@ -609,23 +642,23 @@ let split_text str = else acc and word acc st i = if i < String.length str then - match String.get str i, acc with + match (str.[i], acc) with | (' ' | '\t' | '\r' | '\n'), acc -> spaces (cut acc st i) (succ i) | _, acc -> word acc st (succ i) else cut acc st i and cut acc i j = - if i = j then acc else Text (String.sub str i (j - i)) :: acc in + if i = j then acc else Text (String.sub str i (j - i)) :: acc + in List.rev (spaces [] 0) -let success ~points ~message = - Message (split_text message, Success points) -let failure ~message = - Message (split_text message, Failure) -let message ~message = - Message (split_text message, Informative) -let info ~message = - Message (split_text message, Important) -let warning ~message = - Message (split_text message, Warning) -let section ~title contents = - Section (split_text title, contents) +let success ~points ~message = Message (split_text message, Success points) + +let failure ~message = Message (split_text message, Failure) + +let message ~message = Message (split_text message, Informative) + +let info ~message = Message (split_text message, Important) + +let warning ~message = Message (split_text message, Warning) + +let section ~title contents = Section (split_text title, contents) diff --git a/src/grader/learnocaml_report.mli b/src/grader/learnocaml_report.mli index 0427cfcdd..f5ef39d95 100644 --- a/src/grader/learnocaml_report.mli +++ b/src/grader/learnocaml_report.mli @@ -11,55 +11,60 @@ type t = item list and item = - | Section of text * t (** A titled block that groups subreports *) - | SectionMin of text * t * int (** A section with a minimum score *) - | Message of text * status (** Basic report block *) + | Section of text * t (** A titled block that groups subreports *) + | SectionMin of text * t * int (** A section with a minimum score *) + | Message of text * status (** Basic report block *) and status = - | Success of int (** With given points *) - | Penalty of int (** With taken points *) - | Failure (** With missed points *) - | Warning (** A student error without influence on the grade *) - | Informative (** A message for the student *) - | Important (** An important message *) + | Success of int (** With given points *) + | Penalty of int (** With taken points *) + | Failure (** With missed points *) + | Warning (** A student error without influence on the grade *) + | Informative (** A message for the student *) + | Important (** An important message *) and text = inline list and inline = - | Text of string (** A word *) - | Break (** Line separator *) - | Code of string (** For expressions *) - | Output of string (** For output *) + | Text of string (** A word *) + | Break (** Line separator *) + | Code of string (** For expressions *) + | Output of string (** For output *) -(** Gets the total successes of a report, and tells if a failure happened *) val result : t -> int * bool +(** Gets the total successes of a report, and tells if a failure happened *) +val scale : ?penalties:bool -> int -> t -> t (** Scales all of the point values of the items in a report by an integer factor. Useful for weighting different components of an exercise. If [penalties] ([true] by default), scales the values of [Penalty] items and the minimum values for [SectionMin], otherwise leaves them untouched. *) -val scale : ?penalties: bool -> int -> t -> t +val to_html : ?bare:bool -> t -> string (** Gets a report as HTML in a string (if [not bare] add a container div and inline style) *) -val to_html : ?bare: bool -> t -> string -(** Outputs a report in text format *) val print : Format.formatter -> t -> unit +(** Outputs a report in text format *) +val output_html : ?bare:bool -> Format.formatter -> t -> unit (** Prints a report as HTML (if [not bare] add a container div and inline style) *) -val output_html : ?bare: bool -> Format.formatter -> t -> unit -(** JSON serializer *) val enc : t Json_encoding.encoding +(** JSON serializer *) (** {2 Learnocaml_report building combinators} *) val failure : message:string -> item + val success : points:int -> message:string -> item + val warning : message:string -> item + val message : message:string -> item + val info : message:string -> item + val section : title:string -> t -> item diff --git a/src/grader/mutation_test.ml b/src/grader/mutation_test.ml index ccba461ff..73c94cf54 100644 --- a/src/grader/mutation_test.ml +++ b/src/grader/mutation_test.ml @@ -1,133 +1,146 @@ open Learnocaml_report -type 'a test_result = - | Pass - | Fail of 'a - | Err of exn +type 'a test_result = Pass | Fail of 'a | Err of exn type 'a mutant_info = string * int * 'a -let uncurry2 f = fun (x, y) -> f x y -let uncurry3 f = fun (x, y, z) -> f x y z -let uncurry4 f = fun (x, y, z, w) -> f x y z w -let map_third f = fun (x, y, z) -> (x, y, f z) +let uncurry2 f (x, y) = f x y + +let uncurry3 f (x, y, z) = f x y z + +let uncurry4 f (x, y, z, w) = f x y z w + +let map_third f (x, y, z) = (x, y, f z) module type S = sig - val run_test_against_mutant: - ?compare: ('b -> 'b -> bool) -> - ('a -> 'b) -> ('a * 'b) -> bool - val test_unit_tests_1: - ?test_student_soln: bool -> - ?test: ('b -> 'b -> bool) -> - ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant_info list -> Learnocaml_report.t - val test_unit_tests_2: - ?test_student_soln: bool -> - ?test: ('c -> 'c -> bool) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant_info list -> Learnocaml_report.t - val test_unit_tests_3: - ?test_student_soln: bool -> - ?test: ('d -> 'd -> bool) -> - ('a -> 'b -> 'c -> 'd) Ty.ty + val run_test_against_mutant : + ?compare:('b -> 'b -> bool) -> ('a -> 'b) -> 'a * 'b -> bool + + val test_unit_tests_1 : + ?test_student_soln:bool + -> ?test:('b -> 'b -> bool) + -> ('a -> 'b) Ty.ty + -> string + -> ('a -> 'b) mutant_info list + -> Learnocaml_report.t + + val test_unit_tests_2 : + ?test_student_soln:bool + -> ?test:('c -> 'c -> bool) + -> ('a -> 'b -> 'c) Ty.ty + -> string + -> ('a -> 'b -> 'c) mutant_info list + -> Learnocaml_report.t + + val test_unit_tests_3 : + ?test_student_soln:bool + -> ?test:('d -> 'd -> bool) + -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) mutant_info list -> Learnocaml_report.t - val test_unit_tests_4: - ?test_student_soln: bool -> - ?test: ('e -> 'e -> bool) -> - ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + + val test_unit_tests_4 : + ?test_student_soln:bool + -> ?test:('e -> 'e -> bool) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) mutant_info list -> Learnocaml_report.t - val passed_mutation_testing: Learnocaml_report.t -> bool + + val passed_mutation_testing : Learnocaml_report.t -> bool end -module Make (Test_lib: Test_lib.S) : S = struct +module Make (Test_lib : Test_lib.S) : S = struct open Test_lib - let run_test_against ?(compare = (=)) f (input, expected) = + let run_test_against ?(compare = ( = )) f (input, expected) = try let run_f () = f input in let output = run_timeout run_f in - if compare output expected then Pass - else Fail output + if compare output expected then Pass else Fail output with exn -> Err exn - let run_test_against_mutant ?(compare = (=)) f (input, expected) = + let run_test_against_mutant ?(compare = ( = )) f (input, expected) = match run_test_against ~compare f (input, expected) with | Pass -> false | _ -> true let typed_printer ty ppf v = Introspection.print_value ppf v ty + let print_with ty = Format.asprintf "%a" (typed_printer ty) + let string_of_exn = print_with [%ty: exn] let test_against_mutant ~compare (name, points, mut) num tests = let result = List.exists (run_test_against_mutant ~compare mut) tests in if result then Message - ([Text "Your tests successfully revealed the bug in implementation"; - Text num; - Text ": "; - Text name], - Success points) + ( [ Text "Your tests successfully revealed the bug in implementation" + ; Text num + ; Text ": " + ; Text name ] + , Success points ) else Message - ([Text "Your tests did not expose the bug in implementation"; Text num], - Failure) - - let test_against_fn - ~compare - ?(show_output=false) - f printer out_printer (input, expected) = - let msg = Message ([Text "Running test"; Code (printer input)], Informative) in + ( [Text "Your tests did not expose the bug in implementation"; Text num] + , Failure ) + + let test_against_fn ~compare ?(show_output = false) f printer out_printer + (input, expected) = + let msg = + Message ([Text "Running test"; Code (printer input)], Informative) + in let expected_str = out_printer expected in let result = run_test_against ~compare f (input, expected) in let report = match result with - | Pass -> [Message ([Text "Test passed with output"; - Code expected_str], - Important)] + | Pass -> + [ Message + ([Text "Test passed with output"; Code expected_str], Important) + ] | Fail out -> - [Message ([Text "Test failed: expected output"; - Code expected_str; - Text "but got"; - if show_output then Code (out_printer out) - else Text "something else"], - Failure)] + [ Message + ( [ Text "Test failed: expected output" + ; Code expected_str + ; Text "but got" + ; ( if show_output then Code (out_printer out) + else Text "something else" ) ] + , Failure ) ] | Err exn -> - [Message ([Text "Test failed: expected output"; - Code expected_str; - Text "but got an unexpected exception"; - Code (string_of_exn exn)], - Failure)] + [ Message + ( [ Text "Test failed: expected output" + ; Code expected_str + ; Text "but got an unexpected exception" + ; Code (string_of_exn exn) ] + , Failure ) ] in msg :: report let section_header = "Your tests..." + let soln_header = "...against the solution" + let mutation_header = "...against our buggy implementations" + let stud_header = "...against your implementation" let test_against_mutants ~compare muts tests = - let string_of_num x = "#" ^ (string_of_int x) in + let string_of_num x = "#" ^ string_of_int x in let test_against_mutant_i i mut = - test_against_mutant - ~compare - mut (string_of_num (succ i)) tests + test_against_mutant ~compare mut (string_of_num (succ i)) tests in List.mapi test_against_mutant_i muts let test_report soln_report stud_section maybe_mut_report = - let soln_section = - Section ([Text soln_header], soln_report) - in + let soln_section = Section ([Text soln_header], soln_report) in let mut_report = match maybe_mut_report with | None -> - Message ([Text "Some of your tests are incorrect and need to be fixed"], - Failure) - | Some report -> - Section ([Text mutation_header], report) + Message + ( [Text "Some of your tests are incorrect and need to be fixed"] + , Failure ) + | Some report -> Section ([Text mutation_header], report) in soln_section :: mut_report :: stud_section @@ -146,20 +159,19 @@ module Make (Test_lib: Test_lib.S) : S = struct not (snd (Learnocaml_report.result report')) | _ -> false - type 'a lookup = - | Unbound of Learnocaml_report.t - | Found of 'a + type 'a lookup = Unbound of Learnocaml_report.t | Found of 'a let no_test_cases_report = [Message ([Text "You have not yet written any test cases."], Failure)] + let soln_not_found_msg = - Message ([Text "Reference solution not found."; - Text "This is an error with the grader."; - Text "Please contact your instructor."], - Failure) + Message + ( [ Text "Reference solution not found." + ; Text "This is an error with the grader." + ; Text "Please contact your instructor." ] + , Failure ) - let append_map f l = - List.fold_right (fun x acc -> (f x) @ acc) l [] + let append_map f l = List.fold_right (fun x acc -> f x @ acc) l [] let test_soln_report ~compare soln printer out_printer tests = match soln with @@ -177,10 +189,8 @@ module Make (Test_lib: Test_lib.S) : S = struct | Unbound report -> report | Found stud -> let tester = - test_against_fn - ~compare - ~show_output: true - stud printer out_printer + test_against_fn ~compare ~show_output:true stud printer + out_printer in append_map tester tests in @@ -189,10 +199,9 @@ module Make (Test_lib: Test_lib.S) : S = struct let test ~compare test_ty printer out_printer name soln stud muts = let test_name = name ^ "_tests" in let report = - test_variable_property test_ty test_name @@ - fun tests -> - if List.length tests = 0 then - no_test_cases_report + test_variable_property test_ty test_name + @@ fun tests -> + if List.length tests = 0 then no_test_cases_report else let soln_report = test_soln_report ~compare soln printer out_printer tests @@ -213,12 +222,9 @@ module Make (Test_lib: Test_lib.S) : S = struct | `Unbound (_, report) -> Unbound report | `Found (_, _, data) -> Found (process data) - - let test_unit_tests_1 - ?(test_student_soln = true) - ?test:(compare = (=)) - ty name muts = - let (domain, range) = Ty.domains ty in + let test_unit_tests_1 ?(test_student_soln = true) ?test:(compare = ( = )) ty + name muts = + let domain, range = Ty.domains ty in let test_ty = Ty.lst (Ty.pair2 domain range) in let in_printer = typed_printer domain in let printer input = @@ -233,18 +239,16 @@ module Make (Test_lib: Test_lib.S) : S = struct in test ~compare test_ty printer out_printer name soln stud muts - let test_unit_tests_2 - ?(test_student_soln = true) - ?test:(compare = (=)) - ty name muts = - let (dom1, rng) = Ty.domains ty in - let (dom2, range) = Ty.domains rng in + let test_unit_tests_2 ?(test_student_soln = true) ?test:(compare = ( = )) ty + name muts = + let dom1, rng = Ty.domains ty in + let dom2, range = Ty.domains rng in let test_ty = Ty.lst (Ty.pair2 (Ty.pair2 dom1 dom2) range) in let in1_printer = typed_printer dom1 in let in2_printer = typed_printer dom2 in let printer (in1, in2) = - Format.asprintf "@[%s@ %a@ %a@]" - name in1_printer in1 in2_printer in2 + Format.asprintf "@[%s@ %a@ %a@]" name in1_printer in1 in2_printer + in2 in let out_printer = print_with range in let muts = List.map (map_third uncurry2) muts in @@ -254,26 +258,20 @@ module Make (Test_lib: Test_lib.S) : S = struct Some (process_lookup uncurry2 lookup_student ty name) else None in - test - ~compare - test_ty printer out_printer name soln stud muts - - let test_unit_tests_3 - ?(test_student_soln = true) - ?test:(compare = (=)) - ty name muts = - let (dom1, rng1) = Ty.domains ty in - let (dom2, rng2) = Ty.domains rng1 in - let (dom3, range) = Ty.domains rng2 in - let test_ty = - Ty.lst (Ty.pair2 (Ty.pair3 dom1 dom2 dom3) range) - in + test ~compare test_ty printer out_printer name soln stud muts + + let test_unit_tests_3 ?(test_student_soln = true) ?test:(compare = ( = )) ty + name muts = + let dom1, rng1 = Ty.domains ty in + let dom2, rng2 = Ty.domains rng1 in + let dom3, range = Ty.domains rng2 in + let test_ty = Ty.lst (Ty.pair2 (Ty.pair3 dom1 dom2 dom3) range) in let in1_printer = typed_printer dom1 in let in2_printer = typed_printer dom2 in let in3_printer = typed_printer dom3 in let printer (in1, in2, in3) = - Format.asprintf "@[%s@ %a@ %a@ %a@]" - name in1_printer in1 in2_printer in2 in3_printer in3 + Format.asprintf "@[%s@ %a@ %a@ %a@]" name in1_printer in1 + in2_printer in2 in3_printer in3 in let out_printer = print_with range in let muts = List.map (map_third uncurry3) muts in @@ -283,28 +281,22 @@ module Make (Test_lib: Test_lib.S) : S = struct Some (process_lookup uncurry3 lookup_student ty name) else None in - test - ~compare - test_ty printer out_printer name soln stud muts - - let test_unit_tests_4 - ?(test_student_soln = true) - ?test:(compare = (=)) - ty name muts = - let (dom1, rng1) = Ty.domains ty in - let (dom2, rng2) = Ty.domains rng1 in - let (dom3, rng3) = Ty.domains rng2 in - let (dom4, range) = Ty.domains rng3 in - let test_ty = - Ty.lst (Ty.pair2 (Ty.pair4 dom1 dom2 dom3 dom4) range) - in + test ~compare test_ty printer out_printer name soln stud muts + + let test_unit_tests_4 ?(test_student_soln = true) ?test:(compare = ( = )) ty + name muts = + let dom1, rng1 = Ty.domains ty in + let dom2, rng2 = Ty.domains rng1 in + let dom3, rng3 = Ty.domains rng2 in + let dom4, range = Ty.domains rng3 in + let test_ty = Ty.lst (Ty.pair2 (Ty.pair4 dom1 dom2 dom3 dom4) range) in let in1_printer = typed_printer dom1 in let in2_printer = typed_printer dom2 in let in3_printer = typed_printer dom3 in let in4_printer = typed_printer dom4 in let printer (in1, in2, in3, in4) = - Format.asprintf "@[%s@ %a@ %a@ %a@ %a@]" - name in1_printer in1 in2_printer in2 in3_printer in3 in4_printer in4 + Format.asprintf "@[%s@ %a@ %a@ %a@ %a@]" name in1_printer in1 + in2_printer in2 in3_printer in3 in4_printer in4 in let out_printer = print_with range in let muts = List.map (map_third uncurry4) muts in @@ -314,8 +306,5 @@ module Make (Test_lib: Test_lib.S) : S = struct Some (process_lookup uncurry4 lookup_student ty name) else None in - test - ~compare - test_ty printer out_printer name soln stud muts - + test ~compare test_ty printer out_printer name soln stud muts end diff --git a/src/grader/mutation_test.mli b/src/grader/mutation_test.mli index d6e81d044..887e6c000 100644 --- a/src/grader/mutation_test.mli +++ b/src/grader/mutation_test.mli @@ -59,7 +59,8 @@ type 'a mutant_info = string * int * 'a equality ([(=)]). *) module type S = sig - + val run_test_against_mutant : + ?compare:('b -> 'b -> bool) -> ('a -> 'b) -> 'a * 'b -> bool (** Run a test (a pair of input and expected output) on a mutant function. Returns true if the mutant *fails* the test, either by deviating @@ -69,33 +70,40 @@ module type S = sig comparing the expected and actual outputs, and defaults to structural equality ([(=)]). *) - val run_test_against_mutant: - ?compare: ('b -> 'b -> bool) -> - ('a -> 'b) -> ('a * 'b) -> bool - - val test_unit_tests_1: - ?test_student_soln: bool -> - ?test: ('b -> 'b -> bool) -> - ('a -> 'b) Ty.ty -> string -> ('a -> 'b) mutant_info list -> Learnocaml_report.t - val test_unit_tests_2: - ?test_student_soln: bool -> - ?test: ('c -> 'c -> bool) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) mutant_info list -> Learnocaml_report.t - val test_unit_tests_3: - ?test_student_soln: bool -> - ?test: ('d -> 'd -> bool) -> - ('a -> 'b -> 'c -> 'd) Ty.ty + + val test_unit_tests_1 : + ?test_student_soln:bool + -> ?test:('b -> 'b -> bool) + -> ('a -> 'b) Ty.ty + -> string + -> ('a -> 'b) mutant_info list + -> Learnocaml_report.t + + val test_unit_tests_2 : + ?test_student_soln:bool + -> ?test:('c -> 'c -> bool) + -> ('a -> 'b -> 'c) Ty.ty + -> string + -> ('a -> 'b -> 'c) mutant_info list + -> Learnocaml_report.t + + val test_unit_tests_3 : + ?test_student_soln:bool + -> ?test:('d -> 'd -> bool) + -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) mutant_info list -> Learnocaml_report.t - val test_unit_tests_4: - ?test_student_soln: bool -> - ?test: ('e -> 'e -> bool) -> - ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + + val test_unit_tests_4 : + ?test_student_soln:bool + -> ?test:('e -> 'e -> bool) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) mutant_info list -> Learnocaml_report.t + val passed_mutation_testing : Learnocaml_report.t -> bool (** To be called on a report returned by one of the [test_unit_tests_] functions, for checking whether the student passed or failed mutation testing. @@ -106,7 +114,6 @@ module type S = sig If this function is called on a report that did not result from one of the above 4 functions, the result is undefined. *) - val passed_mutation_testing: Learnocaml_report.t -> bool end -module Make (Test_lib: Test_lib.S) : S +module Make (Test_lib : Test_lib.S) : S diff --git a/src/grader/test_lib.ml b/src/grader/test_lib.ml index ec82afea6..916c1e482 100644 --- a/src/grader/test_lib.ml +++ b/src/grader/test_lib.ml @@ -7,340 +7,503 @@ * included LICENSE file for details. *) module type S = sig - val set_result : Learnocaml_report.t -> unit type nonrec 'a result = ('a, exn) result (*----------------------------------------------------------------------------*) - module Ast_checker : sig type 'a ast_checker = - ?on_expression: (Parsetree.expression -> Learnocaml_report.t) -> - ?on_pattern: (Parsetree.pattern -> Learnocaml_report.t) -> - ?on_structure_item: (Parsetree.structure_item -> Learnocaml_report.t) -> - ?on_external: (Parsetree.value_description -> Learnocaml_report.t) -> - ?on_include: (Parsetree.include_declaration -> Learnocaml_report.t) -> - ?on_open: (Parsetree.open_description -> Learnocaml_report.t) -> - ?on_module_occurence: (string -> Learnocaml_report.t) -> - ?on_variable_occurence: (string -> Learnocaml_report.t) -> - ?on_function_call: ((Parsetree.expression * (string * Parsetree.expression) list) -> Learnocaml_report.t) -> - 'a -> Learnocaml_report.t + ?on_expression:(Parsetree.expression -> Learnocaml_report.t) + -> ?on_pattern:(Parsetree.pattern -> Learnocaml_report.t) + -> ?on_structure_item:(Parsetree.structure_item -> Learnocaml_report.t) + -> ?on_external:(Parsetree.value_description -> Learnocaml_report.t) + -> ?on_include:(Parsetree.include_declaration -> Learnocaml_report.t) + -> ?on_open:(Parsetree.open_description -> Learnocaml_report.t) + -> ?on_module_occurence:(string -> Learnocaml_report.t) + -> ?on_variable_occurence:(string -> Learnocaml_report.t) + -> ?on_function_call:( Parsetree.expression + * (string * Parsetree.expression) list + -> Learnocaml_report.t) + -> 'a + -> Learnocaml_report.t val ast_check_expr : Parsetree.expression ast_checker + val ast_check_structure : Parsetree.structure ast_checker - val find_binding : Parsetree.structure -> string -> (Parsetree.expression -> Learnocaml_report.t) -> Learnocaml_report.t + val find_binding : + Parsetree.structure + -> string + -> (Parsetree.expression -> Learnocaml_report.t) + -> Learnocaml_report.t + + val forbid : + string -> ('a -> string) -> 'a list -> 'a -> Learnocaml_report.t + + val restrict : + string -> ('a -> string) -> 'a list -> 'a -> Learnocaml_report.t + + val require : string -> ('a -> string) -> 'a -> 'a -> Learnocaml_report.t - val forbid : string -> ('a -> string) -> 'a list -> ('a -> Learnocaml_report.t) - val restrict : string -> ('a -> string) -> 'a list -> ('a -> Learnocaml_report.t) - val require : string -> ('a -> string) -> 'a -> ('a -> Learnocaml_report.t) + val forbid_expr : + string + -> Parsetree.expression list + -> Parsetree.expression + -> Learnocaml_report.t - val forbid_expr : string -> Parsetree.expression list -> (Parsetree.expression -> Learnocaml_report.t) - val restrict_expr : string -> Parsetree.expression list -> (Parsetree.expression -> Learnocaml_report.t) - val require_expr : string -> Parsetree.expression -> (Parsetree.expression -> Learnocaml_report.t) - val forbid_syntax : string -> (_ -> Learnocaml_report.t) - val require_syntax : string -> (_ -> Learnocaml_report.t) + val restrict_expr : + string + -> Parsetree.expression list + -> Parsetree.expression + -> Learnocaml_report.t - val ast_sanity_check : ?modules: string list -> Parsetree.structure -> (unit -> Learnocaml_report.t) -> Learnocaml_report.t + val require_expr : + string + -> Parsetree.expression + -> Parsetree.expression + -> Learnocaml_report.t + val forbid_syntax : string -> _ -> Learnocaml_report.t + + val require_syntax : string -> _ -> Learnocaml_report.t + + val ast_sanity_check : + ?modules:string list + -> Parsetree.structure + -> (unit -> Learnocaml_report.t) + -> Learnocaml_report.t end (*----------------------------------------------------------------------------*) - type 'a tester = - 'a Ty.ty -> 'a result -> 'a result -> Learnocaml_report.t + type 'a tester = 'a Ty.ty -> 'a result -> 'a result -> Learnocaml_report.t - type io_tester = - string -> string -> Learnocaml_report.t + type io_tester = string -> string -> Learnocaml_report.t - type io_postcond = - string -> Learnocaml_report.t + type io_postcond = string -> Learnocaml_report.t exception Timeout of int (*----------------------------------------------------------------------------*) - module Tester : sig - val test : 'a tester + val test_ignore : 'a tester + val test_eq : ('a result -> 'a result -> bool) -> 'a tester + val test_eq_ok : ('a -> 'a -> bool) -> 'a tester + val test_eq_exn : (exn -> exn -> bool) -> 'a tester + val test_canon : ('a result -> 'a result) -> 'a tester + val test_canon_ok : ('a -> 'a) -> 'a tester + val test_canon_error : (exn -> exn) -> 'a tester + val test_translate : ('a -> 'b) -> 'b tester -> 'b Ty.ty -> 'a tester val io_test_ignore : io_tester - val io_test_equals : - ?trim: char list -> ?drop: char list -> io_tester + + val io_test_equals : ?trim:char list -> ?drop:char list -> io_tester + val io_test_lines : - ?trim: char list -> ?drop: char list -> - ?skip_empty: bool -> ?test_line: io_tester -> io_tester - val io_test_items : - ?split: char list -> ?trim: char list -> ?drop: char list -> - ?skip_empty: bool -> ?test_item: io_tester -> io_tester + ?trim:char list + -> ?drop:char list + -> ?skip_empty:bool + -> ?test_line:io_tester + -> io_tester + val io_test_items : + ?split:char list + -> ?trim:char list + -> ?drop:char list + -> ?skip_empty:bool + -> ?test_item:io_tester + -> io_tester end (*----------------------------------------------------------------------------*) - module Mutation : sig - type 'arg arg_mutation_test_callbacks = - { before_reference : 'arg -> unit ; - before_user : 'arg -> unit ; - test : 'ret. ?test_result: 'ret tester -> 'ret tester } - - val arg_mutation_test_callbacks: - ?test: 'a tester -> dup: ('a -> 'a) -> blit:('a -> 'a -> unit) -> 'a Ty.ty -> - 'a arg_mutation_test_callbacks - - val array_arg_mutation_test_callbacks: - ?test: 'a array tester -> 'a array Ty.ty -> - 'a array arg_mutation_test_callbacks - - val ref_arg_mutation_test_callbacks: - ?test: 'a ref tester -> 'a ref Ty.ty -> - 'a ref arg_mutation_test_callbacks - + { before_reference : 'arg -> unit + ; before_user : 'arg -> unit + ; test : 'ret. ?test_result:'ret tester -> 'ret tester } + + val arg_mutation_test_callbacks : + ?test:'a tester + -> dup:('a -> 'a) + -> blit:('a -> 'a -> unit) + -> 'a Ty.ty + -> 'a arg_mutation_test_callbacks + + val array_arg_mutation_test_callbacks : + ?test:'a array tester + -> 'a array Ty.ty + -> 'a array arg_mutation_test_callbacks + + val ref_arg_mutation_test_callbacks : + ?test:'a ref tester -> 'a ref Ty.ty -> 'a ref arg_mutation_test_callbacks end (*----------------------------------------------------------------------------*) - module Sampler : sig type 'a sampler = unit -> 'a + val sample_int : int sampler + val sample_float : float sampler + val sample_string : string sampler + val sample_char : char sampler + val sample_bool : bool sampler - val sample_list : ?min_size: int -> ?max_size: int -> ?dups: bool -> ?sorted: bool -> 'a sampler -> 'a list sampler - val sample_array : ?min_size: int -> ?max_size: int -> ?dups: bool -> ?sorted: bool -> 'a sampler -> 'a array sampler + + val sample_list : + ?min_size:int + -> ?max_size:int + -> ?dups:bool + -> ?sorted:bool + -> 'a sampler + -> 'a list sampler + + val sample_array : + ?min_size:int + -> ?max_size:int + -> ?dups:bool + -> ?sorted:bool + -> 'a sampler + -> 'a array sampler + val sample_pair : 'a sampler -> 'b sampler -> ('a * 'b) sampler + val sample_alternatively : 'a sampler list -> 'a sampler + val sample_cases : 'a list -> 'a sampler + val sample_option : 'a sampler -> 'a option sampler val printable_fun : string -> (_ -> _ as 'f) -> 'f end -(*----------------------------------------------------------------------------*) - + (*----------------------------------------------------------------------------*) module Test_functions_ref_var : sig + val test_ref : 'a Ty.ty -> 'a ref -> 'a -> Learnocaml_report.t - val test_ref : - 'a Ty.ty -> 'a ref -> 'a -> Learnocaml_report.t - - val test_variable : - 'a Ty.ty -> string -> 'a -> Learnocaml_report.t + val test_variable : 'a Ty.ty -> string -> 'a -> Learnocaml_report.t val test_variable_property : 'a Ty.ty -> string -> ('a -> Learnocaml_report.t) -> Learnocaml_report.t val test_variable_against_solution : 'a Ty.ty -> string -> Learnocaml_report.t - end (*----------------------------------------------------------------------------*) - module Test_functions_types : sig val compatible_type : expected:string -> string -> Learnocaml_report.t val existing_type : ?score:int -> string -> bool * Learnocaml_report.t - val abstract_type : ?allow_private:bool -> ?score:int -> string -> bool * Learnocaml_report.t + val abstract_type : + ?allow_private:bool -> ?score:int -> string -> bool * Learnocaml_report.t - val test_student_code : 'a Ty.ty -> ('a -> Learnocaml_report.t) -> Learnocaml_report.t + val test_student_code : + 'a Ty.ty -> ('a -> Learnocaml_report.t) -> Learnocaml_report.t val test_module_property : 'a Ty.ty -> string -> ('a -> Learnocaml_report.t) -> Learnocaml_report.t end (*----------------------------------------------------------------------------*) - module Test_functions_function : sig - val test_function_1 : - ?test: 'b tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : ('a -> unit) -> - ?after : ('a -> ('b * string * string) -> ('b * string * string) -> Learnocaml_report.t) -> - ('a -> 'b) Ty.ty -> string -> ('a * 'b * string * string) list -> Learnocaml_report.t + ?test:'b tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:('a -> unit) + -> ?after:( 'a + -> 'b * string * string + -> 'b * string * string + -> Learnocaml_report.t) + -> ('a -> 'b) Ty.ty + -> string + -> ('a * 'b * string * string) list + -> Learnocaml_report.t val test_function_1_against : - ?gen: int -> - ?test: 'b tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> unit) -> - ?before_user : ('a -> unit) -> - ?after : ('a -> ('b * string * string) -> ('b * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a) -> - ('a -> 'b) Ty.ty -> string -> ('a -> 'b) -> 'a list -> Learnocaml_report.t + ?gen:int + -> ?test:'b tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> unit) + -> ?before_user:('a -> unit) + -> ?after:( 'a + -> 'b * string * string + -> 'b * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a) + -> ('a -> 'b) Ty.ty + -> string + -> ('a -> 'b) + -> 'a list + -> Learnocaml_report.t val test_function_1_against_solution : - ?gen: int -> - ?test: 'b tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> unit) -> - ?before_user : ('a -> unit) -> - ?after : ('a -> ('b * string * string) -> ('b * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a) -> - ('a -> 'b) Ty.ty -> string -> 'a list -> Learnocaml_report.t + ?gen:int + -> ?test:'b tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> unit) + -> ?before_user:('a -> unit) + -> ?after:( 'a + -> 'b * string * string + -> 'b * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a) + -> ('a -> 'b) Ty.ty + -> string + -> 'a list + -> Learnocaml_report.t val test_function_1_against_postcond : - ?gen: int -> - ?test_stdout: io_postcond -> - ?test_stderr: io_postcond -> - ?before_reference : ('a -> unit) -> - ?before_user : ('a -> unit) -> - ?after : ('a -> ('b * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a) -> - ('a -> 'b Ty.ty -> 'b result -> Learnocaml_report.t) -> - ('a -> 'b) Ty.ty -> string -> 'a list -> Learnocaml_report.t + ?gen:int + -> ?test_stdout:io_postcond + -> ?test_stderr:io_postcond + -> ?before_reference:('a -> unit) + -> ?before_user:('a -> unit) + -> ?after:('a -> 'b * string * string -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a) + -> ('a -> 'b Ty.ty -> 'b result -> Learnocaml_report.t) + -> ('a -> 'b) Ty.ty + -> string + -> 'a list + -> Learnocaml_report.t (*----------------------------------------------------------------------------*) val test_function_2 : - ?test: 'c tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : ('a -> 'b -> unit) -> - ?after : ('a -> 'b -> ('c * string * string) -> ('c * string * string) -> Learnocaml_report.t) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a * 'b * 'c * string * string) list -> Learnocaml_report.t + ?test:'c tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:('a -> 'b -> unit) + -> ?after:( 'a + -> 'b + -> 'c * string * string + -> 'c * string * string + -> Learnocaml_report.t) + -> ('a -> 'b -> 'c) Ty.ty + -> string + -> ('a * 'b * 'c * string * string) list + -> Learnocaml_report.t val test_function_2_against : - ?gen: int -> - ?test: 'c tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> unit) -> - ?before_user : ('a -> 'b -> unit) -> - ?after : ('a -> 'b -> ('c * string * string) -> ('c * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a -> 'b -> 'c) -> ('a * 'b) list -> Learnocaml_report.t + ?gen:int + -> ?test:'c tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> unit) + -> ?before_user:('a -> 'b -> unit) + -> ?after:( 'a + -> 'b + -> 'c * string * string + -> 'c * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b) + -> ('a -> 'b -> 'c) Ty.ty + -> string + -> ('a -> 'b -> 'c) + -> ('a * 'b) list + -> Learnocaml_report.t val test_function_2_against_solution : - ?gen: int -> - ?test: 'c tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> unit) -> - ?before_user : ('a -> 'b -> unit) -> - ?after : ('a -> 'b -> ('c * string * string) -> ('c * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a * 'b) list -> Learnocaml_report.t + ?gen:int + -> ?test:'c tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> unit) + -> ?before_user:('a -> 'b -> unit) + -> ?after:( 'a + -> 'b + -> 'c * string * string + -> 'c * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b) + -> ('a -> 'b -> 'c) Ty.ty + -> string + -> ('a * 'b) list + -> Learnocaml_report.t val test_function_2_against_postcond : - ?gen: int -> - ?test_stdout: io_postcond -> - ?test_stderr: io_postcond -> - ?before_reference : ('a -> 'b -> unit) -> - ?before_user : ('a -> 'b -> unit) -> - ?after : ('a -> 'b -> ('c * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b) -> - ('a -> 'b -> 'c Ty.ty -> 'c result -> Learnocaml_report.t) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a * 'b) list -> Learnocaml_report.t + ?gen:int + -> ?test_stdout:io_postcond + -> ?test_stderr:io_postcond + -> ?before_reference:('a -> 'b -> unit) + -> ?before_user:('a -> 'b -> unit) + -> ?after:('a -> 'b -> 'c * string * string -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b) + -> ('a -> 'b -> 'c Ty.ty -> 'c result -> Learnocaml_report.t) + -> ('a -> 'b -> 'c) Ty.ty + -> string + -> ('a * 'b) list + -> Learnocaml_report.t (*----------------------------------------------------------------------------*) val test_function_3 : - ?test: 'd tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : ('a -> 'b -> 'c -> unit) -> - ?after : ('a -> 'b -> 'c -> ('d * string * string) -> ('d * string * string) -> Learnocaml_report.t) -> - ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a * 'b * 'c * 'd * string * string) list -> Learnocaml_report.t + ?test:'d tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:('a -> 'b -> 'c -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd * string * string + -> 'd * string * string + -> Learnocaml_report.t) + -> ('a -> 'b -> 'c -> 'd) Ty.ty + -> string + -> ('a * 'b * 'c * 'd * string * string) list + -> Learnocaml_report.t val test_function_3_against : - ?gen: int -> - ?test: 'd tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> 'c -> unit) -> - ?before_user : ('a -> 'b -> 'c -> unit) -> - ?after : ('a -> 'b -> 'c -> ('d * string * string) -> ('d * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c) -> - ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd) -> ('a * 'b * 'c) list -> Learnocaml_report.t + ?gen:int + -> ?test:'d tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> 'c -> unit) + -> ?before_user:('a -> 'b -> 'c -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd * string * string + -> 'd * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c) + -> ('a -> 'b -> 'c -> 'd) Ty.ty + -> string + -> ('a -> 'b -> 'c -> 'd) + -> ('a * 'b * 'c) list + -> Learnocaml_report.t val test_function_3_against_solution : - ?gen: int -> - ?test: 'd tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> 'c -> unit) -> - ?before_user : ('a -> 'b -> 'c -> unit) -> - ?after : ('a -> 'b -> 'c -> ('d * string * string) -> ('d * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c) -> - ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a * 'b * 'c) list -> Learnocaml_report.t + ?gen:int + -> ?test:'d tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> 'c -> unit) + -> ?before_user:('a -> 'b -> 'c -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd * string * string + -> 'd * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c) + -> ('a -> 'b -> 'c -> 'd) Ty.ty + -> string + -> ('a * 'b * 'c) list + -> Learnocaml_report.t val test_function_3_against_postcond : - ?gen: int -> - ?test_stdout: io_postcond -> - ?test_stderr: io_postcond -> - ?before_reference : ('a -> 'b -> 'c -> unit) -> - ?before_user : ('a -> 'b -> 'c -> unit) -> - ?after : ('a -> 'b -> 'c -> ('d * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c) -> - ('a -> 'b -> 'c -> 'd Ty.ty -> 'd result -> Learnocaml_report.t) -> - ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a * 'b * 'c) list -> Learnocaml_report.t + ?gen:int + -> ?test_stdout:io_postcond + -> ?test_stderr:io_postcond + -> ?before_reference:('a -> 'b -> 'c -> unit) + -> ?before_user:('a -> 'b -> 'c -> unit) + -> ?after:('a -> 'b -> 'c -> 'd * string * string -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c) + -> ('a -> 'b -> 'c -> 'd Ty.ty -> 'd result -> Learnocaml_report.t) + -> ('a -> 'b -> 'c -> 'd) Ty.ty + -> string + -> ('a * 'b * 'c) list + -> Learnocaml_report.t (*----------------------------------------------------------------------------*) val test_function_4 : - ?test: 'e tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : ('a -> 'b -> 'c -> 'd -> unit) -> - ?after : ('a -> 'b -> 'c -> 'd -> ('e * string * string) -> ('e * string * string) -> Learnocaml_report.t) -> - ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a * 'b * 'c * 'd * 'e * string * string) list -> Learnocaml_report.t + ?test:'e tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:('a -> 'b -> 'c -> 'd -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd + -> 'e * string * string + -> 'e * string * string + -> Learnocaml_report.t) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a * 'b * 'c * 'd * 'e * string * string) list + -> Learnocaml_report.t val test_function_4_against : - ?gen: int -> - ?test: 'e tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> 'c -> 'd -> unit) -> - ?before_user : ('a -> 'b -> 'c -> 'd -> unit) -> - ?after : ('a -> 'b -> 'c -> 'd -> ('e * string * string) -> ('e * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c * 'd) -> - ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a -> 'b -> 'c -> 'd -> 'e) - -> ('a * 'b * 'c * 'd) list -> Learnocaml_report.t + ?gen:int + -> ?test:'e tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> 'c -> 'd -> unit) + -> ?before_user:('a -> 'b -> 'c -> 'd -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd + -> 'e * string * string + -> 'e * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c * 'd) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a -> 'b -> 'c -> 'd -> 'e) + -> ('a * 'b * 'c * 'd) list + -> Learnocaml_report.t val test_function_4_against_solution : - ?gen: int -> - ?test: 'e tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> 'c -> 'd -> unit) -> - ?before_user : ('a -> 'b -> 'c -> 'd -> unit) -> - ?after : ('a -> 'b -> 'c -> 'd -> ('e * string * string) -> ('e * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c * 'd) -> - ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a * 'b * 'c * 'd) list -> Learnocaml_report.t + ?gen:int + -> ?test:'e tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> 'c -> 'd -> unit) + -> ?before_user:('a -> 'b -> 'c -> 'd -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd + -> 'e * string * string + -> 'e * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c * 'd) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a * 'b * 'c * 'd) list + -> Learnocaml_report.t val test_function_4_against_postcond : - ?gen: int -> - ?test_stdout: io_postcond -> - ?test_stderr: io_postcond -> - ?before_reference : ('a -> 'b -> 'c -> 'd -> unit) -> - ?before_user : ('a -> 'b -> 'c -> 'd -> unit) -> - ?after : ('a -> 'b -> 'c -> 'd -> ('e * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c * 'd) -> - ('a -> 'b -> 'c -> 'd -> 'e Ty.ty -> 'e result -> Learnocaml_report.t) -> - ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a * 'b * 'c * 'd) list -> Learnocaml_report.t - + ?gen:int + -> ?test_stdout:io_postcond + -> ?test_stderr:io_postcond + -> ?before_reference:('a -> 'b -> 'c -> 'd -> unit) + -> ?before_user:('a -> 'b -> 'c -> 'd -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd + -> 'e * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c * 'd) + -> ('a -> 'b -> 'c -> 'd -> 'e Ty.ty -> 'e result -> Learnocaml_report.t) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a * 'b * 'c * 'd) list + -> Learnocaml_report.t end (*----------------------------------------------------------------------------*) - module Test_functions_generic : sig - val run_timeout : ?time:int -> (unit -> 'a) -> 'a val exec : (unit -> 'a) -> ('a * string * string) result @@ -349,113 +512,125 @@ module type S = sig (*----------------------------------------------------------------------------*) - include (module type of Fun_ty - with type ('a, 'b, 'c) args = ('a, 'b, 'c) Fun_ty.args - and type ('a, 'b, 'c) fun_ty = ('a, 'b, 'c) Fun_ty.fun_ty) + include + module type of Fun_ty + with type ('a, 'b, 'c) args = ('a, 'b, 'c) Fun_ty.args + and type ('a, 'b, 'c) fun_ty = ('a, 'b, 'c) Fun_ty.fun_ty val ty_of_prot : (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> ('ar -> 'row) Ty.ty - [@@ocaml.deprecated "Use ty_of_fun_ty instead."] + [@@ocaml.deprecated "Use ty_of_fun_ty instead."] + + type 'a lookup = + unit + -> [ `Found of string * Learnocaml_report.t * 'a + | `Unbound of string * Learnocaml_report.t ] - type 'a lookup = unit -> [ `Found of string * Learnocaml_report.t * 'a | `Unbound of string * Learnocaml_report.t ] + val lookup : 'a Ty.ty -> ?display_name:string -> string -> 'a lookup - val lookup : 'a Ty.ty -> ?display_name: string -> string -> 'a lookup val lookup_student : 'a Ty.ty -> string -> 'a lookup + val lookup_solution : 'a Ty.ty -> string -> 'a lookup + val found : string -> 'a -> 'a lookup + val name : 'a lookup -> string - val test_value : 'a lookup -> ('a -> Learnocaml_report.t) -> Learnocaml_report.t + val test_value : + 'a lookup -> ('a -> Learnocaml_report.t) -> Learnocaml_report.t val test_function : - ?test: 'ret tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> - unit) -> - ?after : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> - ('ret * string * string) -> - ('ret * string * string) -> - Learnocaml_report.t) -> - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - ('ar -> 'row) lookup -> - (('ar -> 'row, 'ar -> 'urow, 'ret) args * (unit -> 'ret)) list -> - Learnocaml_report.t - - val test_function_against : - ?gen: int -> - ?test: 'ret tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) -> - ?before_user : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) -> - ?after : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> - ('ret * string * string) -> - ('ret * string * string) -> - Learnocaml_report.t) -> - ?sampler: - (unit -> ('ar -> 'row, 'ar -> 'urow, 'ret) args) -> - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - ('ar -> 'row) lookup -> ('ar -> 'row) lookup -> - ('ar -> 'row, 'ar -> 'urow, 'ret) args list -> - Learnocaml_report.t - - val test_function_against_solution : - ?gen:int -> - ?test: 'ret tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference: - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) -> - ?before_user: - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) -> - ?after: - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> - 'ret * string * string -> - 'ret * string * string -> - Learnocaml_report.item list) -> - ?sampler: - (unit -> ('ar -> 'row, 'ar -> 'urow, 'ret) args) -> - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - string -> - ('ar -> 'row, 'ar -> 'urow, 'ret) args list -> - Learnocaml_report.item list - - val (==>) : 'params -> 'ret -> 'params * (unit -> 'ret) - + ?test:'ret tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?after:( ('ar -> 'row, 'ar -> 'urow, 'ret) args + -> 'ret * string * string + -> 'ret * string * string + -> Learnocaml_report.t) + -> (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty + -> ('ar -> 'row) lookup + -> (('ar -> 'row, 'ar -> 'urow, 'ret) args * (unit -> 'ret)) list + -> Learnocaml_report.t + + val test_function_against : + ?gen:int + -> ?test:'ret tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?before_user:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?after:( ('ar -> 'row, 'ar -> 'urow, 'ret) args + -> 'ret * string * string + -> 'ret * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> ('ar -> 'row, 'ar -> 'urow, 'ret) args) + -> (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty + -> ('ar -> 'row) lookup + -> ('ar -> 'row) lookup + -> ('ar -> 'row, 'ar -> 'urow, 'ret) args list + -> Learnocaml_report.t + + val test_function_against_solution : + ?gen:int + -> ?test:'ret tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?before_user:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?after:( ('ar -> 'row, 'ar -> 'urow, 'ret) args + -> 'ret * string * string + -> 'ret * string * string + -> Learnocaml_report.item list) + -> ?sampler:(unit -> ('ar -> 'row, 'ar -> 'urow, 'ret) args) + -> (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty + -> string + -> ('ar -> 'row, 'ar -> 'urow, 'ret) args list + -> Learnocaml_report.item list + + val ( ==> ) : 'params -> 'ret -> 'params * (unit -> 'ret) end - val (@@@) : ('a -> Learnocaml_report.t) -> ('a -> Learnocaml_report.t) -> ('a -> Learnocaml_report.t) - val (@@>) : Learnocaml_report.t -> (unit -> Learnocaml_report.t) -> Learnocaml_report.t - val (@@=) : Learnocaml_report.t -> (unit -> Learnocaml_report.t) -> Learnocaml_report.t + val ( @@@ ) : + ('a -> Learnocaml_report.t) + -> ('a -> Learnocaml_report.t) + -> 'a + -> Learnocaml_report.t + + val ( @@> ) : + Learnocaml_report.t -> (unit -> Learnocaml_report.t) -> Learnocaml_report.t + + val ( @@= ) : + Learnocaml_report.t -> (unit -> Learnocaml_report.t) -> Learnocaml_report.t (**/**) - include (module type of Ast_checker) - include (module type of Tester) - include (module type of Mutation) - include (module type of Sampler) - include (module type of Test_functions_types) - include (module type of Test_functions_ref_var) - include (module type of Test_functions_function) - include (module type of Test_functions_generic) + include module type of Ast_checker + + include module type of Tester + + include module type of Mutation + + include module type of Sampler + + include module type of Test_functions_types + + include module type of Test_functions_ref_var + + include module type of Test_functions_function + + include module type of Test_functions_generic end -module Make - (Params : sig - val results : Learnocaml_report.t option ref - (* val set_progress : string -> unit *) - val timeout : int option - module Introspection : Introspection_intf.INTROSPECTION - end) : S = struct +module Make (Params : sig + val results : Learnocaml_report.t option ref + + (* val set_progress : string -> unit *) + val timeout : int option - let set_result report = - Params.results := Some report + module Introspection : Introspection_intf.INTROSPECTION +end) : S = struct + let set_result report = Params.results := Some report type nonrec 'a result = ('a, exn) result @@ -463,354 +638,424 @@ module Make module Ast_checker = struct type 'a ast_checker = - ?on_expression: (Parsetree.expression -> Learnocaml_report.t) -> - ?on_pattern: (Parsetree.pattern -> Learnocaml_report.t) -> - ?on_structure_item: (Parsetree.structure_item -> Learnocaml_report.t) -> - ?on_external: (Parsetree.value_description -> Learnocaml_report.t) -> - ?on_include: (Parsetree.include_declaration -> Learnocaml_report.t) -> - ?on_open: (Parsetree.open_description -> Learnocaml_report.t) -> - ?on_module_occurence: (string -> Learnocaml_report.t) -> - ?on_variable_occurence: (string -> Learnocaml_report.t) -> - ?on_function_call: ((Parsetree.expression * (string * Parsetree.expression) list) -> Learnocaml_report.t) -> - 'a -> Learnocaml_report.t - - let ast_check f - ?(on_expression = fun _ -> []) - ?(on_pattern = fun _ -> []) - ?(on_structure_item = fun _ -> []) - ?(on_external = fun _ -> []) - ?(on_include = fun _ -> []) - ?(on_open = fun _ -> []) - ?(on_module_occurence = fun _ -> []) - ?(on_variable_occurence = fun _ -> []) - ?(on_function_call = fun _ -> []) - arg = + ?on_expression:(Parsetree.expression -> Learnocaml_report.t) + -> ?on_pattern:(Parsetree.pattern -> Learnocaml_report.t) + -> ?on_structure_item:(Parsetree.structure_item -> Learnocaml_report.t) + -> ?on_external:(Parsetree.value_description -> Learnocaml_report.t) + -> ?on_include:(Parsetree.include_declaration -> Learnocaml_report.t) + -> ?on_open:(Parsetree.open_description -> Learnocaml_report.t) + -> ?on_module_occurence:(string -> Learnocaml_report.t) + -> ?on_variable_occurence:(string -> Learnocaml_report.t) + -> ?on_function_call:( Parsetree.expression + * (string * Parsetree.expression) list + -> Learnocaml_report.t) + -> 'a + -> Learnocaml_report.t + + let ast_check f ?(on_expression = fun _ -> []) ?(on_pattern = fun _ -> []) + ?(on_structure_item = fun _ -> []) ?(on_external = fun _ -> []) + ?(on_include = fun _ -> []) ?(on_open = fun _ -> []) + ?(on_module_occurence = fun _ -> []) + ?(on_variable_occurence = fun _ -> []) + ?(on_function_call = fun _ -> []) arg = let open Parsetree in let open Ast_mapper in - let res : Learnocaml_report.t list ref = ref [] in + let res : Learnocaml_report.t list ref = ref [] in let add l = res := l :: !res in - let name { Location.txt; _ } = String.concat "." (Longident.flatten txt) in + let name {Location.txt; _} = String.concat "." (Longident.flatten txt) in let variables = ref [] in let modules = ref [] in - let treat_module_prefixes { Location.txt; _ } = + let treat_module_prefixes {Location.txt; _} = match Longident.flatten txt with | fst :: _ when List.mem fst !modules (* shadowed *) -> () - | txt -> - let rec all = function - | [] -> () - | _ :: rest as l -> - add @@ on_module_occurence (String.concat "." (List.rev l)) ; - all rest in - match List.rev txt with - | [] -> () - | _ :: prefixes -> all prefixes in - let treat_module ({ Location.txt; _ } as ident) = - treat_module_prefixes ident ; + | txt -> ( + let rec all = function + | [] -> () + | _ :: rest as l -> + add @@ on_module_occurence (String.concat "." (List.rev l)); + all rest + in + match List.rev txt with [] -> () | _ :: prefixes -> all prefixes ) + in + let treat_module ({Location.txt; _} as ident) = + treat_module_prefixes ident; match Longident.flatten txt with - | [ m ] | m :: _ when List.mem m !modules (* shadowed *) -> () - | _ -> add @@ on_module_occurence (name ident) in - let treat_variable ({ Location.txt; _ } as ident) = - treat_module_prefixes ident ; + | ([m] | m :: _) when List.mem m !modules (* shadowed *) -> () + | _ -> add @@ on_module_occurence (name ident) + in + let treat_variable ({Location.txt; _} as ident) = + treat_module_prefixes ident; match Longident.flatten txt with - | m :: _ :: _ when List.mem m !modules (* shadowed *) -> () - | [ v ] when List.mem v !variables (* shadowed *) -> () - | _ -> add @@ on_variable_occurence (name ident) in + | m :: _ :: _ when List.mem m !modules (* shadowed *) -> () + | [v] when List.mem v !variables (* shadowed *) -> () + | _ -> add @@ on_variable_occurence (name ident) + in let expr mapper expr = - add @@ on_expression expr ; + add @@ on_expression expr; match expr with - | { pexp_desc = Pexp_open (popen_override, popen_lid, iexpr); _ } -> - let o = { popen_lid ; popen_override ; - popen_loc = Location.none ; - popen_attributes = [] } in - let before = !modules in - add @@ on_open o ; - treat_module popen_lid ; - variables := [] ; - modules := [] (* over approximation *) ; - ignore (mapper.expr mapper iexpr) ; - modules := before ; - expr - | { pexp_desc = Pexp_letmodule ({ Location.txt = name; _ }, mexpr, iexpr); _ } -> - let before = !modules in - let variables_before = !variables in - ignore (mapper.module_expr mapper mexpr) ; - modules := name :: before ; - variables := variables_before ; - ignore (mapper.expr mapper iexpr) ; - modules := before ; - expr - | { pexp_desc = Pexp_apply (fn, args); _ } as e -> - let args = List.map - (function - | (Asttypes.Nolabel, v) -> ("", v) - | ((Asttypes.Labelled n | Asttypes.Optional n), v) -> (n, v)) - args in - add @@ on_function_call (fn, args) ; - default_mapper.expr mapper e - | { pexp_desc = Pexp_ident ident; _ } as e -> - treat_variable ident ; - default_mapper.expr mapper e - | { pexp_desc = Pexp_construct (ident, _); _ } as e -> - treat_module_prefixes ident ; - default_mapper.expr mapper e - | { pexp_desc = Pexp_record (fields, _); _ } as e -> - List.iter (fun (ident, _) -> treat_module_prefixes ident) fields ; - default_mapper.expr mapper e - | { pexp_desc = Pexp_field (_, ident); _ } as e -> - treat_module_prefixes ident ; - default_mapper.expr mapper e - | { pexp_desc = Pexp_setfield (_, ident, _); _ } as e -> - treat_module_prefixes ident ; - default_mapper.expr mapper e - | { pexp_desc = Pexp_new ident; _ } as e -> - treat_module_prefixes ident ; - default_mapper.expr mapper e - | { pexp_desc = Pexp_let (Asttypes.Nonrecursive, pvs, iexpr); _ } -> - let before = !variables in - let modules_before = !modules in - List.iter (fun { pvb_expr; _ } -> - ignore (mapper.expr mapper pvb_expr)) - pvs ; - List.iter (fun { pvb_pat; _ } -> - ignore (mapper.pat mapper pvb_pat)) - pvs ; - ignore (mapper.expr mapper iexpr) ; - variables := before ; - modules := modules_before ; - expr - | { pexp_desc = Pexp_let (Asttypes.Recursive, pvs, iexpr); _ } -> - let before = !variables in - let modules_before = !modules in - List.iter (fun { pvb_pat; _ } -> - ignore (mapper.pat mapper pvb_pat)) - pvs ; - List.iter (fun { pvb_expr; _ } -> - ignore (mapper.expr mapper pvb_expr)) - pvs ; - ignore (mapper.expr mapper iexpr) ; - variables := before ; - modules := modules_before ; - expr - | { pexp_desc = Pexp_for (pat, sexpr, eexpr, _, iexpr); _ } -> - let before = !variables in - let modules_before = !modules in - ignore (mapper.expr mapper sexpr) ; - ignore (mapper.expr mapper eexpr) ; - ignore (mapper.pat mapper pat) ; - ignore (mapper.expr mapper iexpr) ; - variables := before ; - modules := modules_before ; - expr - | { pexp_desc = Pexp_fun (label, vexpr, pat, iexpr); _ } -> - let before = !variables in - let modules_before = !modules in - (match vexpr with + | {pexp_desc = Pexp_open (popen_override, popen_lid, iexpr); _} -> + let o = + { popen_lid + ; popen_override + ; popen_loc = Location.none + ; popen_attributes = [] } + in + let before = !modules in + add @@ on_open o; + treat_module popen_lid; + variables := []; + modules := [] (* over approximation *); + ignore (mapper.expr mapper iexpr); + modules := before; + expr + | { pexp_desc = Pexp_letmodule ({Location.txt = name; _}, mexpr, iexpr); _ + } -> + let before = !modules in + let variables_before = !variables in + ignore (mapper.module_expr mapper mexpr); + modules := name :: before; + variables := variables_before; + ignore (mapper.expr mapper iexpr); + modules := before; + expr + | {pexp_desc = Pexp_apply (fn, args); _} as e -> + let args = + List.map + (function + | Asttypes.Nolabel, v -> ("", v) + | (Asttypes.Labelled n | Asttypes.Optional n), v -> (n, v)) + args + in + add @@ on_function_call (fn, args); + default_mapper.expr mapper e + | {pexp_desc = Pexp_ident ident; _} as e -> + treat_variable ident; + default_mapper.expr mapper e + | {pexp_desc = Pexp_construct (ident, _); _} as e -> + treat_module_prefixes ident; + default_mapper.expr mapper e + | {pexp_desc = Pexp_record (fields, _); _} as e -> + List.iter (fun (ident, _) -> treat_module_prefixes ident) fields; + default_mapper.expr mapper e + | {pexp_desc = Pexp_field (_, ident); _} as e -> + treat_module_prefixes ident; + default_mapper.expr mapper e + | {pexp_desc = Pexp_setfield (_, ident, _); _} as e -> + treat_module_prefixes ident; + default_mapper.expr mapper e + | {pexp_desc = Pexp_new ident; _} as e -> + treat_module_prefixes ident; + default_mapper.expr mapper e + | {pexp_desc = Pexp_let (Asttypes.Nonrecursive, pvs, iexpr); _} -> + let before = !variables in + let modules_before = !modules in + List.iter + (fun {pvb_expr; _} -> ignore (mapper.expr mapper pvb_expr)) + pvs; + List.iter + (fun {pvb_pat; _} -> ignore (mapper.pat mapper pvb_pat)) + pvs; + ignore (mapper.expr mapper iexpr); + variables := before; + modules := modules_before; + expr + | {pexp_desc = Pexp_let (Asttypes.Recursive, pvs, iexpr); _} -> + let before = !variables in + let modules_before = !modules in + List.iter + (fun {pvb_pat; _} -> ignore (mapper.pat mapper pvb_pat)) + pvs; + List.iter + (fun {pvb_expr; _} -> ignore (mapper.expr mapper pvb_expr)) + pvs; + ignore (mapper.expr mapper iexpr); + variables := before; + modules := modules_before; + expr + | {pexp_desc = Pexp_for (pat, sexpr, eexpr, _, iexpr); _} -> + let before = !variables in + let modules_before = !modules in + ignore (mapper.expr mapper sexpr); + ignore (mapper.expr mapper eexpr); + ignore (mapper.pat mapper pat); + ignore (mapper.expr mapper iexpr); + variables := before; + modules := modules_before; + expr + | {pexp_desc = Pexp_fun (label, vexpr, pat, iexpr); _} -> + let before = !variables in + let modules_before = !modules in + ( match vexpr with | Some vexpr -> ignore (mapper.expr mapper vexpr) - | None -> ()) ; - (match label with + | None -> () ); + ( match label with | Asttypes.Nolabel -> () - | Asttypes.Labelled label - | Asttypes.Optional label -> - variables := label :: !variables) ; - ignore (mapper.pat mapper pat) ; - ignore (mapper.expr mapper iexpr) ; - variables := before ; - modules := modules_before ; - expr - | e -> default_mapper.expr mapper e in + | Asttypes.Labelled label | Asttypes.Optional label -> + variables := label :: !variables ); + ignore (mapper.pat mapper pat); + ignore (mapper.expr mapper iexpr); + variables := before; + modules := modules_before; + expr + | e -> default_mapper.expr mapper e + in let structure_item mapper structure_item = - add @@ on_structure_item structure_item ; + add @@ on_structure_item structure_item; match structure_item with - | { pstr_desc = Pstr_module { pmb_name ; pmb_expr; _ }; _ } -> - let before = !modules in - let variables_before = !variables in - ignore (mapper.module_expr mapper pmb_expr) ; - modules := pmb_name.Location.txt :: before ; - variables := variables_before ; - structure_item - | { pstr_desc = Pstr_recmodule mbs; _ } -> - let variables_before = !variables in - List.iter (fun { pmb_name; _ } -> modules := pmb_name.Location.txt :: !modules) mbs ; - let before = !modules in - List.iter (fun { pmb_expr; _ } -> - ignore (mapper.module_expr mapper pmb_expr) ; - variables := variables_before ; - modules := before) mbs ; - structure_item - | { pstr_desc = Pstr_open o; _ } as si -> - add @@ on_open o ; - treat_module o.popen_lid ; - ignore (default_mapper.structure_item mapper si) ; - variables := [] ; - modules := [] (* over approximation *) ; - structure_item - | { pstr_desc = Pstr_include i; _ } as si -> - add @@ on_include i ; - ignore (default_mapper.structure_item mapper si) ; - variables := [] ; - modules := [] (* over approximation *) ; - structure_item - | { pstr_desc = Pstr_primitive p; _ } as si -> - add @@ on_external p ; - ignore (default_mapper.structure_item mapper si) ; - variables := p.pval_name.Location.txt :: !variables ; - structure_item - | si -> default_mapper.structure_item mapper si in + | {pstr_desc = Pstr_module {pmb_name; pmb_expr; _}; _} -> + let before = !modules in + let variables_before = !variables in + ignore (mapper.module_expr mapper pmb_expr); + modules := pmb_name.Location.txt :: before; + variables := variables_before; + structure_item + | {pstr_desc = Pstr_recmodule mbs; _} -> + let variables_before = !variables in + List.iter + (fun {pmb_name; _} -> + modules := pmb_name.Location.txt :: !modules ) + mbs; + let before = !modules in + List.iter + (fun {pmb_expr; _} -> + ignore (mapper.module_expr mapper pmb_expr); + variables := variables_before; + modules := before ) + mbs; + structure_item + | {pstr_desc = Pstr_open o; _} as si -> + add @@ on_open o; + treat_module o.popen_lid; + ignore (default_mapper.structure_item mapper si); + variables := []; + modules := [] (* over approximation *); + structure_item + | {pstr_desc = Pstr_include i; _} as si -> + add @@ on_include i; + ignore (default_mapper.structure_item mapper si); + variables := []; + modules := [] (* over approximation *); + structure_item + | {pstr_desc = Pstr_primitive p; _} as si -> + add @@ on_external p; + ignore (default_mapper.structure_item mapper si); + variables := p.pval_name.Location.txt :: !variables; + structure_item + | si -> default_mapper.structure_item mapper si + in let typ mapper typ = match typ with - | { ptyp_desc = Ptyp_constr (ident, _); _ } as t -> - treat_module_prefixes ident ; - default_mapper.typ mapper t - | { ptyp_desc = Ptyp_class (lid, _); _ } as t -> - treat_module_prefixes lid ; - default_mapper.typ mapper t + | {ptyp_desc = Ptyp_constr (ident, _); _} as t -> + treat_module_prefixes ident; + default_mapper.typ mapper t + | {ptyp_desc = Ptyp_class (lid, _); _} as t -> + treat_module_prefixes lid; + default_mapper.typ mapper t | typ -> default_mapper.typ mapper typ in let module_expr mapper module_expr = match module_expr with - | { pmod_desc = Pmod_ident ident; _ } as me -> - treat_module ident ; - default_mapper.module_expr mapper me + | {pmod_desc = Pmod_ident ident; _} as me -> + treat_module ident; + default_mapper.module_expr mapper me | me -> default_mapper.module_expr mapper me in let pat mapper pat = - add @@ on_pattern pat ; + add @@ on_pattern pat; match pat with - | { ppat_desc = (Ppat_var n | Ppat_alias (_, n)); _ } as p -> - variables := n.Location.txt :: !variables ; - default_mapper.pat mapper p - | { ppat_desc = Ppat_unpack n; _ } as p -> - modules := n.Location.txt :: !modules ; - default_mapper.pat mapper p - | { ppat_desc = Ppat_construct (ident, _); _ } as p -> - treat_module_prefixes ident ; - default_mapper.pat mapper p - | { ppat_desc = Ppat_record (fields, _); _ } as p -> - List.iter (fun (ident, _) -> treat_module_prefixes ident) fields ; - default_mapper.pat mapper p - | { ppat_desc = Ppat_type ident; _ } as p -> - treat_module_prefixes ident ; - default_mapper.pat mapper p - | p -> default_mapper.pat mapper p in - let case mapper ({ pc_lhs ; pc_guard ; pc_rhs } as case) = + | {ppat_desc = Ppat_var n | Ppat_alias (_, n); _} as p -> + variables := n.Location.txt :: !variables; + default_mapper.pat mapper p + | {ppat_desc = Ppat_unpack n; _} as p -> + modules := n.Location.txt :: !modules; + default_mapper.pat mapper p + | {ppat_desc = Ppat_construct (ident, _); _} as p -> + treat_module_prefixes ident; + default_mapper.pat mapper p + | {ppat_desc = Ppat_record (fields, _); _} as p -> + List.iter (fun (ident, _) -> treat_module_prefixes ident) fields; + default_mapper.pat mapper p + | {ppat_desc = Ppat_type ident; _} as p -> + treat_module_prefixes ident; + default_mapper.pat mapper p + | p -> default_mapper.pat mapper p + in + let case mapper ({pc_lhs; pc_guard; pc_rhs} as case) = let before = !variables in let modules_before = !modules in - ignore (mapper.pat mapper pc_lhs) ; - (match pc_guard with Some pc_guard -> ignore (mapper.expr mapper pc_guard) | None -> ()) ; - ignore (mapper.expr mapper pc_rhs) ; - variables := before ; - modules := modules_before ; - case in - let mapper = { default_mapper with - case ; expr ; structure_item ; pat ; typ ; module_expr } in - f (mapper.expr mapper, mapper.structure mapper) arg ; List.flatten (List.rev !res) + ignore (mapper.pat mapper pc_lhs); + ( match pc_guard with + | Some pc_guard -> ignore (mapper.expr mapper pc_guard) + | None -> () ); + ignore (mapper.expr mapper pc_rhs); + variables := before; + modules := modules_before; + case + in + let mapper = + {default_mapper with case; expr; structure_item; pat; typ; module_expr} + in + f (mapper.expr mapper, mapper.structure mapper) arg; + List.flatten (List.rev !res) let ast_location_stripper = let open Ast_mapper in - { default_mapper with location = (fun _ _ -> Location.none) } + {default_mapper with location = (fun _ _ -> Location.none)} let ast_check_expr : Parsetree.expression ast_checker = - ast_check @@ fun (check_expr, _) expr -> + ast_check + @@ fun (check_expr, _) expr -> let expr = ast_location_stripper.Ast_mapper.expr ast_location_stripper expr in ignore @@ check_expr expr let ast_check_structure = - ast_check @@ fun (_, check_structure) structure -> - let structure = ast_location_stripper.Ast_mapper.structure ast_location_stripper structure in + ast_check + @@ fun (_, check_structure) structure -> + let structure = + ast_location_stripper.Ast_mapper.structure ast_location_stripper + structure + in ignore @@ check_structure structure - let forbid_syntax n = let already = ref false in fun _ -> - if !already then [] else begin - already := true ; - Learnocaml_report.[ Message ([ Text "The " ; Code n ; - Text " syntax is forbidden" ], Failure) ] - end - - let require_syntax n = let already = ref false in fun _ -> - if !already then [] else begin - already := true ; - Learnocaml_report.[ Message ([ Text "The " ; Code n ; - Text " syntax has been found, as expected" ], Success 5) ] - end - - let forbid k pr ls = - let t = Hashtbl.create 10 in - List.iter (fun e -> Hashtbl.add t e false) ls ; - fun n -> + let forbid_syntax n = + let already = ref false in + fun _ -> + if !already then [] + else ( + already := true; + Learnocaml_report. + [ Message + ([Text "The "; Code n; Text " syntax is forbidden"], Failure) + ] ) + + let require_syntax n = + let already = ref false in + fun _ -> + if !already then [] + else ( + already := true; + Learnocaml_report. + [ Message + ( [ Text "The " + ; Code n + ; Text " syntax has been found, as expected" ] + , Success 5 ) ] ) + + let forbid k pr ls = + let t = Hashtbl.create 10 in + List.iter (fun e -> Hashtbl.add t e false) ls; + fun n -> match Hashtbl.find t n with | false -> - Hashtbl.add t n true ; - Learnocaml_report.[ Message ([ Text "The " ; Code (pr n) ; Text " " ; Text k ; - Text " is forbidden" ], Failure) ] + Hashtbl.add t n true; + Learnocaml_report. + [ Message + ( [ Text "The " + ; Code (pr n) + ; Text " " + ; Text k + ; Text " is forbidden" ] + , Failure ) ] | true -> [] | exception Not_found -> [] - let restrict k pr ls = - let t = Hashtbl.create 10 in - List.iter (fun e -> Hashtbl.add t e ()) ls ; - fun n -> - try Hashtbl.find t n ; [] with Not_found -> - Hashtbl.add t n () ; - Learnocaml_report.[ Message ([ Text "The " ; Code (pr n) ; Text " " ; Text k ; - Text " is not allowed" ], Failure) ] - - let require k pr _ = - let already = ref false in - fun n -> - if !already then [] else begin - already := true ; - Learnocaml_report.[ Message ([ Text "Found " ; Text k ; Text " " ; Code (pr n) ], Success 5) ] - end - - let print_exp = Format.asprintf "%a" Pprintast.expression - let stripper = ast_location_stripper.Ast_mapper.expr ast_location_stripper - - let restrict_expr name exprs = - restrict name print_exp (List.map stripper exprs) - - let forbid_expr name exprs = - forbid name print_exp (List.map stripper exprs) - - let require_expr name expr = - require name print_exp (stripper expr) - - let ast_sanity_check ?(modules = []) ast cb = - let modules = - (* Some may not even be present, we just want to display a message. *) - [ "Obj" ; "Marshal" ; "Pervasives" ; "Sys" ; - "Test_lib" ; "Introspection" ; "Report" ; - "Js" ; "Toploop" ; "Compiler" ; "Unix" ] @ modules in - let sanity_report = - ast_check_structure - ~on_external:(forbid_syntax "external") - ~on_module_occurence:(forbid "module" (fun name -> name) modules) - ast |> List.sort_uniq compare in - if snd (Learnocaml_report.result sanity_report) then - sanity_report - else - cb () - - let find_binding code_ast name cb = - let open Parsetree in - let open Learnocaml_report in - let rec findlet = function - | [] -> [ Message ([ Text "I could not find " ; Code name ; Text "." ; - Break ; - Text "Check that it is defined as a simple " ; Code "let" ; - Text " at top level." ], Failure) ] - | { pstr_desc = Pstr_value (_, bds); _ } :: rest -> - let rec findvar = function - | [] -> findlet rest - | { pvb_pat = { ppat_desc = Ppat_var { Location.txt; _ }; _ } ; pvb_expr; _ } :: _ when txt = name -> - Message ([ Text "Found a toplevel definition for " ; Code name ; Text "."], Informative) - :: cb pvb_expr - | _ :: rest -> findvar rest in - findvar bds - | _ :: rest -> findlet rest - in findlet (List.rev code_ast) + let restrict k pr ls = + let t = Hashtbl.create 10 in + List.iter (fun e -> Hashtbl.add t e ()) ls; + fun n -> + try Hashtbl.find t n; [] with Not_found -> + Hashtbl.add t n (); + Learnocaml_report. + [ Message + ( [ Text "The " + ; Code (pr n) + ; Text " " + ; Text k + ; Text " is not allowed" ] + , Failure ) ] + + let require k pr _ = + let already = ref false in + fun n -> + if !already then [] + else ( + already := true; + Learnocaml_report. + [ Message + ([Text "Found "; Text k; Text " "; Code (pr n)], Success 5) ] ) + + let print_exp = Format.asprintf "%a" Pprintast.expression + + let stripper = ast_location_stripper.Ast_mapper.expr ast_location_stripper + + let restrict_expr name exprs = + restrict name print_exp (List.map stripper exprs) + + let forbid_expr name exprs = + forbid name print_exp (List.map stripper exprs) + + let require_expr name expr = require name print_exp (stripper expr) + + let ast_sanity_check ?(modules = []) ast cb = + let modules = + (* Some may not even be present, we just want to display a message. *) + [ "Obj" + ; "Marshal" + ; "Pervasives" + ; "Sys" + ; "Test_lib" + ; "Introspection" + ; "Report" + ; "Js" + ; "Toploop" + ; "Compiler" + ; "Unix" ] + @ modules + in + let sanity_report = + ast_check_structure ~on_external:(forbid_syntax "external") + ~on_module_occurence:(forbid "module" (fun name -> name) modules) + ast + |> List.sort_uniq compare + in + if snd (Learnocaml_report.result sanity_report) then sanity_report + else cb () + let find_binding code_ast name cb = + let open Parsetree in + let open Learnocaml_report in + let rec findlet = function + | [] -> + [ Message + ( [ Text "I could not find " + ; Code name + ; Text "." + ; Break + ; Text "Check that it is defined as a simple " + ; Code "let" + ; Text " at top level." ] + , Failure ) ] + | {pstr_desc = Pstr_value (_, bds); _} :: rest -> + let rec findvar = function + | [] -> findlet rest + | { pvb_pat = {ppat_desc = Ppat_var {Location.txt; _}; _} + ; pvb_expr; _ } + :: _ + when txt = name -> + Message + ( [ Text "Found a toplevel definition for " + ; Code name + ; Text "." ] + , Informative ) + :: cb pvb_expr + | _ :: rest -> findvar rest + in + findvar bds + | _ :: rest -> findlet rest + in + findlet (List.rev code_ast) end (*----------------------------------------------------------------------------*) @@ -820,34 +1065,63 @@ module Make let compatible_type ~expected:exp got = let open Learnocaml_report in - [ Message ([ Text "Checking that " ; Code got ; - Text "is compatible with " ; Code exp ], Informative) ; - match Introspection.compatible_type exp ("Code." ^ got) with - | Introspection.Absent -> - Message ([ Text "Type not found" ], Failure) - | Introspection.Incompatible msg -> - Message ([ Text msg ], Failure) + [ Message + ( [ Text "Checking that " + ; Code got + ; Text "is compatible with " + ; Code exp ] + , Informative ) + ; ( match Introspection.compatible_type exp ("Code." ^ got) with + | Introspection.Absent -> Message ([Text "Type not found"], Failure) + | Introspection.Incompatible msg -> Message ([Text msg], Failure) | Introspection.Present () -> - Message ([ Text "Type found and compatible" ], Success 5) ] + Message ([Text "Type found and compatible"], Success 5) ) ] let existing_type ?(score = 1) name = let open Learnocaml_report in - try let path = Env.lookup_type Longident.(parse ("Code." ^ name)) !Toploop.toplevel_env in - let _ = Env.find_type path !Toploop.toplevel_env in - true, [ Message ( [ Text "Type" ; Code name ; Text "found" ], Success score ) ] - with Not_found -> false, [ Message ( [ Text "type" ; Code name ; Text "not found" ], Failure ) ] + try + let path = + Env.lookup_type + Longident.(parse ("Code." ^ name)) + !Toploop.toplevel_env + in + let _ = Env.find_type path !Toploop.toplevel_env in + ( true + , [Message ([Text "Type"; Code name; Text "found"], Success score)] ) + with Not_found -> + (false, [Message ([Text "type"; Code name; Text "not found"], Failure)]) let abstract_type ?(allow_private = true) ?(score = 5) name = let open Learnocaml_report in - try let path = Env.lookup_type Longident.(parse ("Code." ^ name)) !Toploop.toplevel_env in - match Env.find_type path !Toploop.toplevel_env with - | { Types. type_kind = Types.Type_abstract ; Types. type_manifest = None; _ } -> - true, [ Message ([Text "Type" ; Code name ; Text "is abstract as expected." ], Success score) ] - | { Types. type_kind = _ ; type_private = Asttypes.Private; _ } when allow_private -> - true, [ Message ([Text "Type" ; Code name ; Text "is private, I'll accept that :-)." ], Success score) ] - | { Types. type_kind = _; _ } -> - false, [ Message ([Text "Type" ; Code name ; Text "should be abstract!" ], Failure) ] - with Not_found -> false, [ Message ( [Text "Type" ; Code name ; Text "not found." ], Failure) ] + try + let path = + Env.lookup_type + Longident.(parse ("Code." ^ name)) + !Toploop.toplevel_env + in + match Env.find_type path !Toploop.toplevel_env with + | {Types.type_kind = Types.Type_abstract; Types.type_manifest = None; _} + -> + ( true + , [ Message + ( [Text "Type"; Code name; Text "is abstract as expected."] + , Success score ) ] ) + | {Types.type_kind = _; type_private = Asttypes.Private; _} + when allow_private -> + ( true + , [ Message + ( [ Text "Type" + ; Code name + ; Text "is private, I'll accept that :-)." ] + , Success score ) ] ) + | {Types.type_kind = _; _} -> + ( false + , [ Message + ( [Text "Type"; Code name; Text "should be abstract!"] + , Failure ) ] ) + with Not_found -> + ( false + , [Message ([Text "Type"; Code name; Text "not found."], Failure)] ) let test_student_code ty cb = let open Learnocaml_report in @@ -855,262 +1129,296 @@ module Make | Introspection.Present v -> cb v | Introspection.Absent -> assert false | Introspection.Incompatible msg -> - [ Message ([ Text "Your code doesn't match the expected signature." ; Break ; - Code msg (* TODO: hide or fix locations *) ], Failure) ] + [ Message + ( [ Text "Your code doesn't match the expected signature." + ; Break + ; Code msg + (* TODO: hide or fix locations *) ] + , Failure ) ] let test_module_property ty name cb = let open Learnocaml_report in match Introspection.get_value ("Code." ^ name) ty with | Introspection.Present v -> cb v | Introspection.Absent -> - [ Message ([ Text "Module" ; Code name ; Text "not found." ], Failure) ] + [Message ([Text "Module"; Code name; Text "not found."], Failure)] | Introspection.Incompatible msg -> - [ Message ([ Text "Module" ; Code name ; Text "doesn't match the expected signature." ; - Break ; Code msg (* TODO: hide or fix locations *) ], Failure) ] - + [ Message + ( [ Text "Module" + ; Code name + ; Text "doesn't match the expected signature." + ; Break + ; Code msg + (* TODO: hide or fix locations *) ] + , Failure ) ] end (*----------------------------------------------------------------------------*) - type 'a tester = - 'a Ty.ty -> 'a result -> 'a result -> Learnocaml_report.t + type 'a tester = 'a Ty.ty -> 'a result -> 'a result -> Learnocaml_report.t - type io_tester = - string -> string -> Learnocaml_report.t + type io_tester = string -> string -> Learnocaml_report.t - type io_postcond = - string -> Learnocaml_report.t + type io_postcond = string -> Learnocaml_report.t - let typed_printer ty ppf v = - Introspection.print_value ppf v ty + let typed_printer ty ppf v = Introspection.print_value ppf v ty exception Timeout of int (*----------------------------------------------------------------------------*) module Tester = struct - let print_with ty = Format.asprintf "%a" (typed_printer ty) + let exn_to_string = print_with [%ty: exn] let test_generic eq canon ty va vb = - let to_string = print_with ty in - if eq (canon va) (canon vb) then - let txt_msg, code_msg = - match va with - | Ok v -> "Correct value" , to_string v - | Error exn -> "Correct exception" , exn_to_string exn - in Learnocaml_report.[ Message ([Text txt_msg; Code code_msg], Success 1) ] - else - let txt_msgs = - match va with - | Ok v -> - Learnocaml_report.[Text "Wrong value" ; Code (to_string v)] - | Error (Failure s) when s = "EXCESS" -> - Learnocaml_report.[Text "Your code exceeded the output buffer size limit."] - | Error Stack_overflow -> - Learnocaml_report.[Text "Stack overflow. Too many recursions?"] - | Error (Timeout limit) -> - Learnocaml_report.[Text (Format.sprintf "Your code exceeded the time limit of %d seconds." limit)] - | Error exn -> - Learnocaml_report.[Text "Wrong exception" ; Code (exn_to_string exn)] - in Learnocaml_report.[ Message (txt_msgs, Failure) ] - - let test_ignore ty va vb = - match va, vb with - | Ok _, Ok _ -> [] - | Ok v, Error _ -> - Learnocaml_report.[ Message ([ Text "Unexpected result" ; - Code (print_with ty v) ; - Text "instead of exception" ], Failure) ] - | Error (Failure s), _ when s = "EXCESS" -> - Learnocaml_report.[ Message ([ Text "Your code exceeded the output buffer size limit." ], Failure) ] - | Error Stack_overflow, _ -> - Learnocaml_report.[ Message ([ Text "Stack overflow. Too many recursions?" ], Failure) ] - | Error _, Error _ -> [] - | Error exn, Ok _ -> - Learnocaml_report.[ Message ([ Text "Unexpected exception" ; Code (exn_to_string exn) ], Failure) ] - - let test ty va vb = - test_generic (=) (fun x -> x) ty va vb - let test_eq eq = - test_generic eq (fun x -> x) - let test_eq_ok eq = - let eq ra rb = match ra, rb with - | Ok a, Ok b -> eq a b - | _ -> ra = rb in - test_generic eq (fun x -> x) - let test_eq_exn eq = - let eq ra rb = match ra, rb with - | Error a, Error b -> eq a b - | _ -> ra = rb in - test_generic eq (fun x -> x) - let test_canon canon = - test_generic (=) canon - let test_canon_ok canon = - let canon = function | Ok v -> Ok (canon v) | err -> err in - test_generic (=) canon - let test_canon_error canon = - let canon = function | Error v -> Error (canon v) | ok -> ok in - test_generic (=) canon - let test_translate conv test wit _ got exp = - let conv = function Error exn -> Error exn | Ok v -> Ok (conv v) in - test wit (conv got) (conv exp) + let to_string = print_with ty in + if eq (canon va) (canon vb) then + let txt_msg, code_msg = + match va with + | Ok v -> ("Correct value", to_string v) + | Error exn -> ("Correct exception", exn_to_string exn) + in + Learnocaml_report.[Message ([Text txt_msg; Code code_msg], Success 1)] + else + let txt_msgs = + match va with + | Ok v -> Learnocaml_report.[Text "Wrong value"; Code (to_string v)] + | Error (Failure s) when s = "EXCESS" -> + Learnocaml_report. + [Text "Your code exceeded the output buffer size limit."] + | Error Stack_overflow -> + Learnocaml_report.[Text "Stack overflow. Too many recursions?"] + | Error (Timeout limit) -> + Learnocaml_report. + [ Text + (Format.sprintf + "Your code exceeded the time limit of %d seconds." limit) + ] + | Error exn -> + Learnocaml_report. + [Text "Wrong exception"; Code (exn_to_string exn)] + in + Learnocaml_report.[Message (txt_msgs, Failure)] + + let test_ignore ty va vb = + match (va, vb) with + | Ok _, Ok _ -> [] + | Ok v, Error _ -> + Learnocaml_report. + [ Message + ( [ Text "Unexpected result" + ; Code (print_with ty v) + ; Text "instead of exception" ] + , Failure ) ] + | Error (Failure s), _ when s = "EXCESS" -> + Learnocaml_report. + [ Message + ( [Text "Your code exceeded the output buffer size limit."] + , Failure ) ] + | Error Stack_overflow, _ -> + Learnocaml_report. + [Message ([Text "Stack overflow. Too many recursions?"], Failure)] + | Error _, Error _ -> [] + | Error exn, Ok _ -> + Learnocaml_report. + [ Message + ( [Text "Unexpected exception"; Code (exn_to_string exn)] + , Failure ) ] + + let test ty va vb = test_generic ( = ) (fun x -> x) ty va vb + + let test_eq eq = test_generic eq (fun x -> x) + + let test_eq_ok eq = + let eq ra rb = + match (ra, rb) with Ok a, Ok b -> eq a b | _ -> ra = rb + in + test_generic eq (fun x -> x) - (*----------------------------------------------------------------------------*) + let test_eq_exn eq = + let eq ra rb = + match (ra, rb) with Error a, Error b -> eq a b | _ -> ra = rb + in + test_generic eq (fun x -> x) + + let test_canon canon = test_generic ( = ) canon - let io_test_ignore _ _ = [] - - let splitter chars = - if chars = [] then - fun s -> [ s ] - else - let pattern = Array.make 256 false in - List.iter (fun c -> pattern.(Char.code c) <- true) chars ; - fun s -> - let len = String.length s in - let rec loop acc i j = - if j >= len then - List.rev (if i >= len then acc else String.sub s i (j - i) :: acc) - else if pattern.(Char.code (String.get s j)) then - loop (String.sub s i (j - i) :: acc) (j + 1) (j + 1) - else - loop acc i (j + 1) in - loop [] 0 0 - - let trimmer chars = - if chars = [] then - fun s -> s - else - let pattern = Array.make 256 false in - List.iter (fun c -> pattern.(Char.code c) <- true) chars ; - fun s -> - let len = String.length s in - let rec start i = - if i >= len then - stop i (len - 1) - else if pattern.(Char.code (String.get s i)) then - start (i + 1) - else - stop i (len - 1) - and stop i j = - if j < 0 then - (i, 0) - else if pattern.(Char.code (String.get s j)) then - stop i (j - 1) - else - (i, j + 1) in - let i, j = start 0 in - if j <= i then "" else String.sub s i (j - i) - - let dropper chars = - if chars = [] then - fun s -> s - else - let pattern = Array.make 256 false in - List.iter (fun c -> pattern.(Char.code c) <- true) chars ; - fun s -> - let len = String.length s in - let buf = Bytes.create len in - let rec drop i m = - if i = len then m else - let c = String.get s i in - if pattern.(Char.code c) then - drop (i + 1) m - else begin - Bytes.set buf m c ; - drop (i + 1) (m + 1) - end in - Bytes.sub_string buf 0 (drop 0 0) - - let io_test_equals ?(trim = []) ?(drop = []) = - let open Learnocaml_report in - let trim = trimmer trim in - let drop = dropper drop in - let tr s = trim (drop s) in - fun got expected -> - if tr got = tr expected then - [ Message ([ Text "Expected output" ; Output got ], Success 5) ] + let test_canon_ok canon = + let canon = function Ok v -> Ok (canon v) | err -> err in + test_generic ( = ) canon + + let test_canon_error canon = + let canon = function Error v -> Error (canon v) | ok -> ok in + test_generic ( = ) canon + + let test_translate conv test wit _ got exp = + let conv = function Error exn -> Error exn | Ok v -> Ok (conv v) in + test wit (conv got) (conv exp) + + (*----------------------------------------------------------------------------*) + + let io_test_ignore _ _ = [] + + let splitter chars = + if chars = [] then fun s -> [s] else - [ Message ([ Text "Unexpected output" ; Output got ], Failure) ] - - let io_test_items - ?(split = []) ?(trim = []) ?(drop = []) - ?(skip_empty = false) ?(test_item = io_test_equals ~trim:[] ~drop:[]) = - let open Learnocaml_report in - let split = splitter split in - let trim = trimmer trim in - let drop = dropper drop in - let tr s = trim (drop s) in - fun sgot sexpected -> - let got = List.map tr (split sgot) in - let expected = List.map tr (split sexpected) in - let got = if skip_empty then List.filter ((<>) "") got else got in - let expected = if skip_empty then List.filter ((<>) "") expected else expected in - let rec test_items = function - | [], [] -> - [ Message ([ Text "Expected output" ; Output sgot ], Success 5) ] - | got :: gots, expected :: expecteds -> - if snd (result (test_item got expected)) then - [ Message ([ Text "Unexpected output" ; Output sgot ], Failure) ] + let pattern = Array.make 256 false in + List.iter (fun c -> pattern.(Char.code c) <- true) chars; + fun s -> + let len = String.length s in + let rec loop acc i j = + if j >= len then + List.rev (if i >= len then acc else String.sub s i (j - i) :: acc) + else if pattern.(Char.code s.[j]) then + loop (String.sub s i (j - i) :: acc) (j + 1) (j + 1) + else loop acc i (j + 1) + in + loop [] 0 0 + + let trimmer chars = + if chars = [] then fun s -> s + else + let pattern = Array.make 256 false in + List.iter (fun c -> pattern.(Char.code c) <- true) chars; + fun s -> + let len = String.length s in + let rec start i = + if i >= len then stop i (len - 1) + else if pattern.(Char.code s.[i]) then start (i + 1) + else stop i (len - 1) + and stop i j = + if j < 0 then (i, 0) + else if pattern.(Char.code s.[j]) then stop i (j - 1) + else (i, j + 1) + in + let i, j = start 0 in + if j <= i then "" else String.sub s i (j - i) + + let dropper chars = + if chars = [] then fun s -> s + else + let pattern = Array.make 256 false in + List.iter (fun c -> pattern.(Char.code c) <- true) chars; + fun s -> + let len = String.length s in + let buf = Bytes.create len in + let rec drop i m = + if i = len then m else - test_items (gots, expecteds) - | _, _ -> - [ Message ([ Text "Unexpected output" ; Output sgot ], Failure) ] - in test_items (got, expected) - - let io_test_lines - ?(trim = []) ?(drop = []) - ?(skip_empty = false) ?(test_line = io_test_equals ~trim:[] ~drop:[]) got expected = - io_test_items ~split: [ '\n' ] ~trim ~drop ~skip_empty ~test_item: test_line got expected + let c = s.[i] in + if pattern.(Char.code c) then drop (i + 1) m + else ( + Bytes.set buf m c; + drop (i + 1) (m + 1) ) + in + Bytes.sub_string buf 0 (drop 0 0) + + let io_test_equals ?(trim = []) ?(drop = []) = + let open Learnocaml_report in + let trim = trimmer trim in + let drop = dropper drop in + let tr s = trim (drop s) in + fun got expected -> + if tr got = tr expected then + [Message ([Text "Expected output"; Output got], Success 5)] + else [Message ([Text "Unexpected output"; Output got], Failure)] + + let io_test_items ?(split = []) ?(trim = []) ?(drop = []) + ?(skip_empty = false) ?(test_item = io_test_equals ~trim:[] ~drop:[]) = + let open Learnocaml_report in + let split = splitter split in + let trim = trimmer trim in + let drop = dropper drop in + let tr s = trim (drop s) in + fun sgot sexpected -> + let got = List.map tr (split sgot) in + let expected = List.map tr (split sexpected) in + let got = if skip_empty then List.filter (( <> ) "") got else got in + let expected = + if skip_empty then List.filter (( <> ) "") expected else expected + in + let rec test_items = function + | [], [] -> + [Message ([Text "Expected output"; Output sgot], Success 5)] + | got :: gots, expected :: expecteds -> + if snd (result (test_item got expected)) then + [Message ([Text "Unexpected output"; Output sgot], Failure)] + else test_items (gots, expecteds) + | _, _ -> [Message ([Text "Unexpected output"; Output sgot], Failure)] + in + test_items (got, expected) + + let io_test_lines ?(trim = []) ?(drop = []) ?(skip_empty = false) + ?(test_line = io_test_equals ~trim:[] ~drop:[]) got expected = + io_test_items ~split:['\n'] ~trim ~drop ~skip_empty ~test_item:test_line + got expected end module Mutation = struct open Tester type 'arg arg_mutation_test_callbacks = - { before_reference : 'arg -> unit ; - before_user : 'arg -> unit ; - test : 'ret. ?test_result: 'ret tester -> 'ret tester } + { before_reference : 'arg -> unit + ; before_user : 'arg -> unit + ; test : 'ret. ?test_result:'ret tester -> 'ret tester } - let arg_mutation_test_callbacks ?(test = test) ~dup ~blit ty = + let arg_mutation_test_callbacks ?(test = test) ~dup ~blit ty = let sam = ref None in let got = ref None in let exp = ref None in - let before_reference a = - sam := Some (dup a) in + let before_reference a = sam := Some (dup a) in let before_user a = - exp := Some (dup a) ; - got := Some a ; - match !sam with None -> () | Some src -> blit src a in + exp := Some (dup a); + got := Some a; + match !sam with None -> () | Some src -> blit src a + in let test ?(test_result = test_ignore) ret_ty va vb = let result_report = test_result ret_ty va vb in - let report = match va, vb with + let report = + match (va, vb) with | Ok _, Ok _ -> - let got = match !got with Some g -> g | None -> invalid_arg "arg_mutation_test_callbacks" in - let exp = match !exp with Some e -> e | None -> invalid_arg "arg_mutation_test_callbacks" in - test ty (Ok got) (Ok exp) + let got = + match !got with + | Some g -> g + | None -> invalid_arg "arg_mutation_test_callbacks" + in + let exp = + match !exp with + | Some e -> e + | None -> invalid_arg "arg_mutation_test_callbacks" + in + test ty (Ok got) (Ok exp) | Error ea, Ok _ -> - let exp = match !exp with Some e -> e | None -> invalid_arg "arg_mutation_test_callbacks" in - test ty (Error ea) (Ok exp) + let exp = + match !exp with + | Some e -> e + | None -> invalid_arg "arg_mutation_test_callbacks" + in + test ty (Error ea) (Ok exp) | Ok _, Error eb -> - let got = match !got with Some g -> g | None -> invalid_arg "arg_mutation_test_callbacks" in - test ty (Ok got) (Error eb) - | Error ea, Error eb -> - test ty (Error ea) (Error eb) in - result_report @ report in - { before_reference ; before_user ; test } - - let array_arg_mutation_test_callbacks ?(test = test) ty = - let blit src dst = Array.blit src 0 dst 0 (Array.length dst) in - let dup = Array.copy in - arg_mutation_test_callbacks ~test ~blit ~dup ty - - let ref_arg_mutation_test_callbacks ?(test = test) ty = - let blit src dst = dst := !src in - let dup r = ref !r in - arg_mutation_test_callbacks ~test ~blit ~dup ty + let got = + match !got with + | Some g -> g + | None -> invalid_arg "arg_mutation_test_callbacks" + in + test ty (Ok got) (Error eb) + | Error ea, Error eb -> test ty (Error ea) (Error eb) + in + result_report @ report + in + {before_reference; before_user; test} + let array_arg_mutation_test_callbacks ?(test = test) ty = + let blit src dst = Array.blit src 0 dst 0 (Array.length dst) in + let dup = Array.copy in + arg_mutation_test_callbacks ~test ~blit ~dup ty + + let ref_arg_mutation_test_callbacks ?(test = test) ty = + let blit src dst = dst := !src in + let dup r = ref !r in + arg_mutation_test_callbacks ~test ~blit ~dup ty end (*----------------------------------------------------------------------------*) @@ -1119,151 +1427,175 @@ module Make open Params open Tester - let sigalrm_handler time = - Sys.Signal_handle (fun _ -> raise (Timeout time)) + let sigalrm_handler time = Sys.Signal_handle (fun _ -> raise (Timeout time)) let run_timeout ~time v = let old_behavior = Sys.signal Sys.sigalrm (sigalrm_handler time) in - let reset_sigalrm () = Sys.set_signal Sys.sigalrm old_behavior - in ignore (Unix.alarm time); - try - let res = v () in - reset_sigalrm (); res - with exc -> - reset_sigalrm (); raise exc + let reset_sigalrm () = Sys.set_signal Sys.sigalrm old_behavior in + ignore (Unix.alarm time); + try + let res = v () in + reset_sigalrm (); res + with exc -> reset_sigalrm (); raise exc let run_timeout ?time v = - match time, Params.timeout with - | Some time, _ | None, Some time -> - run_timeout ~time v - | None, None -> - v() + match (time, Params.timeout) with + | Some time, _ | None, Some time -> run_timeout ~time v + | None, None -> v () let exec v = - Introspection.grab_stdout () ; - Introspection.grab_stderr () ; + Introspection.grab_stdout (); + Introspection.grab_stderr (); try let res = run_timeout v in let out = Introspection.release_stdout () in let err = Introspection.release_stderr () in Ok (res, out, err) with exn -> - ignore (Introspection.release_stdout ()) ; - ignore (Introspection.release_stderr ()) ; + ignore (Introspection.release_stdout ()); + ignore (Introspection.release_stderr ()); Error exn - let result v = match exec v with - | Ok (v, _, _) -> Ok v - | Error exn -> Error exn - + let result v = + match exec v with Ok (v, _, _) -> Ok v | Error exn -> Error exn (*----------------------------------------------------------------------------*) - let verify - ?(test_stdout = fun _ -> []) ?(test_stderr = fun _ -> []) - ?(pre = (fun _ -> ())) ?(post = (fun _ -> [])) test ty v = + let verify ?(test_stdout = fun _ -> []) ?(test_stderr = fun _ -> []) + ?(pre = fun _ -> ()) ?(post = fun _ -> []) test ty v = let v = pre (); exec v in match v with | Ok (v, out, err) -> - let post_report = post (v, out, err) in - let report = test ty (Ok v) in - let stdout_report = test_stdout out in - let stderr_report = test_stderr err in - report @ stdout_report @ stderr_report @ post_report + let post_report = post (v, out, err) in + let report = test ty (Ok v) in + let stdout_report = test_stdout out in + let stderr_report = test_stderr err in + report @ stdout_report @ stderr_report @ post_report | Error exn -> test ty (Error exn) - let expect - ?(test = test) ?(test_stdout = io_test_ignore) ?(test_stderr = io_test_ignore) - ?(pre = (fun _ -> ())) ?(post = (fun _ _ -> [])) ty va vb = + let expect ?(test = test) ?(test_stdout = io_test_ignore) + ?(test_stderr = io_test_ignore) ?(pre = fun _ -> ()) + ?(post = fun _ _ -> []) ty va vb = let vb = exec vb in - let va = pre () ; exec va in - match va, vb with + let va = pre (); exec va in + match (va, vb) with | Ok (va, outa, erra), Ok (vb, outb, errb) -> - let post_report = post (va, outa, erra) (vb, outb, errb) in - let report = test ty (Ok va) (Ok vb) in - let stdout_report = test_stdout outa outb in - let stderr_report = test_stderr erra errb in - report @ stdout_report @ stderr_report @ post_report - | Ok (va, _, _), Error exnb -> - test ty (Ok va) (Error exnb) - | Error exna, Ok (vb, _, _) -> - test ty (Error exna) (Ok vb) - | Error exna, Error exnb -> - test ty (Error exna) (Error exnb) + let post_report = post (va, outa, erra) (vb, outb, errb) in + let report = test ty (Ok va) (Ok vb) in + let stdout_report = test_stdout outa outb in + let stderr_report = test_stderr erra errb in + report @ stdout_report @ stderr_report @ post_report + | Ok (va, _, _), Error exnb -> test ty (Ok va) (Error exnb) + | Error exna, Ok (vb, _, _) -> test ty (Error exna) (Ok vb) + | Error exna, Error exnb -> test ty (Error exna) (Error exnb) (*----------------------------------------------------------------------------*) include Fun_ty (* The GADT [args] & [last, arg] are defined in [fun_ty.ml] *) - + (* The GADT [fun_ty] & [last_ty, arg_ty, ty_of_fun_ty, apply, get_ret_ty, print, get_sampler] are defined in [fun_ty.ml] *) - let ty_of_prot = ty_of_fun_ty - [@@ocaml.deprecated "Use ty_of_fun_ty instead."] + let[@ocaml.deprecated "Use ty_of_fun_ty instead."] ty_of_prot = + ty_of_fun_ty module Aux = struct let typed_printer = typed_printer + let typed_sampler = Introspection.get_sampler end - module FunTyAux = Make(Aux) + + module FunTyAux = Make (Aux) (*----------------------------------------------------------------------------*) - type 'a lookup = unit -> [ `Found of string * Learnocaml_report.t * 'a | `Unbound of string * Learnocaml_report.t ] + type 'a lookup = + unit + -> [ `Found of string * Learnocaml_report.t * 'a + | `Unbound of string * Learnocaml_report.t ] let lookup ty ?display_name name = - let display_name = match display_name with None -> name | Some name -> name in + let display_name = + match display_name with None -> name | Some name -> name + in let open Learnocaml_report in - let res = match Introspection.get_value name ty with + let res = + match Introspection.get_value name ty with | Introspection.Present v -> - let msg = - [ Message ([ Text "Found" ; Code display_name ; - Text "with compatible type." ], Informative) ] in - `Found (display_name, msg, v) + let msg = + [ Message + ( [ Text "Found" + ; Code display_name + ; Text "with compatible type." ] + , Informative ) ] + in + `Found (display_name, msg, v) | Introspection.Absent -> - `Unbound - (name, [ Message ([ Text "Cannot find " ; Code display_name ], Failure) ]) + `Unbound + ( name + , [Message ([Text "Cannot find "; Code display_name], Failure)] + ) | Introspection.Incompatible msg -> - `Unbound - (name, [ Message ([ Text "Found" ; Code display_name ; - Text "with unexpected type:" ; Break ; - Code msg ], Failure) ]) in + `Unbound + ( name + , [ Message + ( [ Text "Found" + ; Code display_name + ; Text "with unexpected type:" + ; Break + ; Code msg ] + , Failure ) ] ) + in fun () -> res let lookup_student ty name = let open Learnocaml_report in - let res = match Introspection.get_value ("Code." ^ name) ty with + let res = + match Introspection.get_value ("Code." ^ name) ty with | Introspection.Present v -> - let msg = - [ Message ([ Text "Found" ; Code name ; - Text "with compatible type." ], Informative) ] in - `Found (name, msg, v) + let msg = + [ Message + ( [Text "Found"; Code name; Text "with compatible type."] + , Informative ) ] + in + `Found (name, msg, v) | Introspection.Absent -> - `Unbound - (name, [ Message ([ Text "Cannot find " ; Code name ], Failure) ]) + `Unbound + (name, [Message ([Text "Cannot find "; Code name], Failure)]) | Introspection.Incompatible msg -> - `Unbound - (name, [ Message ([ Text "Found" ; Code name ; - Text "with unexpected type:" ; Break ; - Code msg ], Failure) ]) in + `Unbound + ( name + , [ Message + ( [ Text "Found" + ; Code name + ; Text "with unexpected type:" + ; Break + ; Code msg ] + , Failure ) ] ) + in fun () -> res let lookup_solution ty name = let open Learnocaml_report in - let res = match Introspection.get_value ("Solution." ^ name) ty with - | Introspection.Present v -> - `Found (name, [], v) + let res = + match Introspection.get_value ("Solution." ^ name) ty with + | Introspection.Present v -> `Found (name, [], v) | Introspection.Absent -> - `Unbound - (name, [ Message ([ Text "Looking for " ; Code name ], Informative) ; - Message ([ Text "Solution not found!" ], Failure) ]) + `Unbound + ( name + , [ Message ([Text "Looking for "; Code name], Informative) + ; Message ([Text "Solution not found!"], Failure) ] ) | Introspection.Incompatible msg -> - `Unbound - (name, [ Message ([ Text "Looking for " ; Code name ], Informative) ; - Message ([ Text "Solution is wrong!" ; Break ; Code msg ], Failure) ]) in + `Unbound + ( name + , [ Message ([Text "Looking for "; Code name], Informative) + ; Message + ([Text "Solution is wrong!"; Break; Code msg], Failure) ] + ) + in fun () -> res let name f = match f () with `Unbound (n, _) | `Found (n, _, _) -> n @@ -1277,105 +1609,97 @@ module Make (*----------------------------------------------------------------------------*) - let run_test - ?(before = (fun _ -> ())) ~after name prot tests for_case = + let run_test ?(before = fun _ -> ()) ~after name prot tests for_case = let before args () = before args in let ty = Fun_ty.ty_of_fun_ty prot in let for_casel case = let args, ret = case () in - let code = Format.asprintf "@[%s%a@]" name (FunTyAux.print prot) args in + let code = + Format.asprintf "@[%s%a@]" name (FunTyAux.print prot) args + in let ret_ty = Fun_ty.get_ret_ty ty args in - Learnocaml_report.(Message ([ Text "Computing" ; Code code ], Informative)) :: - for_case (before args) (after args) args ret_ty ret - in List.flatten @@ List.map for_casel tests - - let test_function_generic - ?test ?test_stdout ?test_stderr - ?before ?(after = (fun _ _ _ -> [])) - prot uf tests = - test_value uf @@ fun ruf -> + Learnocaml_report.( + Message ([Text "Computing"; Code code], Informative)) + :: for_case (before args) (after args) args ret_ty ret + in + List.flatten @@ List.map for_casel tests + + let test_function_generic ?test ?test_stdout ?test_stderr ?before + ?(after = fun _ _ _ -> []) prot uf tests = + test_value uf + @@ fun ruf -> let for_case pre post args ret_ty = - expect ~pre ~post - ?test ?test_stdout ?test_stderr - ret_ty (fun () -> Fun_ty.apply ruf args) - in run_test ?before ~after (name uf) prot tests for_case - - let test_function_generic_postcond - ?test_stdout ?test_stderr - ?before ?(after = (fun _ _ -> [])) - test name prot tests = + expect ~pre ~post ?test ?test_stdout ?test_stderr ret_ty (fun () -> + Fun_ty.apply ruf args ) + in + run_test ?before ~after (name uf) prot tests for_case + + let test_function_generic_postcond ?test_stdout ?test_stderr ?before + ?(after = fun _ _ -> []) test name prot tests = let for_case pre post args = - verify ~pre ~post - ?test_stdout ?test_stderr - (test args) - in run_test ?before ~after name prot tests for_case - - let test_function - ?test ?test_stdout ?test_stderr - ?before ?after - prot uf tests = - test_function_generic - ?test ?test_stdout ?test_stderr - ?before ?after - prot uf + verify ~pre ~post ?test_stdout ?test_stderr (test args) + in + run_test ?before ~after name prot tests for_case + + let test_function ?test ?test_stdout ?test_stderr ?before ?after prot uf + tests = + test_function_generic ?test ?test_stdout ?test_stderr ?before ?after prot + uf (List.map (fun x () -> x) tests) - let make_tests ?gen ?sampler ?(before_reference = fun _ -> ()) prot rf tests = - let gen = match gen with - | Some n -> n - | None -> max 5 (10 - List.length tests) in - let tests = match gen with + let make_tests ?gen ?sampler ?(before_reference = fun _ -> ()) prot rf + tests = + let gen = + match gen with Some n -> n | None -> max 5 (10 - List.length tests) + in + let tests = + match gen with | 0 -> List.map (fun x () -> x) tests | _ -> - let sampler = - match sampler with - | None -> FunTyAux.get_sampler prot - | Some sampler -> sampler in - let rec make i = - if i <= 0 then [] else sampler :: make (i - 1) in - List.map (fun x () -> x) tests @ make gen in + let sampler = + match sampler with + | None -> FunTyAux.get_sampler prot + | Some sampler -> sampler + in + let rec make i = if i <= 0 then [] else sampler :: make (i - 1) in + List.map (fun x () -> x) tests @ make gen + in List.map - (fun a () -> let a = a () in (a, (fun () -> before_reference a ; Fun_ty.apply rf a))) + (fun a () -> + let a = a () in + (a, fun () -> before_reference a; Fun_ty.apply rf a) ) tests - let test_function_against_generic ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler - prot uf rf tests = - test_value rf @@ fun rf -> + let test_function_against_generic ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler prot uf rf tests = + test_value rf + @@ fun rf -> let tests = make_tests ?gen ?sampler ?before_reference prot rf tests in - test_function_generic - ?test ?test_stdout ?test_stderr - ?before:before_user ?after prot uf tests - - let test_function_against_generic_postcond ?gen - ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler - test name prot rf tests = - test_value rf @@ fun rf -> + test_function_generic ?test ?test_stdout ?test_stderr ?before:before_user + ?after prot uf tests + + let test_function_against_generic_postcond ?gen ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler test name prot rf tests + = + test_value rf + @@ fun rf -> let tests = make_tests ?gen ?sampler ?before_reference prot rf tests in - test_function_generic_postcond - ?test_stdout ?test_stderr + test_function_generic_postcond ?test_stdout ?test_stderr ?before:before_user ?after test name prot tests - let test_function_against ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler prot uf rf tests = - test_function_against_generic ?gen - ?test ?test_stdout ?test_stderr + let test_function_against ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler prot uf rf tests = + test_function_against_generic ?gen ?test ?test_stdout ?test_stderr ?before_reference ?before_user ?after ?sampler prot uf rf tests - let test_function_against_solution ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler prot name tests = + let test_function_against_solution ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler prot name tests = let ty = Fun_ty.ty_of_fun_ty prot in - test_function_against_generic ?gen - ?test ?test_stdout ?test_stderr + test_function_against_generic ?gen ?test ?test_stdout ?test_stderr ?before_reference ?before_user ?after ?sampler prot (lookup_student ty name) (lookup_solution ty name) tests - let (==>) params ret = (params, fun () -> ret) - + let ( ==> ) params ret = (params, fun () -> ret) end (*----------------------------------------------------------------------------*) @@ -1386,24 +1710,20 @@ module Make let test_ref ty got exp = let open Learnocaml_report in - let mk_txt str = - [ Text str; Code (print_with ty !got)] in - if !got = exp then - [ Message (mk_txt "Correct value", Success 1) ] - else - [ Message (mk_txt "Wrong value" , Failure) ] + let mk_txt str = [Text str; Code (print_with ty !got)] in + if !got = exp then [Message (mk_txt "Correct value", Success 1)] + else [Message (mk_txt "Wrong value", Failure)] let test_variable ty name r = - test_value (lookup_student ty name) @@ fun v -> - expect ~test ty (fun () -> v) (fun () -> r) + test_value (lookup_student ty name) + @@ fun v -> expect ~test ty (fun () -> v) (fun () -> r) let test_variable_property ty name cb = test_value (lookup_student ty name) cb let test_variable_against_solution ty name = - test_value (lookup_solution ty name) @@ fun sol -> - test_variable ty name sol - + test_value (lookup_solution ty name) + @@ fun sol -> test_variable ty name sol end (*----------------------------------------------------------------------------*) @@ -1413,297 +1733,338 @@ module Make let function_1_adapter_pre sampler ty = let pre = function - | None -> (fun _ -> ()) - | Some pre -> Fun_ty.apply_args_1 pre in - let sampler = match sampler with + | None -> fun _ -> () + | Some pre -> Fun_ty.apply_args_1 pre + in + let sampler = + match sampler with | None -> None - | Some sampler -> Some (fun () -> Fun_ty.last (sampler ())) in + | Some sampler -> Some (fun () -> Fun_ty.last (sampler ())) + in let arg_ty, ret_ty = Ty.domains ty in let prot = Fun_ty.last_ty arg_ty @@ ret_ty in - pre, sampler, prot + (pre, sampler, prot) let function_1_adapter after sampler ty = - let after = match after with + let after = + match after with | None -> fun _ _ _ -> [] | Some after -> Fun_ty.apply_args_1 after in let pre, sampler, prot = function_1_adapter_pre sampler ty in - after, pre, sampler, prot - - let test_function_1 - ?test ?test_stdout ?test_stderr - ?before ?after ty name tests = - let tests = List.map (fun (x, r, out, err) -> - (Fun_ty.last x, (fun () -> output_string stdout out ; output_string stderr err ; r))) - tests in + (after, pre, sampler, prot) + + let test_function_1 ?test ?test_stdout ?test_stderr ?before ?after ty name + tests = + let tests = + List.map + (fun (x, r, out, err) -> + ( Fun_ty.last x + , fun () -> output_string stdout out; output_string stderr err; r + ) ) + tests + in let after, pre, _, prot = function_1_adapter after None ty in - test_function - ?test ?test_stdout ?test_stderr - ~before:(pre before) - ~after prot (lookup_student ty name) tests - - let test_function_1_against ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler ty name rf tests = + test_function ?test ?test_stdout ?test_stderr ~before:(pre before) ~after + prot (lookup_student ty name) tests + + let test_function_1_against ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler ty name rf tests = let tests = List.map (fun x -> Fun_ty.last x) tests in let after, pre, sampler, prot = function_1_adapter after sampler ty in - test_function_against ?gen - ?test ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) + test_function_against ?gen ?test ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) ~after ?sampler prot (lookup_student ty name) (found name rf) tests - let test_function_1_against_solution ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler ty name tests = + let test_function_1_against_solution ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler ty name tests = let tests = List.map (fun x -> Fun_ty.last x) tests in let after, pre, sampler, prot = function_1_adapter after sampler ty in - test_function_against ?gen - ?test ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) - ~after ?sampler prot (lookup_student ty name) (lookup_solution ty name) tests - - let test_function_1_against_postcond ?gen - ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler test ty name tests = + test_function_against ?gen ?test ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) + ~after ?sampler prot (lookup_student ty name) (lookup_solution ty name) + tests + + let test_function_1_against_postcond ?gen ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler test ty name tests = let tests = List.map (fun x -> Fun_ty.last x) tests in - let after = match after with + let after = + match after with | None -> fun _ _ -> [] | Some after -> Fun_ty.apply_args_1 after in let pre, sampler, prot = function_1_adapter_pre sampler ty in - test_function_against_generic_postcond ?gen - ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) - ~after ?sampler (Fun_ty.apply_args_1 test) name prot (lookup_student ty name) tests + test_function_against_generic_postcond ?gen ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) + ~after ?sampler (Fun_ty.apply_args_1 test) name prot + (lookup_student ty name) tests (*----------------------------------------------------------------------------*) let function_2_adapter_pre sampler ty = let pre = function - | None -> (fun _ -> ()) - | Some pre -> Fun_ty.apply_args_2 pre in - let sampler = match sampler with + | None -> fun _ -> () + | Some pre -> Fun_ty.apply_args_2 pre + in + let sampler = + match sampler with | None -> None | Some sampler -> - Some (fun () -> let a, b = sampler () in Fun_ty.arg a (Fun_ty.last b)) in + Some + (fun () -> + let a, b = sampler () in + Fun_ty.arg a (Fun_ty.last b) ) + in let arg1_ty, ret_ty = Ty.domains ty in let arg2_ty, ret_ty = Ty.domains ret_ty in let prot = Fun_ty.arg_ty arg1_ty @@ Fun_ty.last_ty arg2_ty @@ ret_ty in - pre, sampler, prot + (pre, sampler, prot) let function_2_adapter after sampler ty = - let after = match after with - | None -> (fun _ _ _ -> []) - | Some after -> Fun_ty.apply_args_2 after in + let after = + match after with + | None -> fun _ _ _ -> [] + | Some after -> Fun_ty.apply_args_2 after + in let pre, sampler, prot = function_2_adapter_pre sampler ty in - after, pre, sampler, prot - - let test_function_2 - ?test ?test_stdout ?test_stderr - ?before ?after ty name tests = - let tests = List.map (fun (x, y, r, out, err) -> - (Fun_ty.arg x @@ Fun_ty.last y, (fun () -> output_string stdout out ; output_string stderr err ; r))) - tests in + (after, pre, sampler, prot) + + let test_function_2 ?test ?test_stdout ?test_stderr ?before ?after ty name + tests = + let tests = + List.map + (fun (x, y, r, out, err) -> + ( Fun_ty.arg x @@ Fun_ty.last y + , fun () -> output_string stdout out; output_string stderr err; r + ) ) + tests + in let after, pre, _, prot = function_2_adapter after None ty in - test_function - ?test ?test_stdout ?test_stderr - ~before:(pre before) - ~after prot (lookup_student ty name) tests - - let test_function_2_against ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler ty name rf tests = - let tests = List.map (fun (x, y) -> Fun_ty.arg x @@ Fun_ty.last y) tests in + test_function ?test ?test_stdout ?test_stderr ~before:(pre before) ~after + prot (lookup_student ty name) tests + + let test_function_2_against ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler ty name rf tests = + let tests = + List.map (fun (x, y) -> Fun_ty.arg x @@ Fun_ty.last y) tests + in let after, pre, sampler, prot = function_2_adapter after sampler ty in - test_function_against ?gen - ?test ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) + test_function_against ?gen ?test ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) ~after ?sampler prot (lookup_student ty name) (found name rf) tests - let test_function_2_against_solution ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler ty name tests = - let tests = List.map (fun (x, y) -> Fun_ty.arg x @@ Fun_ty.last y) tests in + let test_function_2_against_solution ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler ty name tests = + let tests = + List.map (fun (x, y) -> Fun_ty.arg x @@ Fun_ty.last y) tests + in let after, pre, sampler, prot = function_2_adapter after sampler ty in - test_function_against ?gen - ?test ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) - ~after ?sampler prot (lookup_student ty name) (lookup_solution ty name) tests - - let test_function_2_against_postcond ?gen - ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler test ty name tests = - let tests = List.map (fun (x, y) -> Fun_ty.arg x @@ Fun_ty.last y) tests in - let after = match after with - | None -> (fun _ _ -> []) - | Some after -> Fun_ty.apply_args_2 after in + test_function_against ?gen ?test ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) + ~after ?sampler prot (lookup_student ty name) (lookup_solution ty name) + tests + + let test_function_2_against_postcond ?gen ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler test ty name tests = + let tests = + List.map (fun (x, y) -> Fun_ty.arg x @@ Fun_ty.last y) tests + in + let after = + match after with + | None -> fun _ _ -> [] + | Some after -> Fun_ty.apply_args_2 after + in let pre, sampler, prot = function_2_adapter_pre sampler ty in - test_function_against_generic_postcond ?gen - ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) - ~after ?sampler (Fun_ty.apply_args_2 test) name prot (lookup_student ty name) tests + test_function_against_generic_postcond ?gen ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) + ~after ?sampler (Fun_ty.apply_args_2 test) name prot + (lookup_student ty name) tests (*----------------------------------------------------------------------------*) let function_3_adapter_pre sampler ty = let pre = function - | None -> (fun _ -> ()) - | Some pre -> Fun_ty.apply_args_3 pre in - let sampler = match sampler with + | None -> fun _ -> () + | Some pre -> Fun_ty.apply_args_3 pre + in + let sampler = + match sampler with | None -> None | Some sampler -> - Some - (fun () -> - let a, b, c = sampler () in - Fun_ty.arg a (Fun_ty.arg b (Fun_ty.last c))) in + Some + (fun () -> + let a, b, c = sampler () in + Fun_ty.arg a (Fun_ty.arg b (Fun_ty.last c)) ) + in let arg1_ty, ret_ty = Ty.domains ty in let arg2_ty, ret_ty = Ty.domains ret_ty in let arg3_ty, ret_ty = Ty.domains ret_ty in - let prot = Fun_ty.arg_ty arg1_ty @@ Fun_ty.arg_ty arg2_ty @@ Fun_ty.last_ty arg3_ty @@ ret_ty in - pre, sampler, prot + let prot = + Fun_ty.arg_ty arg1_ty @@ Fun_ty.arg_ty arg2_ty + @@ Fun_ty.last_ty arg3_ty @@ ret_ty + in + (pre, sampler, prot) let function_3_adapter after sampler ty = - let after = match after with - | None -> (fun _ _ _-> []) - | Some after -> Fun_ty.apply_args_3 after in + let after = + match after with + | None -> fun _ _ _ -> [] + | Some after -> Fun_ty.apply_args_3 after + in let pre, sampler, prot = function_3_adapter_pre sampler ty in - after, pre, sampler, prot - - let test_function_3 - ?test ?test_stdout ?test_stderr - ?before ?after ty name tests = - let tests = List.map (fun (w, x, y, r, out, err) -> - (Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.last y, (fun () -> output_string stdout out ; output_string stderr err ; r))) - tests in + (after, pre, sampler, prot) + + let test_function_3 ?test ?test_stdout ?test_stderr ?before ?after ty name + tests = + let tests = + List.map + (fun (w, x, y, r, out, err) -> + ( Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.last y + , fun () -> output_string stdout out; output_string stderr err; r + ) ) + tests + in let after, pre, _, prot = function_3_adapter after None ty in - test_function - ?test ?test_stdout ?test_stderr - ~before:(pre before) - ~after prot (lookup_student ty name) tests - - let test_function_3_against ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler ty name rf tests = - let tests = List.map (fun (w, x, y) -> Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.last y) tests in + test_function ?test ?test_stdout ?test_stderr ~before:(pre before) ~after + prot (lookup_student ty name) tests + + let test_function_3_against ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler ty name rf tests = + let tests = + List.map + (fun (w, x, y) -> Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.last y) + tests + in let after, pre, sampler, prot = function_3_adapter after sampler ty in - test_function_against ?gen - ?test ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) + test_function_against ?gen ?test ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) ~after ?sampler prot (lookup_student ty name) (found name rf) tests - let test_function_3_against_solution ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler ty name tests = - let tests = List.map (fun (w, x, y) -> Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.last y) tests in + let test_function_3_against_solution ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler ty name tests = + let tests = + List.map + (fun (w, x, y) -> Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.last y) + tests + in let after, pre, sampler, prot = function_3_adapter after sampler ty in - test_function_against ?gen - ?test ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) - ~after ?sampler prot (lookup_student ty name) (lookup_solution ty name) tests - - let test_function_3_against_postcond ?gen - ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler test ty name tests = - let tests = List.map (fun (w, x, y) -> Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.last y) tests in - let after = match after with - | None -> (fun _ _ -> []) - | Some after -> Fun_ty.apply_args_3 after in + test_function_against ?gen ?test ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) + ~after ?sampler prot (lookup_student ty name) (lookup_solution ty name) + tests + + let test_function_3_against_postcond ?gen ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler test ty name tests = + let tests = + List.map + (fun (w, x, y) -> Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.last y) + tests + in + let after = + match after with + | None -> fun _ _ -> [] + | Some after -> Fun_ty.apply_args_3 after + in let pre, sampler, prot = function_3_adapter_pre sampler ty in - test_function_against_generic_postcond ?gen - ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) - ~after ?sampler (Fun_ty.apply_args_3 test) name prot (lookup_student ty name) tests + test_function_against_generic_postcond ?gen ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) + ~after ?sampler (Fun_ty.apply_args_3 test) name prot + (lookup_student ty name) tests (*----------------------------------------------------------------------------*) let function_4_adapter_pre sampler ty = let pre = function - | None -> (fun _ -> ()) - | Some pre -> Fun_ty.apply_args_4 pre in - let sampler = match sampler with + | None -> fun _ -> () + | Some pre -> Fun_ty.apply_args_4 pre + in + let sampler = + match sampler with | None -> None | Some sampler -> - Some - (fun () -> - let a, b, c, d = sampler () in - Fun_ty.arg a (Fun_ty.arg b (Fun_ty.arg c (Fun_ty.last d)))) in + Some + (fun () -> + let a, b, c, d = sampler () in + Fun_ty.arg a (Fun_ty.arg b (Fun_ty.arg c (Fun_ty.last d))) ) + in let arg1_ty, ret_ty = Ty.domains ty in let arg2_ty, ret_ty = Ty.domains ret_ty in let arg3_ty, ret_ty = Ty.domains ret_ty in let arg4_ty, ret_ty = Ty.domains ret_ty in let prot = - Fun_ty.arg_ty arg1_ty @@ Fun_ty.arg_ty arg2_ty @@ Fun_ty.arg_ty arg3_ty @@ - Fun_ty.last_ty arg4_ty @@ ret_ty in - pre, sampler, prot + Fun_ty.arg_ty arg1_ty @@ Fun_ty.arg_ty arg2_ty @@ Fun_ty.arg_ty arg3_ty + @@ Fun_ty.last_ty arg4_ty @@ ret_ty + in + (pre, sampler, prot) let function_4_adapter after sampler ty = - let after = match after with - | None -> (fun _ _ _-> []) - | Some after -> Fun_ty.apply_args_4 after in + let after = + match after with + | None -> fun _ _ _ -> [] + | Some after -> Fun_ty.apply_args_4 after + in let pre, sampler, prot = function_4_adapter_pre sampler ty in - after, pre, sampler, prot - - let test_function_4 - ?test ?test_stdout ?test_stderr - ?before ?after ty name tests = - let tests = List.map (fun (w, x, y, z, r, out, err) -> - (Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.arg y @@ Fun_ty.last z, (fun () -> - output_string stdout out ; output_string stderr err ; r))) - tests in + (after, pre, sampler, prot) + + let test_function_4 ?test ?test_stdout ?test_stderr ?before ?after ty name + tests = + let tests = + List.map + (fun (w, x, y, z, r, out, err) -> + ( Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.arg y @@ Fun_ty.last z + , fun () -> output_string stdout out; output_string stderr err; r + ) ) + tests + in let after, pre, _, prot = function_4_adapter after None ty in - test_function - ?test ?test_stdout ?test_stderr - ~before:(pre before) - ~after prot (lookup_student ty name) tests - - let test_function_4_against ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler ty name rf tests = - let tests = List.map (fun (w, x, y, z) -> Fun_ty.arg w @@ Fun_ty.arg x @@ - Fun_ty.arg y @@ Fun_ty.last z) tests in + test_function ?test ?test_stdout ?test_stderr ~before:(pre before) ~after + prot (lookup_student ty name) tests + + let test_function_4_against ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler ty name rf tests = + let tests = + List.map + (fun (w, x, y, z) -> + Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.arg y @@ Fun_ty.last z ) + tests + in let after, pre, sampler, prot = function_4_adapter after sampler ty in - test_function_against ?gen - ?test ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) + test_function_against ?gen ?test ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) ~after ?sampler prot (lookup_student ty name) (found name rf) tests - let test_function_4_against_solution ?gen - ?test ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler ty name tests = - let tests = List.map (fun (w, x, y, z) -> Fun_ty.arg w @@ Fun_ty.arg x @@ - Fun_ty.arg y @@ Fun_ty.last z) tests in + let test_function_4_against_solution ?gen ?test ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler ty name tests = + let tests = + List.map + (fun (w, x, y, z) -> + Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.arg y @@ Fun_ty.last z ) + tests + in let after, pre, sampler, prot = function_4_adapter after sampler ty in - test_function_against ?gen - ?test ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) - ~after ?sampler prot (lookup_student ty name) (lookup_solution ty name) tests - - let test_function_4_against_postcond ?gen - ?test_stdout ?test_stderr - ?before_reference ?before_user ?after ?sampler test ty name tests = - let tests = List.map (fun (w, x, y, z) -> Fun_ty.arg w @@ Fun_ty.arg x @@ - Fun_ty.arg y @@ Fun_ty.last z) tests in - let after = match after with - | None -> (fun _ _ -> []) - | Some after -> Fun_ty.apply_args_4 after in - let pre, sampler, prot = function_4_adapter_pre sampler ty in - test_function_against_generic_postcond ?gen - ?test_stdout ?test_stderr - ~before_reference:(pre before_reference) - ~before_user:(pre before_user) - ~after ?sampler (Fun_ty.apply_args_4 test) name prot (lookup_student ty name) tests + test_function_against ?gen ?test ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) + ~after ?sampler prot (lookup_student ty name) (lookup_solution ty name) + tests + let test_function_4_against_postcond ?gen ?test_stdout ?test_stderr + ?before_reference ?before_user ?after ?sampler test ty name tests = + let tests = + List.map + (fun (w, x, y, z) -> + Fun_ty.arg w @@ Fun_ty.arg x @@ Fun_ty.arg y @@ Fun_ty.last z ) + tests + in + let after = + match after with + | None -> fun _ _ -> [] + | Some after -> Fun_ty.apply_args_4 after + in + let pre, sampler, prot = function_4_adapter_pre sampler ty in + test_function_against_generic_postcond ?gen ?test_stdout ?test_stderr + ~before_reference:(pre before_reference) ~before_user:(pre before_user) + ~after ?sampler (Fun_ty.apply_args_4 test) name prot + (lookup_student ty name) tests end (*----------------------------------------------------------------------------*) @@ -1717,30 +2078,32 @@ module Make let sample_float () = Random.float 10. -. 5. - let sample_char () = - Char.chr (Random.int 26 + Char.code 'a') + let sample_char () = Char.chr (Random.int 26 + Char.code 'a') let sample_string () = let sample_piece () = - [| "ba" ; "be" ; "4456" ; - ", " ; "-" ; " " ; - "OCaml" ; "OCP" ; "//" ; "#" |].(Random.int 10) in + [|"ba"; "be"; "4456"; ", "; "-"; " "; "OCaml"; "OCP"; "//"; "#"|].(Random + .int + 10) + in let length = Random.int 10 in - let rec make = function 0 -> [] | n -> sample_piece () :: make (n - 1) in + let rec make = function + | 0 -> [] + | n -> sample_piece () :: make (n - 1) + in String.concat "" (make length) let sample_alternatively samplers = let samplers = Array.of_list samplers in let samplers = - if Random.bool () then - Array.concat [ samplers ; samplers ] - else samplers in + if Random.bool () then Array.concat [samplers; samplers] else samplers + in let cycle = Array.length samplers in let t = ref (cycle - 1) in fun () -> - t := (!t + 1) mod cycle ; - if !t = 0 then Array.sort (fun _ _ -> Random.int 2 * 2 - 1) samplers ; - samplers.(!t) () + t := (!t + 1) mod cycle; + if !t = 0 then Array.sort (fun _ _ -> (Random.int 2 * 2) - 1) samplers; + samplers.(!t) () let sample_cases cases = let cases = List.map (fun case () -> case) cases in @@ -1749,56 +2112,67 @@ module Make let sample_option sample = let none () = None in let some () = Some (sample ()) in - sample_alternatively [ none ; some ; some ; some ] + sample_alternatively [none; some; some; some] - let sample_array ?(min_size = 0) ?(max_size = 10) ?(dups = true) ?(sorted = false) sample () = + let sample_array ?(min_size = 0) ?(max_size = 10) ?(dups = true) + ?(sorted = false) sample () = let sample = - if dups then sample else + if dups then sample + else let prev = Hashtbl.create max_size in let rec sample_new steps = - if steps = 0 then invalid_arg "sample_array" else + if steps = 0 then invalid_arg "sample_array" + else let s = sample () in - try Hashtbl.find prev s ; sample_new (steps - 1) - with Not_found -> Hashtbl.add prev s () ; s in - fun () -> sample_new 100 in + try + Hashtbl.find prev s; + sample_new (steps - 1) + with Not_found -> Hashtbl.add prev s (); s + in + fun () -> sample_new 100 + in let len = Random.int (max_size - min_size + 1) + min_size in let arr = Array.init len (fun _ -> sample ()) in - if sorted then Array.sort compare arr ; + if sorted then Array.sort compare arr; arr let sample_list ?min_size ?max_size ?dups ?sorted sample () = Array.to_list (sample_array ?min_size ?max_size ?dups ?sorted sample ()) - let sample_pair sample1 sample2 () = - (sample1 (), sample2 ()) + let sample_pair sample1 sample2 () = (sample1 (), sample2 ()) let printable_funs = ref [] let fun_printer ppf f = let rec find = function | (f', n) :: rest -> - if f == f' then - Format.fprintf ppf "%s" n - else - find rest + if f == f' then Format.fprintf ppf "%s" n else find rest | [] -> Format.fprintf ppf "" in find !printable_funs let printable_fun n f = - printable_funs := (Obj.repr f, n) :: !printable_funs ; f + printable_funs := (Obj.repr f, n) :: !printable_funs; + f let () = let path = Path.Pident (Ident.create "fun_printer") in - let ty = Typetexp.transl_type_scheme !Toploop.toplevel_env (Ty.obj [%ty: _ -> _ ]) in + let ty = + Typetexp.transl_type_scheme !Toploop.toplevel_env + (Ty.obj [%ty: _ -> _]) + in Toploop.install_printer path ty.Typedtree.ctyp_type fun_printer end - let (@@@) f g = fun x -> f x @ g x - let (@@>) r1 f = if snd (Learnocaml_report.result r1) then r1 else f () - let (@@=) r1 f = if snd (Learnocaml_report.result r1) then r1 else r1 @ f () + let ( @@@ ) f g x = f x @ g x + + let ( @@> ) r1 f = if snd (Learnocaml_report.result r1) then r1 else f () + + let ( @@= ) r1 f = + if snd (Learnocaml_report.result r1) then r1 else r1 @ f () (**/**) + include Ast_checker include Tester include Mutation @@ -1807,8 +2181,6 @@ module Make include Test_functions_ref_var include Test_functions_function include Test_functions_generic - end -let () = - Random.self_init () +let () = Random.self_init () diff --git a/src/grader/test_lib.mli b/src/grader/test_lib.mli index 2a8845477..32067eab7 100644 --- a/src/grader/test_lib.mli +++ b/src/grader/test_lib.mli @@ -9,7 +9,6 @@ (** Documentation for [test_lib] library. [Test_lib] module can be used to write graders for learn-ocaml. *) module type S = sig - val set_result : Learnocaml_report.t -> unit type nonrec 'a result = ('a, exn) result @@ -20,7 +19,6 @@ module type S = sig enforce restrictions on the student's code. *) module Ast_checker : sig - (** Since the user's code is reified, the parsed abstract syntax tree is available in the testing environment, as a variable named [code_ast], with type [Parsetree.structure]. As such, it @@ -74,32 +72,38 @@ module type S = sig *) type 'a ast_checker = - ?on_expression: (Parsetree.expression -> Learnocaml_report.t) -> - ?on_pattern: (Parsetree.pattern -> Learnocaml_report.t) -> - ?on_structure_item: (Parsetree.structure_item -> Learnocaml_report.t) -> - ?on_external: (Parsetree.value_description -> Learnocaml_report.t) -> - ?on_include: (Parsetree.include_declaration -> Learnocaml_report.t) -> - ?on_open: (Parsetree.open_description -> Learnocaml_report.t) -> - ?on_module_occurence: (string -> Learnocaml_report.t) -> - ?on_variable_occurence: (string -> Learnocaml_report.t) -> - ?on_function_call: ( - (Parsetree.expression - * (string * Parsetree.expression) list) -> Learnocaml_report.t) - -> 'a -> Learnocaml_report.t + ?on_expression:(Parsetree.expression -> Learnocaml_report.t) + -> ?on_pattern:(Parsetree.pattern -> Learnocaml_report.t) + -> ?on_structure_item:(Parsetree.structure_item -> Learnocaml_report.t) + -> ?on_external:(Parsetree.value_description -> Learnocaml_report.t) + -> ?on_include:(Parsetree.include_declaration -> Learnocaml_report.t) + -> ?on_open:(Parsetree.open_description -> Learnocaml_report.t) + -> ?on_module_occurence:(string -> Learnocaml_report.t) + -> ?on_variable_occurence:(string -> Learnocaml_report.t) + -> ?on_function_call:( Parsetree.expression + * (string * Parsetree.expression) list + -> Learnocaml_report.t) + -> 'a + -> Learnocaml_report.t + val ast_check_expr : Parsetree.expression ast_checker (** [ast_check_expr] builds an {{!ast_checker}AST checker} for [Parsetree] expressions. This function can be used as functional argument for {!find_binding}. *) - val ast_check_expr : Parsetree.expression ast_checker + val ast_check_structure : Parsetree.structure ast_checker (** [ast_check_structure] builds an {{!ast_checker}AST checker} for [Parsetree] structure. The returned AST checker can directly be used with [code_ast] which is available in the grading environment. *) - val ast_check_structure : Parsetree.structure ast_checker (** {2 Finding top level variable in AST}*) + val find_binding : + Parsetree.structure + -> string + -> (Parsetree.expression -> Learnocaml_report.t) + -> Learnocaml_report.t (** [find_binding code_ast name cb] looks for the top level variable [name] in [code_ast] and its associated Parsetree expression [expr] ([let name = expr]). If the variable is @@ -107,12 +111,9 @@ module type S = sig concatenated with the report resulting of [cb] applied to [expr]. Otherwise, it returns a {!Learnocaml_report.Failure} report. *) - val find_binding : - Parsetree.structure -> string - -> (Parsetree.expression -> Learnocaml_report.t) - -> Learnocaml_report.t (** {2 Functions for optional arguments of checkers} *) + (** The following functions are classic functions to use as optional arguments for {!ast_checker} like forbidding, rectricting or requiring some [Parsetree] structures or @@ -120,34 +121,38 @@ module type S = sig (** {3 Generic functions} *) + val forbid : + string -> ('a -> string) -> 'a list -> 'a -> Learnocaml_report.t (** [forbid k pr ls t] returns a {{!Learnocaml_report.Failure}Failure} the first time [t] is tested if [t] is in the list [ls]. The message of the report is {e The text1 text2 is forbidden} where [text1] is the result of [pr] applies to [t] and [text2] is value [k]. Otherwise, an empty report is returned. *) - val forbid : - string -> ('a -> string) -> 'a list -> ('a -> Learnocaml_report.t) + val restrict : + string -> ('a -> string) -> 'a list -> 'a -> Learnocaml_report.t (** [restrict k pr ls t] returns a {{!Learnocaml_report.Failure}Failure} the first time [t] is tested if [t] is {e not} in [ls]. The message of the report is {e The text1 text2 is not allowed} where [text1] is the result of [pr] applies to [t] and [text2] is value of [k]. Otherwise, an empty report is returned. *) - val restrict : - string -> ('a -> string) -> 'a list -> ('a -> Learnocaml_report.t) + val require : string -> ('a -> string) -> 'a -> 'a -> Learnocaml_report.t (** [require k pr _ t] returns a {{!Learnocaml_report.Success 5}Success 5} report the first time this function is called. The message of the report is {e Found text1 text2} where [text1] is value of [k] and [text2] is the result of [pr] applies to [t]. Otherwise, an empty report is returned. *) - val require : - string -> ('a -> string) -> 'a -> ('a -> Learnocaml_report.t) (** {3 For expressions } *) + val forbid_expr : + string + -> Parsetree.expression list + -> Parsetree.expression + -> Learnocaml_report.t (** [forbid_expr name exprs expr] returns a {{!Learnocaml_report.Failure}Failure} report the first time [expr] is tested if [expr] is in the list of forbidden @@ -155,10 +160,12 @@ module type S = sig The text1 text2 is forbidden} where [text1] is [expr] and [text2] is value of [name]. Otherwise, an empty report is returned. *) - val forbid_expr : - string -> Parsetree.expression list - -> (Parsetree.expression -> Learnocaml_report.t) + val restrict_expr : + string + -> Parsetree.expression list + -> Parsetree.expression + -> Learnocaml_report.t (** [restrict_expr name exprs expr] returns a {{!Learnocaml_report.Failure}Failure} report the first time [expr] is tested if [expr] is {e not} in the list of allowed @@ -166,18 +173,17 @@ module type S = sig The text1 text2 is not allowed} where [text1] is [expr] and [text2] is value of [name]. Otherwise, an empty report is returned. *) - val restrict_expr : - string -> Parsetree.expression list - -> (Parsetree.expression -> Learnocaml_report.t) + val require_expr : + string + -> Parsetree.expression + -> Parsetree.expression + -> Learnocaml_report.t (** [require_expr name _ t] returns a {{!Learnocaml_report.Success 5}Success 5} report the first time this function is called. The message of the success report is {e Found text1 text2} where [text1] is value of [name] and [text2] is the result of [pr] applies to [t]. Otherwise, an empty report is returned. *) - val require_expr : - string -> Parsetree.expression - -> (Parsetree.expression -> Learnocaml_report.t) (** {3 For syntax } *) @@ -190,28 +196,28 @@ module type S = sig (forbid "open")] prevents the student from using [open] and [include] syntaxes. *) + val forbid_syntax : string -> _ -> Learnocaml_report.t (** [forbid_syntax n _] returns a {{!Learnocaml_report.Failure}Failure} report the first time it is called. The message of the report is {e The {b text} syntax is forbidden} where [text] is the value of [n]. Otherwise, an empty report is returned. *) - val forbid_syntax : string -> (_ -> Learnocaml_report.t) + val require_syntax : string -> _ -> Learnocaml_report.t (** [require_syntax n _] returns a {{!Learnocaml_report.Success 5}Success 5} report the first time it is called. The message of the report is {e The {b text} syntax has been found, as expected} where [text] is the value of [n]. Otherwise, an empty report is returned. *) - val require_syntax : string -> (_ -> Learnocaml_report.t) (** {2 AST sanity checks } *) - (** [ast_sanity_check ~modules ast cb] *) val ast_sanity_check : - ?modules: string list -> Parsetree.structure + ?modules:string list + -> Parsetree.structure -> (unit -> Learnocaml_report.t) -> Learnocaml_report.t - + (** [ast_sanity_check ~modules ast cb] *) end (** {1 Testers and IO testers} *) @@ -224,18 +230,15 @@ module type S = sig (** Functions of type [tester] are used to compare student result with solution result. The first {!S.result} is the student output and the second one is the solution output. *) - type 'a tester = - 'a Ty.ty -> 'a result -> 'a result -> Learnocaml_report.t + type 'a tester = 'a Ty.ty -> 'a result -> 'a result -> Learnocaml_report.t (** Functions of type [io_tester] are used to compare student standard out or standard error channels with solution ones. *) - type io_tester = - string -> string -> Learnocaml_report.t + type io_tester = string -> string -> Learnocaml_report.t (** Functions of type [io_postcond] are used to verify that student standard out or standard error channels satisfy a postcondition. *) - type io_postcond = - string -> Learnocaml_report.t + type io_postcond = string -> Learnocaml_report.t (** The exception [Timeout limit] is raised by [run_timeout]. Thus, the functions [exec] and [result] can return [Error (Timeout limit)]. @@ -261,50 +264,50 @@ module type S = sig (** {2:tester_sec Pre-defined testers and tester builders} *) + val test : 'a tester (** [test] is a {!S.tester} that compares its two {!S.result} inputs with OCaml structural equality. This is the default value of [~test] optional argument of grading functions for functions.*) - val test : 'a tester + val test_ignore : 'a tester (** [test_ignore] is a {!S.tester} that compares only the constructor of its {S.result} inputs. The content is ignored. If the constructors match, an empty report is returned. *) - val test_ignore : 'a tester + val test_eq : ('a result -> 'a result -> bool) -> 'a tester (** [test_eq eq] builds a {!S.tester} with function [eq] as comparison function. *) - val test_eq : ('a result -> 'a result -> bool) -> 'a tester + val test_eq_ok : ('a -> 'a -> bool) -> 'a tester (** [test_eq_ok eq] builds a {!S.tester} that compares [Ok] results with [eq] and [Error] results with Ocaml structural equality. *) - val test_eq_ok : ('a -> 'a -> bool) -> 'a tester + val test_eq_exn : (exn -> exn -> bool) -> 'a tester (** [test_eq_exn eq] builds a {!S.tester} that compares [Error] results with [eq] and [Ok] results with Ocaml structural equality. *) - val test_eq_exn : (exn -> exn -> bool) -> 'a tester + val test_canon : ('a result -> 'a result) -> 'a tester (** [test_canon canon] builds a {!S.tester} that compares its two {S.result} inputs after application to [canon] function with Ocaml structural equality. *) - val test_canon : ('a result -> 'a result) -> 'a tester + val test_canon_ok : ('a -> 'a) -> 'a tester (** [test_canon_ok canon] builds a {!S.tester} that compares two [Ok] result inputs after application to [canon] function with Ocaml structural equality. [Error] results are compared normally with Ocaml structural equality. *) - val test_canon_ok : ('a -> 'a) -> 'a tester + val test_canon_error : (exn -> exn) -> 'a tester (** [test_canon_error canon] builds a {!S.tester} that compares two [Error] result inputs after application to [canon] function with Ocaml structural equality. [Ok] results are compared normally with Ocaml structural equality. *) - val test_canon_error : (exn -> exn) -> 'a tester + val test_translate : ('a -> 'b) -> 'b tester -> 'b Ty.ty -> 'a tester (** [test_translate conv test ty] builds a {!S.tester} that translates its inputs [va] and [vb] to ['b results] [va_trans] and [vb_trans] using the conversion function [conv] and returns the report of [test ty va_trans vb_trans].*) - val test_translate : ('a -> 'b) -> 'b tester -> 'b Ty.ty -> 'a tester (** {2:io_tester_sec Pre-defined IO testers and IO tester builders} *) @@ -322,36 +325,40 @@ module type S = sig - [~drop] : list of chars removed from IO testers input strings *) - + val io_test_ignore : io_tester (** [io_test_ignore] is the default value of [~test_stdout] and [~test_stderr]. Returns an empty report whatever its inputs. *) - val io_test_ignore : io_tester + val io_test_equals : ?trim:char list -> ?drop:char list -> io_tester (** [io_test_equals] builds a {!S.io_tester} which compares its input strings using Ocaml structural equality. *) - val io_test_equals : - ?trim: char list -> ?drop: char list -> io_tester + val io_test_lines : + ?trim:char list + -> ?drop:char list + -> ?skip_empty:bool + -> ?test_line:io_tester + -> io_tester (** [io_test_lines ~skip_empty ~test_line] builds a {!S.io_tester} which compares each line (separated with ['\n']) of its two string inputs with [test_line]. The default value of [test_line] is [io_tester_equals]. If [skip_empty] is set to [true], the empty lines are skipped (default value is [false]). *) - val io_test_lines : - ?trim: char list -> ?drop: char list -> - ?skip_empty: bool -> ?test_line: io_tester -> io_tester + val io_test_items : + ?split:char list + -> ?trim:char list + -> ?drop:char list + -> ?skip_empty:bool + -> ?test_item:io_tester + -> io_tester (** [io_test_items ~split ~skip_empty ~test_item] buids a {!S.io_tester} which splits its two string inputs into several items using [split] as separators and compares each item with [test_item] ([io_tester_equals] by default). If [skip_empty] is set to [true], the empty items are skipped (default value is [false]). *) - val io_test_items : - ?split: char list -> ?trim: char list -> ?drop: char list -> - ?skip_empty: bool -> ?test_item: io_tester -> io_tester - end (** {1 Mutation observer builders} *) @@ -360,16 +367,21 @@ module type S = sig [~before_reference], [~before_user], [~test] used by grading functions for {b unary} function with a mutable input. *) module Mutation : sig - (** Important warning: this part is useful only to grade unary function using grading functions such than {!S.Test_functions_function.test_function_1_against_solution}. *) type 'arg arg_mutation_test_callbacks = - { before_reference : 'arg -> unit ; - before_user : 'arg -> unit ; - test : 'ret. ?test_result: 'ret tester -> 'ret tester } - + { before_reference : 'arg -> unit + ; before_user : 'arg -> unit + ; test : 'ret. ?test_result:'ret tester -> 'ret tester } + + val arg_mutation_test_callbacks : + ?test:'a tester + -> dup:('a -> 'a) + -> blit:('a -> 'a -> unit) + -> 'a Ty.ty + -> 'a arg_mutation_test_callbacks (** [arg_mutation_test_callbacks ~test_ref ~dup ~blit ty] returns a {!Mutation.arg_mutation_test_callbacks} [out] such than the functions [out.before_reference] and [out.before_user] can @@ -388,31 +400,26 @@ module type S = sig [dup in] returns a copy of [in]. [blit src dst] copies [src] into [dst].*) - val arg_mutation_test_callbacks: - ?test: 'a tester -> dup: ('a -> 'a) - -> blit:('a -> 'a -> unit) -> 'a Ty.ty - -> 'a arg_mutation_test_callbacks + val array_arg_mutation_test_callbacks : + ?test:'a array tester + -> 'a array Ty.ty + -> 'a array arg_mutation_test_callbacks (** [array_arg_mutation_test_callbacks ~test_arr ty] builds [before_user], [before_reference] and [test] such than [test] can compare mutation of an input array through student code and solution. By default, [test_arr] is set to {!Tester.test}.*) - val array_arg_mutation_test_callbacks: - ?test: 'a array tester -> 'a array Ty.ty -> - 'a array arg_mutation_test_callbacks + val ref_arg_mutation_test_callbacks : + ?test:'a ref tester -> 'a ref Ty.ty -> 'a ref arg_mutation_test_callbacks (** [ref_arg_mutation_test_callbacks ~test_ref ty] builds [before_user], [before_reference] and [test] such than [test] can compare mutation of an input reference through student code and solution. By default, [test_ref] is set to {!Tester.test}. *) - val ref_arg_mutation_test_callbacks: - ?test: 'a ref tester -> 'a ref Ty.ty -> - 'a ref arg_mutation_test_callbacks - end (** {1 Samplers } *) @@ -420,28 +427,34 @@ module type S = sig (** [Sampler] provides a library of predefined samplers for {{!Test_functions_function}grading functions}.*) module Sampler : sig - type 'a sampler = unit -> 'a (** {2:sampler_sec Samplers} *) - (** [sample_int ()] returns a random integer between -5 and 5. *) val sample_int : int sampler + (** [sample_int ()] returns a random integer between -5 and 5. *) - (** [sample_float ()] returns a random float between -5. and 5. *) val sample_float : float sampler + (** [sample_float ()] returns a random float between -5. and 5. *) - (** [sample_string ()] returns a randomly long random string. *) val sample_string : string sampler + (** [sample_string ()] returns a randomly long random string. *) - (** [sample_char ()] returns an alphabet letter randomly. *) val sample_char : char sampler + (** [sample_char ()] returns an alphabet letter randomly. *) - (** [sample_bool ()] returns randomly [false] or [true]. *) val sample_bool : bool sampler + (** [sample_bool ()] returns randomly [false] or [true]. *) (** {2 Sampler builders} *) + val sample_list : + ?min_size:int + -> ?max_size:int + -> ?dups:bool + -> ?sorted:bool + -> 'a sampler + -> 'a list sampler (** [sample_list ~min_size ~max_size sample] returns a list sampler that generates a list of random length between [min_size] ([0] by default) and [max_size] ([10] by default) @@ -452,11 +465,14 @@ module type S = sig If [~dups:false] ([true] by default), all elements of generated list are unique.*) - val sample_list : - ?min_size: int -> ?max_size: int -> ?dups: bool -> ?sorted: bool - -> 'a sampler - -> 'a list sampler + val sample_array : + ?min_size:int + -> ?max_size:int + -> ?dups:bool + -> ?sorted:bool + -> 'a sampler + -> 'a array sampler (** [sample_array ~min_size ~max_size sample] returns an array sampler that generates an array of random length between [min_size] ([0] by default) and [max_size] ([10] by default) @@ -467,29 +483,25 @@ module type S = sig If [~dups:false] ([true] by default), all elements of generated array are unique.*) - val sample_array : - ?min_size: int -> ?max_size: int -> ?dups: bool -> ?sorted: bool - -> 'a sampler - -> 'a array sampler + val sample_pair : 'a sampler -> 'b sampler -> ('a * 'b) sampler (** [sample_pair s1 s2] returns a sampler that generates a value of type ['a * 'b] using [s1] and [s2] to generate values of type ['a] and ['b], respectively, on each call. *) - val sample_pair : 'a sampler -> 'b sampler -> ('a * 'b) sampler + val sample_alternatively : 'a sampler list -> 'a sampler (** [sample_alternatively s] returns a sampler that mimics randomly the behavior of one of [s] sampler and change at each call. *) - val sample_alternatively : 'a sampler list -> 'a sampler + val sample_cases : 'a list -> 'a sampler (** [sample_case cases] returns a sampler that generates randomly one of the value of cases. *) - val sample_cases : 'a list -> 'a sampler + val sample_option : 'a sampler -> 'a option sampler (** [sample_option sample] returns a sampler that generates an ['a option] value using [sample] to generate an ['a] value if necessary. *) - val sample_option : 'a sampler -> 'a option sampler (** {2 Utilities} *) @@ -500,7 +512,7 @@ module type S = sig (** Grading function for variables and references. *) module Test_functions_ref_var : sig - + val test_ref : 'a Ty.ty -> 'a ref -> 'a -> Learnocaml_report.t (** [test_ref ty got exp] returns {!LearnOcaml_report.Success 1} report if reference [got] value is equal to [exp] and {!LearnOcaml_report.Failure} report otherwise. @@ -510,35 +522,30 @@ module type S = sig in student's code. In this case, you should use {{!Mutation}mutation functions}. This function should be used for a reference defined locally (in [test.ml]). *) - val test_ref : - 'a Ty.ty -> 'a ref -> 'a -> Learnocaml_report.t + val test_variable : 'a Ty.ty -> string -> 'a -> Learnocaml_report.t (** [test_variable ty name r] returns {!LearnOcaml_report.Success 1} report if variable named [name] exists and is equal to [r]. Otherwise returns {!LearnOcaml_report.Failure} report.*) - val test_variable : - 'a Ty.ty -> string -> 'a -> Learnocaml_report.t + val test_variable_property : + 'a Ty.ty -> string -> ('a -> Learnocaml_report.t) -> Learnocaml_report.t (** [test_variable_property ty name cb] returns the report resulting of application of cb to variable named [name] if it exists. Otherwise returns {!LearnOcaml_report.Failure} report. *) - val test_variable_property : - 'a Ty.ty -> string -> ('a -> Learnocaml_report.t) -> Learnocaml_report.t + val test_variable_against_solution : + 'a Ty.ty -> string -> Learnocaml_report.t (** [test_variable ty name r] returns {!LearnOcaml_report.Success 1} report if variable named [name] exists and is equal to variable with the same name defined in solution. Otherwise returns {!LearnOcaml_report.Failure} report.*) - val test_variable_against_solution : - 'a Ty.ty -> string -> Learnocaml_report.t - end (** {1 Grading functions for types} *) (** Grading function for types. *) module Test_functions_types : sig - val compatible_type : expected:string -> string -> Learnocaml_report.t val existing_type : ?score:int -> string -> bool * Learnocaml_report.t @@ -557,7 +564,6 @@ module type S = sig (** Grading function for functions. *) module Test_functions_function : sig - (** {2:test_functions_fun_sec Grading functions for functions}*) (** Three grading functions for functions are defined for arity one @@ -578,7 +584,6 @@ module type S = sig report concatening reports of each test. Otherwise a {!Learnocaml_report.Failure} report is returned.*) - (** {3 Returned report}*) (** The grading functions for functions return a {report} which @@ -596,9 +601,21 @@ module type S = sig always returns a non-empty report while the other three returns empty reports. *) - (** {3 Unary functions}*) + val test_function_1 : + ?test:'b tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:('a -> unit) + -> ?after:( 'a + -> 'b * string * string + -> 'b * string * string + -> Learnocaml_report.t) + -> ('a -> 'b) Ty.ty + -> string + -> ('a * 'b * string * string) list + -> Learnocaml_report.t (** [test_function_1 ty name tests] tests the function named [name] by directly comparing obtained outputs against expected outputs. @@ -611,17 +628,24 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_1 : - ?test: 'b tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : ('a -> unit) -> - ?after : ('a -> ('b * string * string) - -> ('b * string * string) - -> Learnocaml_report.t) -> - ('a -> 'b) Ty.ty -> string - -> ('a * 'b * string * string) list -> Learnocaml_report.t + val test_function_1_against : + ?gen:int + -> ?test:'b tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> unit) + -> ?before_user:('a -> unit) + -> ?after:( 'a + -> 'b * string * string + -> 'b * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a) + -> ('a -> 'b) Ty.ty + -> string + -> ('a -> 'b) + -> 'a list + -> Learnocaml_report.t (** [test_function_1_against ty name rf tests] tests the function named [name] by comparing outputs obtained with the student function against outputs of [rf]. @@ -633,20 +657,23 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_1_against : - ?gen: int -> - ?test: 'b tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> unit) -> - ?before_user : ('a -> unit) -> - ?after : ('a - -> ('b * string * string) - -> ('b * string * string) - -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a) -> - ('a -> 'b) Ty.ty -> string -> ('a -> 'b) -> 'a list -> Learnocaml_report.t + val test_function_1_against_solution : + ?gen:int + -> ?test:'b tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> unit) + -> ?before_user:('a -> unit) + -> ?after:( 'a + -> 'b * string * string + -> 'b * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a) + -> ('a -> 'b) Ty.ty + -> string + -> 'a list + -> Learnocaml_report.t (** [test_function_1_against_solution ty name tests] tests the function named [name] by comparison to solution function [rf] which must be defined under name [name] in the corresponding @@ -660,38 +687,42 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_1_against_solution : - ?gen: int -> - ?test: 'b tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> unit) -> - ?before_user : ('a -> unit) -> - ?after : ('a - -> ('b * string * string) - -> ('b * string * string) - -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a) -> - ('a -> 'b) Ty.ty -> string -> 'a list -> Learnocaml_report.t + val test_function_1_against_postcond : + ?gen:int + -> ?test_stdout:io_postcond + -> ?test_stderr:io_postcond + -> ?before_reference:('a -> unit) + -> ?before_user:('a -> unit) + -> ?after:('a -> 'b * string * string -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a) + -> ('a -> 'b Ty.ty -> 'b result -> Learnocaml_report.t) + -> ('a -> 'b) Ty.ty + -> string + -> 'a list + -> Learnocaml_report.t (** [test_function_1_against_postcond postcond ty name tests] tests that the function named [name] satisfies the postcondition [postcond]. See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_1_against_postcond : - ?gen: int -> - ?test_stdout: io_postcond -> - ?test_stderr: io_postcond -> - ?before_reference : ('a -> unit) -> - ?before_user : ('a -> unit) -> - ?after : ('a -> ('b * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a) -> - ('a -> 'b Ty.ty -> 'b result -> Learnocaml_report.t) -> - ('a -> 'b) Ty.ty -> string -> 'a list -> Learnocaml_report.t (** {3 Binary functions }*) + val test_function_2 : + ?test:'c tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:('a -> 'b -> unit) + -> ?after:( 'a + -> 'b + -> 'c * string * string + -> 'c * string * string + -> Learnocaml_report.t) + -> ('a -> 'b -> 'c) Ty.ty + -> string + -> ('a * 'b * 'c * string * string) list + -> Learnocaml_report.t (** [test_function_2 ty name tests] tests the function named [name] by directly comparing obtained outputs against expected outputs. @@ -705,19 +736,25 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_2 : - ?test: 'c tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : ('a -> 'b -> unit) -> - ?after : ('a -> 'b -> ('c * string * string) - -> ('c * string * string) - -> Learnocaml_report.t) -> - ('a -> 'b -> 'c) Ty.ty + + val test_function_2_against : + ?gen:int + -> ?test:'c tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> unit) + -> ?before_user:('a -> 'b -> unit) + -> ?after:( 'a + -> 'b + -> 'c * string * string + -> 'c * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b) + -> ('a -> 'b -> 'c) Ty.ty -> string - -> ('a * 'b * 'c * string * string) list + -> ('a -> 'b -> 'c) + -> ('a * 'b) list -> Learnocaml_report.t - (** [test_function_2_against ty name rf tests] tests the function named [name] by comparing outputs obtained with the student function against outputs of [rf]. @@ -730,23 +767,24 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_2_against : - ?gen: int -> - ?test: 'c tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> unit) -> - ?before_user : ('a -> 'b -> unit) -> - ?after : ('a -> 'b - -> ('c * string * string) - -> ('c * string * string) - -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b) -> - ('a -> 'b -> 'c) Ty.ty -> string - -> ('a -> 'b -> 'c) + + val test_function_2_against_solution : + ?gen:int + -> ?test:'c tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> unit) + -> ?before_user:('a -> 'b -> unit) + -> ?after:( 'a + -> 'b + -> 'c * string * string + -> 'c * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b) + -> ('a -> 'b -> 'c) Ty.ty + -> string -> ('a * 'b) list -> Learnocaml_report.t - (** [test_function_2_against_soltion ty name tests] tests the function named [name] by comparison to solution function [rf] which must be defined under name [name] in the corresponding [solution.ml] @@ -760,38 +798,43 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_2_against_solution : - ?gen: int -> - ?test: 'c tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> unit) -> - ?before_user : ('a -> 'b -> unit) -> - ?after : ('a -> 'b - -> ('c * string * string) - -> ('c * string * string) - -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a * 'b) list -> Learnocaml_report.t + val test_function_2_against_postcond : + ?gen:int + -> ?test_stdout:io_postcond + -> ?test_stderr:io_postcond + -> ?before_reference:('a -> 'b -> unit) + -> ?before_user:('a -> 'b -> unit) + -> ?after:('a -> 'b -> 'c * string * string -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b) + -> ('a -> 'b -> 'c Ty.ty -> 'c result -> Learnocaml_report.t) + -> ('a -> 'b -> 'c) Ty.ty + -> string + -> ('a * 'b) list + -> Learnocaml_report.t (** [test_function_2_against_postcond postcond ty name tests] tests that the function named [name] satisfies the postcondition [postcond]. See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_2_against_postcond : - ?gen: int -> - ?test_stdout: io_postcond -> - ?test_stderr: io_postcond -> - ?before_reference : ('a -> 'b -> unit) -> - ?before_user : ('a -> 'b -> unit) -> - ?after : ('a -> 'b -> ('c * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b) -> - ('a -> 'b -> 'c Ty.ty -> 'c result -> Learnocaml_report.t) -> - ('a -> 'b -> 'c) Ty.ty -> string -> ('a * 'b) list -> Learnocaml_report.t (** {3 Three-arguments functions }*) + val test_function_3 : + ?test:'d tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:('a -> 'b -> 'c -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd * string * string + -> 'd * string * string + -> Learnocaml_report.t) + -> ('a -> 'b -> 'c -> 'd) Ty.ty + -> string + -> ('a * 'b * 'c * 'd * string * string) list + -> Learnocaml_report.t (** [test_function_3 ty name tests] tests the function named [name] by directly comparing obtained outputs against expected outputs. @@ -805,18 +848,26 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_3 : - ?test: 'd tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : ('a -> 'b -> 'c -> unit) -> - ?after : ('a -> 'b -> 'c - -> ('d * string * string) - -> ('d * string * string) -> Learnocaml_report.t) - -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string - -> ('a * 'b * 'c * 'd * string * string) list - -> Learnocaml_report.t + val test_function_3_against : + ?gen:int + -> ?test:'d tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> 'c -> unit) + -> ?before_user:('a -> 'b -> 'c -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd * string * string + -> 'd * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c) + -> ('a -> 'b -> 'c -> 'd) Ty.ty + -> string + -> ('a -> 'b -> 'c -> 'd) + -> ('a * 'b * 'c) list + -> Learnocaml_report.t (** [test_function_3_against ty name rf tests] tests the function named [name] by comparing outputs obtained with the student function against outputs of [rf]. @@ -830,24 +881,25 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_3_against : - ?gen: int -> - ?test: 'd tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> 'c -> unit) -> - ?before_user : ('a -> 'b -> 'c -> unit) -> - ?after : ('a -> 'b -> 'c - -> ('d * string * string) - -> ('d * string * string) - -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c) -> - ('a -> 'b -> 'c -> 'd) Ty.ty + + val test_function_3_against_solution : + ?gen:int + -> ?test:'d tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> 'c -> unit) + -> ?before_user:('a -> 'b -> 'c -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd * string * string + -> 'd * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c) + -> ('a -> 'b -> 'c -> 'd) Ty.ty -> string - -> ('a -> 'b -> 'c -> 'd) -> ('a * 'b * 'c) list -> Learnocaml_report.t - (** [test_function_3_against_solution ty name tests] tests the function named [name] by comparison to solution function [rf] which must be defined under name [name] in the corresponding [solution.ml] @@ -862,39 +914,44 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_3_against_solution : - ?gen: int -> - ?test: 'd tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> 'c -> unit) -> - ?before_user : ('a -> 'b -> 'c -> unit) -> - ?after : ('a -> 'b -> 'c - -> ('d * string * string) - -> ('d * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c) -> - ('a -> 'b -> 'c -> 'd) Ty.ty - -> string -> ('a * 'b * 'c) list - -> Learnocaml_report.t + val test_function_3_against_postcond : + ?gen:int + -> ?test_stdout:io_postcond + -> ?test_stderr:io_postcond + -> ?before_reference:('a -> 'b -> 'c -> unit) + -> ?before_user:('a -> 'b -> 'c -> unit) + -> ?after:('a -> 'b -> 'c -> 'd * string * string -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c) + -> ('a -> 'b -> 'c -> 'd Ty.ty -> 'd result -> Learnocaml_report.t) + -> ('a -> 'b -> 'c -> 'd) Ty.ty + -> string + -> ('a * 'b * 'c) list + -> Learnocaml_report.t (** [test_function_3_against_postcond postcond ty name tests] tests that the function named [name] satisfies the postcondition [postcond]. See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_3_against_postcond : - ?gen: int -> - ?test_stdout: io_postcond -> - ?test_stderr: io_postcond -> - ?before_reference : ('a -> 'b -> 'c -> unit) -> - ?before_user : ('a -> 'b -> 'c -> unit) -> - ?after : ('a -> 'b -> 'c -> ('d * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c) -> - ('a -> 'b -> 'c -> 'd Ty.ty -> 'd result -> Learnocaml_report.t) -> - ('a -> 'b -> 'c -> 'd) Ty.ty -> string -> ('a * 'b * 'c) list -> Learnocaml_report.t (** {3 Four-arguments functions }*) + val test_function_4 : + ?test:'e tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:('a -> 'b -> 'c -> 'd -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd + -> 'e * string * string + -> 'e * string * string + -> Learnocaml_report.t) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a * 'b * 'c * 'd * 'e * string * string) list + -> Learnocaml_report.t (** [test_function_4 ty name tests] tests the function named [name] by directly comparing obtained outputs against expected outputs. @@ -908,19 +965,27 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_4 : - ?test: 'e tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : ('a -> 'b -> 'c -> 'd -> unit) -> - ?after : ('a -> 'b -> 'c -> 'd - -> ('e * string * string) - -> ('e * string * string) - -> Learnocaml_report.t) - -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string - -> ('a * 'b * 'c * 'd * 'e * string * string) list - -> Learnocaml_report.t + val test_function_4_against : + ?gen:int + -> ?test:'e tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> 'c -> 'd -> unit) + -> ?before_user:('a -> 'b -> 'c -> 'd -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd + -> 'e * string * string + -> 'e * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c * 'd) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a -> 'b -> 'c -> 'd -> 'e) + -> ('a * 'b * 'c * 'd) list + -> Learnocaml_report.t (** [test_function_4_against ty name rf tests] tests the function named [name] by comparing outputs obtained with the student function against outputs of [rf]. @@ -934,22 +999,26 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_4_against : - ?gen: int -> - ?test: 'e tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> 'c -> 'd -> unit) -> - ?before_user : ('a -> 'b -> 'c -> 'd -> unit) -> - ?after : ('a -> 'b -> 'c -> 'd - -> ('e * string * string) - -> ('e * string * string) - -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c * 'd) - -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string - -> ('a -> 'b -> 'c -> 'd -> 'e) - -> ('a * 'b * 'c * 'd) list -> Learnocaml_report.t + val test_function_4_against_solution : + ?gen:int + -> ?test:'e tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:('a -> 'b -> 'c -> 'd -> unit) + -> ?before_user:('a -> 'b -> 'c -> 'd -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd + -> 'e * string * string + -> 'e * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c * 'd) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a * 'b * 'c * 'd) list + -> Learnocaml_report.t (** [test_function_4_against_solution ty name tests] tests the function named [name] by comparison to solution function [rf] which must be defined under name [name] in the corresponding @@ -964,36 +1033,30 @@ module type S = sig See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_4_against_solution : - ?gen: int -> - ?test: 'e tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : ('a -> 'b -> 'c -> 'd -> unit) -> - ?before_user : ('a -> 'b -> 'c -> 'd -> unit) -> - ?after : ('a -> 'b -> 'c -> 'd - -> ('e * string * string) - -> ('e * string * string) - -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c * 'd) - -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string - -> ('a * 'b * 'c * 'd) list -> Learnocaml_report.t + val test_function_4_against_postcond : + ?gen:int + -> ?test_stdout:io_postcond + -> ?test_stderr:io_postcond + -> ?before_reference:('a -> 'b -> 'c -> 'd -> unit) + -> ?before_user:('a -> 'b -> 'c -> 'd -> unit) + -> ?after:( 'a + -> 'b + -> 'c + -> 'd + -> 'e * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> 'a * 'b * 'c * 'd) + -> ('a -> 'b -> 'c -> 'd -> 'e Ty.ty -> 'e result -> Learnocaml_report.t) + -> ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty + -> string + -> ('a * 'b * 'c * 'd) list + -> Learnocaml_report.t (** [test_function_4_against_postcond postcond ty name tests] tests that the function named [name] satisfies the postcondition [postcond]. See {{!optional_arguments_sec} this section} for information about optional arguments. *) - val test_function_4_against_postcond : - ?gen: int -> - ?test_stdout: io_postcond -> - ?test_stderr: io_postcond -> - ?before_reference : ('a -> 'b -> 'c -> 'd -> unit) -> - ?before_user : ('a -> 'b -> 'c -> 'd -> unit) -> - ?after : ('a -> 'b -> 'c -> 'd -> ('e * string * string) -> Learnocaml_report.t) -> - ?sampler : (unit -> 'a * 'b * 'c * 'd) -> - ('a -> 'b -> 'c -> 'd -> 'e Ty.ty -> 'e result -> Learnocaml_report.t) -> - ('a -> 'b -> 'c -> 'd -> 'e) Ty.ty -> string -> ('a * 'b * 'c * 'd) list -> Learnocaml_report.t (** {2:optional_arguments_sec Optional arguments for grading functions} *) @@ -1082,120 +1145,115 @@ module type S = sig should instead raise an issue to ask for the corresponding grading functions. *) module Test_functions_generic : sig - + val run_timeout : ?time:int -> (unit -> 'a) -> 'a (** [run_timeout ?time v] executes [v()] under an optional time limit. The exceptions raised by [v] are intentionally *not* caught, so the caller is able to catch and get a backtrace, if desired. If given, [time] overrides the global timeout parameter. *) - val run_timeout : ?time:int -> (unit -> 'a) -> 'a + val exec : (unit -> 'a) -> ('a * string * string) result (** [exec v] executes [v ()] and returns [Ok (r, stdout, stderr)] if no exception is raised and where [r] is the result of [v ()], [stdout] the standard output string (possibly empty) and [stderr] the standard error string (possibly empty) or returns [Error exn] is exception [exn] is raised. In particular, a timeout error [Error (Timeout limit)] can be returned. *) - val exec : (unit -> 'a) -> ('a * string * string) result + val result : (unit -> 'a) -> 'a result (** [result v] executes [v ()] and returns [Ok r] where [r] is the result of [v ()] or [Error exn] if exception [exn] is raised. In particular, a timeout error [Error (Timeout limit)] can be returned. *) - val result : (unit -> 'a) -> 'a result - include (module type of Fun_ty - with type ('a, 'b, 'c) args = ('a, 'b, 'c) Fun_ty.args - and type ('a, 'b, 'c) fun_ty = ('a, 'b, 'c) Fun_ty.fun_ty) + include + module type of Fun_ty + with type ('a, 'b, 'c) args = ('a, 'b, 'c) Fun_ty.args + and type ('a, 'b, 'c) fun_ty = ('a, 'b, 'c) Fun_ty.fun_ty val ty_of_prot : (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> ('ar -> 'row) Ty.ty - [@@ocaml.deprecated "Use ty_of_fun_ty instead."] + [@@ocaml.deprecated "Use ty_of_fun_ty instead."] (** {2 Lookup functions} *) type 'a lookup = - unit + unit -> [ `Found of string * Learnocaml_report.t * 'a | `Unbound of string * Learnocaml_report.t ] - val lookup : 'a Ty.ty -> ?display_name: string -> string -> 'a lookup + val lookup : 'a Ty.ty -> ?display_name:string -> string -> 'a lookup + val lookup_student : 'a Ty.ty -> string -> 'a lookup + val lookup_solution : 'a Ty.ty -> string -> 'a lookup + val found : string -> 'a -> 'a lookup + val name : 'a lookup -> string (** {2 Generic grading functions}*) - (** [test_value lookup cb] *) val test_value : 'a lookup -> ('a -> Learnocaml_report.t) -> Learnocaml_report.t + (** [test_value lookup cb] *) + val test_function : + ?test:'ret tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?after:( ('ar -> 'row, 'ar -> 'urow, 'ret) args + -> 'ret * string * string + -> 'ret * string * string + -> Learnocaml_report.t) + -> (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty + -> ('ar -> 'row) lookup + -> (('ar -> 'row, 'ar -> 'urow, 'ret) args * (unit -> 'ret)) list + -> Learnocaml_report.t (** [test_function ~test ~test_stdout ~test_stderr ~before ~after prot uf tests] *) - val test_function : - ?test: 'ret tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> - unit) -> - ?after : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> - ('ret * string * string) -> - ('ret * string * string) -> - Learnocaml_report.t) -> - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - ('ar -> 'row) lookup -> - (('ar -> 'row, 'ar -> 'urow, 'ret) args * (unit -> 'ret)) list -> - Learnocaml_report.t + val test_function_against : + ?gen:int + -> ?test:'ret tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?before_user:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?after:( ('ar -> 'row, 'ar -> 'urow, 'ret) args + -> 'ret * string * string + -> 'ret * string * string + -> Learnocaml_report.t) + -> ?sampler:(unit -> ('ar -> 'row, 'ar -> 'urow, 'ret) args) + -> (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty + -> ('ar -> 'row) lookup + -> ('ar -> 'row) lookup + -> ('ar -> 'row, 'ar -> 'urow, 'ret) args list + -> Learnocaml_report.t (** [test_function_against ~gen ~test ~test_stdout ~test_stderr ~before_reference ~before_user ~after ~sampler prot uf rf tests] *) - val test_function_against : - ?gen: int -> - ?test: 'ret tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) -> - ?before_user : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) -> - ?after : - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> - ('ret * string * string) -> - ('ret * string * string) -> - Learnocaml_report.t) -> - ?sampler: - (unit -> ('ar -> 'row, 'ar -> 'urow, 'ret) args) -> - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - ('ar -> 'row) lookup -> ('ar -> 'row) lookup -> - ('ar -> 'row, 'ar -> 'urow, 'ret) args list -> - Learnocaml_report.t + val test_function_against_solution : + ?gen:int + -> ?test:'ret tester + -> ?test_stdout:io_tester + -> ?test_stderr:io_tester + -> ?before_reference:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?before_user:(('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) + -> ?after:( ('ar -> 'row, 'ar -> 'urow, 'ret) args + -> 'ret * string * string + -> 'ret * string * string + -> Learnocaml_report.item list) + -> ?sampler:(unit -> ('ar -> 'row, 'ar -> 'urow, 'ret) args) + -> (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty + -> string + -> ('ar -> 'row, 'ar -> 'urow, 'ret) args list + -> Learnocaml_report.item list (** [test_function_against_solution ~gen ~test ~test_stdout ~test_stderr ~before_reference ~before_user ~after ~sampler prot name tests] *) - val test_function_against_solution : - ?gen:int -> - ?test: 'ret tester -> - ?test_stdout: io_tester -> - ?test_stderr: io_tester -> - ?before_reference: - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) -> - ?before_user: - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit) -> - ?after: - (('ar -> 'row, 'ar -> 'urow, 'ret) args -> - 'ret * string * string -> - 'ret * string * string -> - Learnocaml_report.item list) -> - ?sampler: - (unit -> ('ar -> 'row, 'ar -> 'urow, 'ret) args) -> - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - string -> - ('ar -> 'row, 'ar -> 'urow, 'ret) args list -> - Learnocaml_report.item list + val ( ==> ) : 'params -> 'ret -> 'params * (unit -> 'ret) (** Helper notation to test pure functions. [p ==> r] is the pair [(p, fun () -> r)]. @@ -1203,46 +1261,52 @@ module type S = sig Example: [test_function prot (lookup_student (ty_of_fun_ty prot) name) [1 @: 2 @: 3 @: 4 @:!! 5 ==> 15; ... ==> ...]] *) - val (==>) : 'params -> 'ret -> 'params * (unit -> 'ret) end - (** [r1 @@@ r2] is the function [x -> r1 x @ r2 x]. *) - val (@@@) : - ('a -> Learnocaml_report.t) - -> ('a -> Learnocaml_report.t) - -> ('a -> Learnocaml_report.t) + val ( @@@ ) : + ('a -> Learnocaml_report.t) + -> ('a -> Learnocaml_report.t) + -> 'a + -> Learnocaml_report.t + (** [r1 @@@ r2] is the function [x -> r1 x @ r2 x]. *) - (** [r1 @@> f] returns [r1] if [r1] is a failure report, + val ( @@> ) : + Learnocaml_report.t -> (unit -> Learnocaml_report.t) -> Learnocaml_report.t + (** [r1 @@> f] returns [r1] if [r1] is a failure report, otherwise returns the result of report generator [f ()]. *) - val (@@>) : - Learnocaml_report.t - -> (unit -> Learnocaml_report.t) - -> Learnocaml_report.t - (** [r1 @@= f] returns [r1] if [r1] is a failure report, + val ( @@= ) : + Learnocaml_report.t -> (unit -> Learnocaml_report.t) -> Learnocaml_report.t + (** [r1 @@= f] returns [r1] if [r1] is a failure report, otherwise concatenates [r1] to the result of report generator [f ()]. *) - val (@@=) : - Learnocaml_report.t - -> (unit -> Learnocaml_report.t) - -> Learnocaml_report.t - - (**/**) - include (module type of Ast_checker) - include (module type of Tester) - include (module type of Mutation) - include (module type of Sampler) - include (module type of Test_functions_types) - include (module type of Test_functions_ref_var) - include (module type of Test_functions_function) - include (module type of Test_functions_generic) + + (**/**) + + include module type of Ast_checker + + include module type of Tester + + include module type of Mutation + + include module type of Sampler + + include module type of Test_functions_types + + include module type of Test_functions_ref_var + + include module type of Test_functions_function + + include module type of Test_functions_generic end -module Make : functor - (Params : sig - val results : Learnocaml_report.t option ref - val set_progress : string -> unit - val timeout : int option - module Introspection : Introspection_intf.INTROSPECTION - end) -> S +module Make (Params : sig + val results : Learnocaml_report.t option ref + + val set_progress : string -> unit + + val timeout : int option + + module Introspection : Introspection_intf.INTROSPECTION +end) : S diff --git a/src/main/learnocaml_client.ml b/src/main/learnocaml_client.ml index d4518988c..942129272 100644 --- a/src/main/learnocaml_client.ml +++ b/src/main/learnocaml_client.ml @@ -9,79 +9,80 @@ open Learnocaml_data open Lwt.Infix module Api = Learnocaml_api - open Cmdliner open Arg let version = Api.version let url_conv = - conv ~docv:"URL" ( - (fun s -> - try Ok (Uri.of_string s) - with e -> Error (`Msg (Printexc.to_string e))), - Uri.pp_hum - ) + conv ~docv:"URL" + ( (fun s -> + try Ok (Uri.of_string s) with e -> Error (`Msg (Printexc.to_string e)) + ) + , Uri.pp_hum ) let token_conv = - conv ~docv:"TOKEN" ( - (fun s -> - try Ok (Token.parse s) - with Failure msg -> - Error (`Msg (Printf.sprintf "Invalid token %s: %s" s msg))), - (fun fmt t -> Format.pp_print_string fmt (Token.to_string t)) - ) + conv ~docv:"TOKEN" + ( (fun s -> + try Ok (Token.parse s) with Failure msg -> + Error (`Msg (Printf.sprintf "Invalid token %s: %s" s msg)) ) + , fun fmt t -> Format.pp_print_string fmt (Token.to_string t) ) module Args_global = struct - type t = { - server_url: Uri.t option; - token: Learnocaml_data.student Learnocaml_data.token option; - local: bool; - } + type t = + { server_url : Uri.t option + ; token : Learnocaml_data.student Learnocaml_data.token option + ; local : bool } let server_url = - value & opt (some url_conv) None & - info ["s";"server"] ~docv:"URL" ~doc: - "The URL of the learn-ocaml server." - ~env:(Term.env_info "LEARNOCAML_SERVER" ~doc: - "Sets the learn-ocaml server URL. Overridden by $(b,--server).") + value + & opt (some url_conv) None + & info ["s"; "server"] ~docv:"URL" + ~doc:"The URL of the learn-ocaml server." + ~env: + (Term.env_info "LEARNOCAML_SERVER" + ~doc: + "Sets the learn-ocaml server URL. Overridden by $(b,--server).") let token = - value & opt (some token_conv) None & info ["token";"t"] ~docv:"TOKEN" ~doc: - "Your token on the learn-ocaml server. This is required when submitting \ - solutions or interacting with the server." - ~env:(Term.env_info "LEARNOCAML_TOKEN" ~doc: - "Sets the learn-ocaml user token on the sever. Overridden by \ - $(b,--token).") + value + & opt (some token_conv) None + & info ["token"; "t"] ~docv:"TOKEN" + ~doc: + "Your token on the learn-ocaml server. This is required when \ + submitting solutions or interacting with the server." + ~env: + (Term.env_info "LEARNOCAML_TOKEN" + ~doc: + "Sets the learn-ocaml user token on the sever. Overridden by \ + $(b,--token).") let local = - value & flag & info ["local"] ~doc: - "Use a configuration file local to the current directory, rather \ - than user-wide." + value & flag + & info ["local"] + ~doc: + "Use a configuration file local to the current directory, rather \ + than user-wide." - let apply server_url token local = - {server_url; token; local} + let apply server_url token local = {server_url; token; local} - let term = - Term.(const apply $server_url $token $local) + let term = Term.(const apply $ server_url $ token $ local) - let term_server = - Term.(const (fun x -> x) $ server_url) + let term_server = Term.(const (fun x -> x) $ server_url) end module Args_create_token = struct - type t = { - nickname : string option; - secret : string; - } + type t = {nickname : string option; secret : string} let nickname = - value & pos 0 (some string) None & info [] ~docv:"NICKNAME" ~doc: - "The desired nickname." + value + & pos 0 (some string) None + & info [] ~docv:"NICKNAME" ~doc:"The desired nickname." let secret = - value & pos 1 string "" & info [] ~docv:"SECRET" ~doc: - "The secret. If not provided, use \"\" as a secret." + value & pos 1 string "" + & info [] ~docv:"SECRET" + ~doc:"The secret. If not provided, use \"\" as a secret." let apply nickname secret = {nickname; secret} @@ -90,131 +91,139 @@ end module Args_exercise_id = struct let id = - value & pos 0 (some string) None & info [] ~docv:"ID" ~doc: - "The exercise identifier." + value + & pos 0 (some string) None + & info [] ~docv:"ID" ~doc:"The exercise identifier." let term = Term.(const (fun x -> x) $ id) end module Args_exercises = struct - type t = { - solution_file: string option; - exercise_id: string option; - output_format: [`Console|`Json|`Html|`Raw]; - submit: bool; - color: bool; - verbosity: int; - } + type t = + { solution_file : string option + ; exercise_id : string option + ; output_format : [`Console | `Json | `Html | `Raw] + ; submit : bool + ; color : bool + ; verbosity : int } let solution_file = - value & pos 0 (some file) None & info [] ~docv:"FILE" ~doc: - "The file containing the user's solution to the exercise." + value + & pos 0 (some file) None + & info [] ~docv:"FILE" + ~doc:"The file containing the user's solution to the exercise." let exercise_id = - value & opt (some string) None & info ["id"] ~docv:"ID" ~doc: - "The exercise identifier. If unspecified, this is inferred from the file \ - name." + value + & opt (some string) None + & info ["id"] ~docv:"ID" + ~doc: + "The exercise identifier. If unspecified, this is inferred from the \ + file name." let output_format = - value & vflag `Console & [ - `Console, info ["console"] ~doc: - "output the results to the console. This is the default."; - `Json, info ["json"] ~doc: - "output the results as JSON, to stdout."; - `Html, info ["html"] ~doc: - "output the results as HTML, to the console."; - `Raw, info ["raw"] ~doc: - "output the results as raw text."; - ] + value + & vflag `Console + & [ ( `Console + , info ["console"] + ~doc:"output the results to the console. This is the default." ) + ; (`Json, info ["json"] ~doc:"output the results as JSON, to stdout.") + ; ( `Html + , info ["html"] ~doc:"output the results as HTML, to the console." ) + ; (`Raw, info ["raw"] ~doc:"output the results as raw text.") ] let dont_submit = - value & flag & info ["n";"dry-run"] ~doc: - "Perform the grading locally, but don't submit back to the server." + value & flag + & info ["n"; "dry-run"] + ~doc: + "Perform the grading locally, but don't submit back to the server." let color_when = - let when_enum = ["always", Some true; "never", Some false; "auto", None] in - value & opt (enum when_enum) None & info ["color"] ~docv:"WHEN" ~doc: - ("Colorise the output, and also allows use of UTF-8 characters. $(docv) \ - must be "^doc_alts_enum when_enum ^".") + let when_enum = + [("always", Some true); ("never", Some false); ("auto", None)] + in + value + & opt (enum when_enum) None + & info ["color"] ~docv:"WHEN" + ~doc: + ( "Colorise the output, and also allows use of UTF-8 characters. \ + $(docv) must be " ^ doc_alts_enum when_enum ^ "." ) let verbose = - value & flag_all & info ["v";"verbose"] ~doc: - "Be more verbose. Can be repeated." + value & flag_all + & info ["v"; "verbose"] ~doc:"Be more verbose. Can be repeated." - let apply solution_file exercise_id output_format dont_submit - color_when verbose = - let color = match color_when with + let apply solution_file exercise_id output_format dont_submit color_when + verbose = + let color = + match color_when with | Some o -> o | None -> Unix.(isatty stdout) && Sys.getenv_opt "TERM" <> Some "dumb" in - { - solution_file; - exercise_id; - output_format; - submit = not dont_submit; - color; - verbosity = List.length verbose; - } + { solution_file + ; exercise_id + ; output_format + ; submit = not dont_submit + ; color + ; verbosity = List.length verbose } let term = - Term.(const apply - $solution_file $exercise_id $output_format $dont_submit - $color_when $verbose ) + Term.( + const apply $ solution_file $ exercise_id $ output_format $ dont_submit + $ color_when $ verbose) end module Args_fetch = struct let id = - value & pos_all string [] & info [] ~docv:"EXERCISE_ID" ~doc: - "Exercise identifier. Can be repeated. \ - If not present, all the exercises will be downloaded." + value & pos_all string [] + & info [] ~docv:"EXERCISE_ID" + ~doc: + "Exercise identifier. Can be repeated. If not present, all the \ + exercises will be downloaded." let term = Term.(const (fun x -> x) $ id) end module ConfigFile = struct - - type t = { - server: Uri.t; - token: Token.t option; - } + type t = {server : Uri.t; token : Token.t option} let local_path, user_path = let ( / ) = Filename.concat in - Sys.getcwd () / ".learnocaml-client", - (try Sys.getenv "HOME" with Not_found -> ".") - / ".config" / "learnocaml" / "client.json" + ( Sys.getcwd () / ".learnocaml-client" + , (try Sys.getenv "HOME" with Not_found -> ".") + / ".config" / "learnocaml" / "client.json" ) - let path ?(local=false) () = - if local then - if Sys.file_exists local_path then Some local_path else None - else - List.find_opt Sys.file_exists [local_path; user_path] + let path ?(local = false) () = + if local then if Sys.file_exists local_path then Some local_path else None + else List.find_opt Sys.file_exists [local_path; user_path] let enc = let open Json_encoding in conv - (fun {server; token} -> server, token) - (fun (server, token) -> {server; token}) @@ - obj2 - (req "server" (conv Uri.to_string Uri.of_string string)) - (req "token" (option Token.(conv to_string parse string))) + (fun {server; token} -> (server, token)) + (fun (server, token) -> {server; token}) + @@ obj2 + (req "server" (conv Uri.to_string Uri.of_string string)) + (req "token" (option Token.(conv to_string parse string))) let read file = - Lwt_io.with_file ~mode:Lwt_io.Input file Lwt_io.read >|= - Ezjsonm.from_string >|= - Json_encoding.destruct enc + Lwt_io.with_file ~mode:Lwt_io.Input file Lwt_io.read + >|= Ezjsonm.from_string >|= Json_encoding.destruct enc let write path t = - Lwt_utils.mkdir_p (Filename.dirname path) >>= fun () -> - Lwt_io.(with_file ~mode:Output ~perm:0o600 path) @@ fun oc -> - Json_encoding.construct enc t |> function - | `O _ | `A _ as json -> Lwt_io.write oc (Ezjsonm.to_string json) + Lwt_utils.mkdir_p (Filename.dirname path) + >>= fun () -> + Lwt_io.(with_file ~mode:Output ~perm:0o600 path) + @@ fun oc -> + Json_encoding.construct enc t + |> function + | (`O _ | `A _) as json -> Lwt_io.write oc (Ezjsonm.to_string json) | _ -> assert false end module Console = struct - let enable_colors = ref false + let enable_utf8 = ref false let color cols = @@ -241,54 +250,48 @@ module Console = struct in if !enable_colors then Printf.sprintf "\027[%sm%s\027[m" - (String.concat ";" @@ (List.map code cols)) - else - fun s -> s + (String.concat ";" @@ List.map code cols) + else fun s -> s let status_line s = - if !enable_colors then - (flush stdout; Printf.eprintf "%s..%!\r\027[K" s) - else - Printf.eprintf "%s..\n" s + if !enable_colors then ( + flush stdout; + Printf.eprintf "%s..%!\r\027[K" s ) + else Printf.eprintf "%s..\n" s let utf default c = if !enable_utf8 then c else default - let block ?title ?border_color ?text_color ?(no_open=false) s = + let block ?title ?border_color ?text_color ?(no_open = false) s = let top = utf "+" "\xe2\x94\x8c\xe2\x94\x80" (*U+250C U+2500*) in let left = utf "|" "\xe2\x94\x82" (*U+2502*) in let bottom = utf "`" "\xe2\x94\x94\xe2\x94\x80" (*U+2514 U+2500*) in - let buf = Buffer.create (String.length s + String.length s / 10) in - let oc = function None -> (fun s -> s) | Some col -> color col in - if not no_open then - Buffer.add_string buf (oc border_color top); - (match title with - | None -> () - | Some s -> - match String.split_on_char '\n' s with - | s1::r -> - if not no_open then - Buffer.add_string buf " "; - Buffer.add_string buf s1; - Buffer.add_char buf '\n'; - List.iter - (fun si -> - if not no_open then - (Buffer.add_string buf left; - Buffer.add_string buf " "); - Buffer.add_string buf si; - Buffer.add_char buf '\n') - r; - if not no_open then - Buffer.add_string buf left; - | [] -> ()); - if not no_open then - Buffer.add_char buf '\n'; + let buf = Buffer.create (String.length s + (String.length s / 10)) in + let oc = function None -> fun s -> s | Some col -> color col in + if not no_open then Buffer.add_string buf (oc border_color top); + ( match title with + | None -> () + | Some s -> ( + match String.split_on_char '\n' s with + | s1 :: r -> + if not no_open then Buffer.add_string buf " "; + Buffer.add_string buf s1; + Buffer.add_char buf '\n'; + List.iter + (fun si -> + if not no_open then ( + Buffer.add_string buf left; Buffer.add_string buf " " ); + Buffer.add_string buf si; + Buffer.add_char buf '\n' ) + r; + if not no_open then Buffer.add_string buf left + | [] -> () ) ); + if not no_open then Buffer.add_char buf '\n'; List.iter (fun s -> - Buffer.add_string buf (oc border_color left); - Buffer.add_string buf " "; - Buffer.add_string buf (oc text_color s); - Buffer.add_char buf '\n') + Buffer.add_string buf (oc border_color left); + Buffer.add_string buf " "; + Buffer.add_string buf (oc text_color s); + Buffer.add_char buf '\n' ) (String.split_on_char '\n' s); Buffer.add_string buf (oc border_color bottom); Buffer.add_char buf '\n'; @@ -299,11 +302,13 @@ module Console = struct let right = utf ">" "\xe2\x9d\xb1" (*U+2771*) in color [`Bg col] left ^ color [`Bg col] s ^ color [`Bg col] right - let hline ?(width=80) () = + let hline ?(width = 80) () = let c = utf "-" "\xe2\x94\x80" (*U+2500*) in let ln = String.length c in - let b = Bytes.create (width * ln + 1) in - for i = 0 to width - 1 do String.blit c 0 b (i * ln) ln done; + let b = Bytes.create ((width * ln) + 1) in + for i = 0 to width - 1 do + String.blit c 0 b (i * ln) ln + done; Bytes.set b (width * ln) '\n'; Bytes.to_string b @@ -316,81 +321,88 @@ module Console = struct Printf.eprintf "I beg you pardon? %!"; input ?default parse in - try match read_line () with + try + match read_line () with | "" -> on_empty () - | s -> - try parse s with Failure msg -> - Printf.eprintf "Invalid input: %s\nPlease try again: %!" msg; - input ?default parse + | s -> ( + try parse s with Failure msg -> + Printf.eprintf "Invalid input: %s\nPlease try again: %!" msg; + input ?default parse ) with | End_of_file -> prerr_newline (); on_empty () | Sys.Break as e -> prerr_newline (); raise e - let yesno ?(default=false) () = - input ~default (fun s -> match String.lowercase_ascii s with + let yesno ?(default = false) () = + input ~default (fun s -> + match String.lowercase_ascii s with | "y" | "yes" -> true | "n" | "no" -> false - | _ -> failwith "please answer 'y' or 'n'.") - + | _ -> failwith "please answer 'y' or 'n'." ) end let get_score = let open Learnocaml_report in let rec get_score report = - List.fold_left (fun acc -> function - | Section (_text, report) -> get_score acc report + List.fold_left + (fun acc -> function Section (_text, report) -> get_score acc report | SectionMin (_text, report, min) -> get_score acc report |> max min - | Message (_text, status) -> match status with + | Message (_text, status) -> ( + match status with | Success i -> acc + i | Penalty i -> acc - i - | _ -> acc) + | _ -> acc ) ) report in get_score 0 let max_score exo = Learnocaml_exercise.(access File.max_score exo) -let print_score ?(max=1) ?color i = - let color = match color with +let print_score ?(max = 1) ?color i = + let color = + match color with | None -> if i <= 0 then `Red else if i >= max then `Green else `Yellow | Some c -> c in - if i <= 1 then - Console.button color (Printf.sprintf " %3d pt " i) - else - Console.button color (Printf.sprintf " %3d pts " i) + if i <= 1 then Console.button color (Printf.sprintf " %3d pt " i) + else Console.button color (Printf.sprintf " %3d pts " i) -let console_report ?(verbose=false) ex report = +let console_report ?(verbose = false) ex report = let open Console in let open Learnocaml_report in let score = get_score report in let max_score = max_score ex in print_string (hline ()); - Printf.printf - "## %-*s %s\n" + Printf.printf "## %-*s %s\n" (65 + String.length (color [`Bold] "")) (color [`Bold] - (if score <= 0 then "Exercise failed" - else if score >= max_score then "Exercise complete" - else Printf.sprintf "Exercise incomplete (%02d%%)" (100 * score / max_score))) + ( if score <= 0 then "Exercise failed" + else if score >= max_score then "Exercise complete" + else + Printf.sprintf "Exercise incomplete (%02d%%)" (100 * score / max_score) + )) (print_score ~max:max_score score); print_string (hline ()); print_newline (); let format_text t = - String.concat " " @@ List.map (function - | Text w -> w - | Break -> "\n" - | Code s when String.contains s '\n' -> "\n"^block ~border_color:[`Cyan] s - | Code s -> color [`Cyan] s - | Output s -> block ~border_color:[`Yellow] s) - t + String.concat " " + @@ List.map + (function + | Text w -> w + | Break -> "\n" + | Code s when String.contains s '\n' -> + "\n" ^ block ~border_color:[`Cyan] s + | Code s -> color [`Cyan] s + | Output s -> block ~border_color:[`Yellow] s) + t in let rec all_good report = - (List.for_all @@ function - | Section (_, report) | SectionMin (_, report, _) -> all_good report - | Message (_, (Success _ | Penalty _ - | Informative | Warning | Important)) -> true - | Message (_, Failure) -> false) + ( List.for_all + @@ function + | Section (_, report) | SectionMin (_, report, _) -> all_good report + | Message (_, (Success _ | Penalty _ | Informative | Warning | Important)) + -> + true + | Message (_, Failure) -> false ) report in let rec format_item = function @@ -403,23 +415,19 @@ let console_report ?(verbose=false) ex report = in print_score ~color score ^ " " ^ format_text text in - if not verbose && all_good report then - title + if (not verbose) && all_good report then title else - "\n" ^ block ~title ~no_open:true - (String.concat "\n" @@ List.map format_item report) - | Message (text, Success i) -> - print_score i ^ " " ^ format_text text - | Message (text, Penalty i) -> - print_score (-i) ^ " " ^ format_text text - | Message (text, Failure) -> - print_score 0 ^ " " ^ format_text text + "\n" + ^ block ~title ~no_open:true + (String.concat "\n" @@ List.map format_item report) + | Message (text, Success i) -> print_score i ^ " " ^ format_text text + | Message (text, Penalty i) -> print_score (-i) ^ " " ^ format_text text + | Message (text, Failure) -> print_score 0 ^ " " ^ format_text text | Message (text, Warning) -> color [`Bg `Yellow] "[ warning ]" ^ " " ^ format_text text - | Message (text, Informative) -> - format_text text + | Message (text, Informative) -> format_text text | Message (text, Important) -> - color [`Bg `Cyan] "[important]" ^ " " ^ format_text text + color [`Bg `Cyan] "[important]" ^ " " ^ format_text text in List.iter (fun i -> print_endline (format_item i)) report; print_newline () @@ -435,32 +443,30 @@ let fetch server_url req = let open Cohttp in let open Cohttp_lwt_unix in let do_req = function - | { Learnocaml_api.meth = `GET; path; args } -> - Client.get (url path args) - | { Learnocaml_api.meth = `POST body; path; args } -> + | {Learnocaml_api.meth = `GET; path; args} -> Client.get (url path args) + | {Learnocaml_api.meth = `POST body; path; args} -> Client.post ~body:(Cohttp_lwt.Body.of_string body) (url path args) in Api_client.make_request (fun http_request -> - do_req http_request >>= function - | {Response.status = `OK; _}, body -> - Cohttp_lwt.Body.to_string body >|= fun s -> Ok s - | {Response.status = `Not_found; _}, _ -> - Lwt.return (Error `Not_found) - | {Response.status; _}, _ -> - Lwt.return (Error (`Failure (Code.string_of_status status)))) + do_req http_request + >>= function + | {Response.status = `OK; _}, body -> + Cohttp_lwt.Body.to_string body >|= fun s -> Ok s + | {Response.status = `Not_found; _}, _ -> Lwt.return (Error `Not_found) + | {Response.status; _}, _ -> + Lwt.return (Error (`Failure (Code.string_of_status status))) ) req >>= function | Ok x -> Lwt.return x | Error `Not_found -> raise Not_found - | Error (`Failure s) -> Lwt.fail_with ("Server request failed: "^ s) + | Error (`Failure s) -> Lwt.fail_with ("Server request failed: " ^ s) let fetch_exercise server_url token id = Lwt.catch (fun () -> fetch server_url (Api.Exercise (token, id))) @@ function | Not_found -> - Printf.ksprintf Lwt.fail_with - "Exercise %S was not found on the server." + Printf.ksprintf Lwt.fail_with "Exercise %S was not found on the server." id | e -> Lwt.fail e @@ -478,21 +484,17 @@ let upload_report server token ex solution report = let id = Learnocaml_exercise.(access File.id ex) in let mtime = Unix.gettimeofday () in let exercise_state = - { Answer. - solution; - grade = if max_score = 0 then None else Some (score * 100 / max_score); - report = Some report; - mtime; - } + { Answer.solution + ; grade = (if max_score = 0 then None else Some (score * 100 / max_score)) + ; report = Some report + ; mtime } in let new_save = - { Save. - nickname = ""; - all_exercise_editors = SMap.empty; - all_exercise_states = SMap.singleton id exercise_state; - all_toplevel_histories = SMap.empty; - all_exercise_toplevel_histories = SMap.empty; - } + { Save.nickname = "" + ; all_exercise_editors = SMap.empty + ; all_exercise_states = SMap.singleton id exercise_state + ; all_toplevel_histories = SMap.empty + ; all_exercise_toplevel_histories = SMap.empty } in Lwt.catch (fun () -> upload_save server token new_save) @@ function @@ -502,47 +504,44 @@ let upload_report server token ex solution report = (Token.to_string token) | e -> Lwt.fail e -let check_server_version ?(allow_static=false) server = +let check_server_version ?(allow_static = false) server = Lwt.catch (fun () -> - fetch server (Api.Version ()) >|= fun (server_version,_) -> - if server_version <> Api.version then - (Printf.eprintf "API version mismatch: client v.%s and server v.%s\n" - Api.version server_version; - exit 1) - else - true) + fetch server (Api.Version ()) + >|= fun (server_version, _) -> + if server_version <> Api.version then ( + Printf.eprintf "API version mismatch: client v.%s and server v.%s\n" + Api.version server_version; + exit 1 ) + else true ) @@ fun e -> - if not allow_static then - begin - Printf.eprintf "[ERROR] Could not reach server: %s\n" - (match e with - | Unix.Unix_error (err, _, _) -> Unix.error_message err - | Failure m -> m - | e -> Printexc.to_string e); - exit 1 - end - else - Lwt.return_false + if not allow_static then ( + Printf.eprintf "[ERROR] Could not reach server: %s\n" + ( match e with + | Unix.Unix_error (err, _, _) -> Unix.error_message err + | Failure m -> m + | e -> Printexc.to_string e ); + exit 1 ) + else Lwt.return_false let get_server = let default_server = Uri.of_string "http://learn-ocaml.org" in function | Some s -> s | None -> - Printf.eprintf - "Please specify the address of the learn-ocaml server to use \ - [default: %s]: " (Uri.to_string default_server); - let uri s = - let u = Uri.of_string s in - match Uri.scheme u with - | None -> Uri.with_scheme u (Some "http") - | Some ("http" (* | "https" *)) -> u - | Some s -> - failwith (Printf.sprintf - "unsupported scheme %S, please use http://." - s) - in - Console.input ~default:default_server uri + Printf.eprintf + "Please specify the address of the learn-ocaml server to use \ + [default: %s]: " + (Uri.to_string default_server); + let uri s = + let u = Uri.of_string s in + match Uri.scheme u with + | None -> Uri.with_scheme u (Some "http") + | Some "http" (* | "https" *) -> u + | Some s -> + failwith + (Printf.sprintf "unsupported scheme %S, please use http://." s) + in + Console.input ~default:default_server uri let get_nonce_and_create_token server nickname secret_candidate = let secret_candidate = Sha.sha512 secret_candidate in @@ -551,112 +550,128 @@ let get_nonce_and_create_token server nickname secret_candidate = fetch server (Api.Create_token (Sha.sha512 (nonce ^ secret_candidate), None, nickname)) -let get_config_option ?local ?(save_back=false) ?(allow_static=false) server_opt token_opt = +let get_config_option ?local ?(save_back = false) ?(allow_static = false) + server_opt token_opt = match ConfigFile.path ?local () with | Some f -> - ConfigFile.read f >>= fun c -> - let c = match server_opt, token_opt with - | Some server, Some _ -> { ConfigFile.server=server; ConfigFile.token=token_opt } - | Some server, None -> { c with ConfigFile.server } - | None, Some _ -> { c with ConfigFile.token=token_opt} + ConfigFile.read f + >>= fun c -> + let c = + match (server_opt, token_opt) with + | Some server, Some _ -> + {ConfigFile.server; ConfigFile.token = token_opt} + | Some server, None -> {c with ConfigFile.server} + | None, Some _ -> {c with ConfigFile.token = token_opt} | None, None -> c in check_server_version ~allow_static c.ConfigFile.server >>= fun _ -> - ( - if save_back - then - ConfigFile.write f c >|= fun () -> - Printf.eprintf "Configuration written to %s\n%!" f - else - Lwt.return_unit - ) + ( if save_back then + ConfigFile.write f c + >|= fun () -> Printf.eprintf "Configuration written to %s\n%!" f + else Lwt.return_unit ) >|= fun () -> Some c | None -> Lwt.return_none -let get_config ?local ?(save_back=false) ?(allow_static=false) server_opt token_opt = +let get_config ?local ?(save_back = false) ?(allow_static = false) server_opt + token_opt = get_config_option ?local ~save_back ~allow_static server_opt token_opt >>= function | Some c -> Lwt.return c - | None -> Lwt.fail_with "No config file found. Please do `learn-ocaml-client init`" - -let man p = [ - `S "DESCRIPTION"; - `P p; - `S "OPTIONS"; - `S "AUTHORS"; - `P "Learn OCaml is written by OCamlPro. Its main authors are Benjamin Canou, \ - Çağdaş Bozman, Grégoire Henry and Louis Gesbert. It is licensed under \ - the MIT License."; - `S "BUGS"; - `P "Bugs should be reported to \ - $(i,https://github.com/ocaml-sf/learn-ocaml/issues)"; - ] - -let get_config_o ?save_back ?(allow_static=false) o = + | None -> + Lwt.fail_with "No config file found. Please do `learn-ocaml-client init`" + +let man p = + [ `S "DESCRIPTION" + ; `P p + ; `S "OPTIONS" + ; `S "AUTHORS" + ; `P + "Learn OCaml is written by OCamlPro. Its main authors are Benjamin \ + Canou, Çağdaş Bozman, Grégoire Henry and Louis Gesbert. It is \ + licensed under the MIT License." + ; `S "BUGS" + ; `P + "Bugs should be reported to \ + $(i,https://github.com/ocaml-sf/learn-ocaml/issues)" ] + +let get_config_o ?save_back ?(allow_static = false) o = let open Args_global in get_config ~local:o.local ?save_back ~allow_static o.server_url o.token module Init = struct open Args_global open Args_create_token - + let init global_args create_token_args = - let path = if global_args.local then ConfigFile.local_path else ConfigFile.user_path in + let path = + if global_args.local then ConfigFile.local_path else ConfigFile.user_path + in let get_token server = match global_args.token with | Some token -> Lwt.return token - | None -> - match create_token_args.nickname with - | Some n -> (get_nonce_and_create_token server (Some n) create_token_args.secret) - | _ -> Lwt.fail_with "You must provide a token or a nickname+secret pair." + | None -> ( + match create_token_args.nickname with + | Some n -> + get_nonce_and_create_token server (Some n) create_token_args.secret + | _ -> + Lwt.fail_with "You must provide a token or a nickname+secret pair." + ) in let get_server () = match global_args.server_url with | None -> Lwt.fail_with "You must provide a server." | Some s -> Lwt.return s in - get_server () >>= fun server -> - check_server_version ~allow_static:true server >>= fun has_server -> - let token = if has_server then - get_token server >>= Lwt.return_some - else - Lwt.return_none in - token >>= fun token -> - let config = { ConfigFile. server; token=token } in - ConfigFile.write path config >|= fun () -> + get_server () + >>= fun server -> + check_server_version ~allow_static:true server + >>= fun has_server -> + let token = + if has_server then get_token server >>= Lwt.return_some + else Lwt.return_none + in + token + >>= fun token -> + let config = {ConfigFile.server; token} in + ConfigFile.write path config + >|= fun () -> Printf.eprintf "Configuration written to %s.\n%!" path; 0 - let man = man "Initialize the configuration file with the server, and \ - a token or a nickname+secret pair" + let man = + man + "Initialize the configuration file with the server, and a token or a \ + nickname+secret pair" let cmd = - Term.( - const (fun go co -> Pervasives.exit (Lwt_main.run (init go co))) - $ Args_global.term $ Args_create_token.term), - Term.info ~man - ~doc:"Initialize the configuration file." - "init" + ( Term.( + const (fun go co -> Pervasives.exit (Lwt_main.run (init go co))) + $ Args_global.term $ Args_create_token.term) + , Term.info ~man ~doc:"Initialize the configuration file." "init" ) end - + module Grade = struct open Args_exercises + let grade go eo = Console.enable_colors := eo.color; Console.enable_utf8 := eo.color; get_config_o ~allow_static:true go - >>= fun { ConfigFile.server; token } -> + >>= fun {ConfigFile.server; token} -> let status_line = - if eo.verbosity >= 2 then Printf.eprintf "%s..\n" else Console.status_line + if eo.verbosity >= 2 then Printf.eprintf "%s..\n" + else Console.status_line in let solution, exercise_id = - match eo.solution_file, eo.exercise_id with - | None, _ -> Printf.eprintf "You must specify a file to grade.\n%!"; exit 2 - | Some f, Some id -> f, id + match (eo.solution_file, eo.exercise_id) with + | None, _ -> + Printf.eprintf "You must specify a file to grade.\n%!"; + exit 2 + | Some f, Some id -> (f, id) | Some f, None -> - let id = Filename.remove_extension f in - f, id + let id = Filename.remove_extension f in + (f, id) in status_line "Reading solution."; Lwt_io.with_file ~mode:Lwt_io.Input solution Lwt_io.read @@ -667,8 +682,7 @@ module Grade = struct if deadline = Some 0. then Printf.eprintf "[ERROR] The deadline is expired, you won't be able to submit.\n"; - Grading_cli.get_grade ~callback:status_line ?timeout:None - exercise solution + Grading_cli.get_grade ~callback:status_line ?timeout:None exercise solution >>= fun (report, ex_stdout, ex_stderr, ex_outcome) -> flush stderr; let pr col title s = @@ -683,61 +697,59 @@ module Grade = struct if eo.verbosity >= 1 then prerr_newline (); match report with | Error e -> - let str = - match Grading.string_of_exn e with - | Some s -> s - | None -> Printexc.to_string e - in - Printf.eprintf "[ERROR] Could not do the grading:\n%s\n" str; - Lwt.return 10 - | Ok report -> - (match eo.output_format with - | `Console -> console_report ~verbose:(eo.verbosity > 0) exercise report - | `Raw -> - Report.print Format.std_formatter report - | `Html -> - Report.output_html Format.std_formatter report - | `Json -> - match Json_encoding.construct Report.enc report - with - | `O _ | `A _ as json -> Ezjsonm.to_channel ~minify:false stdout json - | _ -> assert false); - if deadline = Some 0. then - (Printf.eprintf "Results NOT saved to server (deadline expired)\n"; - Lwt.return 1) - else - match token with - | Some token -> - upload_report server token exercise solution report >>= fun _ -> - Printf.eprintf "Results saved to server\n"; Lwt.return 0 - | None -> Lwt.return 0 + let str = + match Grading.string_of_exn e with + | Some s -> s + | None -> Printexc.to_string e + in + Printf.eprintf "[ERROR] Could not do the grading:\n%s\n" str; + Lwt.return 10 + | Ok report -> ( + ( match eo.output_format with + | `Console -> + console_report ~verbose:(eo.verbosity > 0) exercise report + | `Raw -> Report.print Format.std_formatter report + | `Html -> Report.output_html Format.std_formatter report + | `Json -> ( + match Json_encoding.construct Report.enc report with + | (`O _ | `A _) as json -> + Ezjsonm.to_channel ~minify:false stdout json + | _ -> assert false ) ); + if deadline = Some 0. then ( + Printf.eprintf "Results NOT saved to server (deadline expired)\n"; + Lwt.return 1 ) + else + match token with + | Some token -> + upload_report server token exercise solution report + >>= fun _ -> + Printf.eprintf "Results saved to server\n"; + Lwt.return 0 + | None -> Lwt.return 0 ) let man = man "Grades an OCaml exercise using a learn-ocaml server, and submits \ - solutions." + solutions." let cmd = - Term.( - const (fun go eo -> Pervasives.exit (Lwt_main.run (grade go eo))) - $ Args_global.term $ Args_exercises.term), - Term.info ~man - ~doc:"Learn-ocaml grading client." - "grade" + ( Term.( + const (fun go eo -> Pervasives.exit (Lwt_main.run (grade go eo))) + $ Args_global.term $ Args_exercises.term) + , Term.info ~man ~doc:"Learn-ocaml grading client." "grade" ) end let use_global f = Term.( - const (fun o -> Pervasives.exit (Lwt_main.run (f o))) - $ Args_global.term) + const (fun o -> Pervasives.exit (Lwt_main.run (f o))) $ Args_global.term) module Print_token = struct let print_tok o = get_config_o o >>= fun config -> - (match config.ConfigFile.token with - | Some token -> Lwt_io.print (Token.to_string token ^ "\n") - | None -> Lwt_io.print "Static server -- no token\n") + ( match config.ConfigFile.token with + | Some token -> Lwt_io.print (Token.to_string token ^ "\n") + | None -> Lwt_io.print "Static server -- no token\n" ) >|= fun () -> 0 let explanation = "Just print the configured user token." @@ -745,31 +757,26 @@ module Print_token = struct let man = man explanation let cmd = - use_global print_tok, - Term.info ~man ~doc:explanation "print-token" + (use_global print_tok, Term.info ~man ~doc:explanation "print-token") end module Print_server = struct let print_server o = get_config_o o >>= fun config -> - Lwt_io.printl (Uri.to_string config.ConfigFile.server) - >|= fun () -> 0 - + Lwt_io.printl (Uri.to_string config.ConfigFile.server) >|= fun () -> 0 + let explanation = "Just print the configured server." - + let man = man explanation - + let cmd = - use_global print_server, - Term.info ~man ~doc:explanation "print-server" - + (use_global print_server, Term.info ~man ~doc:explanation "print-server") end - + module Set_options = struct let set_opts o = - get_config_o ~save_back:true ~allow_static:true o - >|= fun _ -> 0 + get_config_o ~save_back:true ~allow_static:true o >|= fun _ -> 0 let man = man @@ -777,77 +784,76 @@ module Set_options = struct ($(b,--server), $(b,--token))." let cmd = - use_global set_opts, - Term.info ~man - ~doc:"Set configuration." - "set-options" + ( use_global set_opts + , Term.info ~man ~doc:"Set configuration." "set-options" ) end let write_exercise_file id str = let f = Filename.concat (Sys.getcwd ()) (id ^ ".ml") in - if Sys.file_exists f then - (Printf.eprintf "File %s already exists, not overwriting.\n" f; - Lwt.return false) + if Sys.file_exists f then ( + Printf.eprintf "File %s already exists, not overwriting.\n" f; + Lwt.return false ) else - Lwt_io.(with_file ~mode:Output ~perm:0o600 f) @@ fun oc -> - Lwt_io.write oc str >|= fun () -> - Printf.printf "Wrote file %s\n%!" f; - true + Lwt_io.(with_file ~mode:Output ~perm:0o600 f) + @@ fun oc -> + Lwt_io.write oc str + >|= fun () -> + Printf.printf "Wrote file %s\n%!" f; + true module Fetch = struct let fetch_save server_url token = Lwt.catch (fun () -> fetch server_url (Api.Fetch_save token)) @@ function - | Not_found -> - Printf.ksprintf Lwt.fail_with - "Token %S not found on the server." - (Token.to_string token) - | e -> Lwt.fail e + | Not_found -> + Printf.ksprintf Lwt.fail_with "Token %S not found on the server." + (Token.to_string token) + | e -> Lwt.fail e let write_save_files lst save = let has_to_fetch x = (* When no exercise identifier was specified, fetch everything *) - match lst with - | [] -> true - | _ -> List.mem x lst + match lst with [] -> true | _ -> List.mem x lst in let already_exists = ref 0 in - Lwt_list.fold_left_s (fun acc (id, st) -> - if not (has_to_fetch id) then Lwt.return acc - else - let f = Filename.concat (Sys.getcwd ()) (id ^ ".ml") in - (if Sys.file_exists f then - (Printf.eprintf "File %s already exists, not overwriting.\n" f; - already_exists := !already_exists + 1; - Lwt.return_unit) - else - Lwt_io.(with_file ~mode:Output ~perm:0o600 f) @@ fun oc -> - Lwt_io.write oc st.Answer.solution >|= fun () -> - Printf.eprintf "Wrote file %s\n%!" f) - >|= fun () -> id::acc ) + Lwt_list.fold_left_s + (fun acc (id, st) -> + if not (has_to_fetch id) then Lwt.return acc + else + let f = Filename.concat (Sys.getcwd ()) (id ^ ".ml") in + ( if Sys.file_exists f then ( + Printf.eprintf "File %s already exists, not overwriting.\n" f; + already_exists := !already_exists + 1; + Lwt.return_unit ) + else + Lwt_io.(with_file ~mode:Output ~perm:0o600 f) + @@ fun oc -> + Lwt_io.write oc st.Answer.solution + >|= fun () -> Printf.eprintf "Wrote file %s\n%!" f ) + >|= fun () -> id :: acc ) [] - (SMap.bindings (save.Save.all_exercise_states)) + (SMap.bindings save.Save.all_exercise_states) >>= fun actually_found -> - let not_found = List.filter (fun x -> not (List.mem x actually_found)) lst in + let not_found = + List.filter (fun x -> not (List.mem x actually_found)) lst + in Lwt_list.iter_s - (Lwt_io.eprintf - ("Warning: exercise %s was not found on the server.\n")) + (Lwt_io.eprintf "Warning: exercise %s was not found on the server.\n") not_found >|= fun () -> let first = if !already_exists = 0 then 0 else 1 in let second = if List.length not_found = 0 then 0 else 1 in - first + 2*second + first + (2 * second) let fetch o lst = get_config_o o - >>= fun { ConfigFile.server; token } -> + >>= fun {ConfigFile.server; token} -> match token with | Some token -> fetch_save server token >>= write_save_files lst | None -> Lwt.return 0 let man = - man - "Fetch the user's solutions on the server to the current directory." + man "Fetch the user's solutions on the server to the current directory." let exits = let open Term in @@ -857,12 +863,10 @@ module Fetch = struct ; exit_info ~doc:"Both of 1 and 2." 3 ] let cmd = - Term.( - const (fun o l -> Pervasives.exit (Lwt_main.run (fetch o l))) - $ Args_global.term $ Args_fetch.term), - Term.info ~man ~exits - ~doc:"Fetch the user's solutions." - "fetch" + ( Term.( + const (fun o l -> Pervasives.exit (Lwt_main.run (fetch o l))) + $ Args_global.term $ Args_fetch.term) + , Term.info ~man ~exits ~doc:"Fetch the user's solutions." "fetch" ) end module Create_token = struct @@ -870,31 +874,29 @@ module Create_token = struct let create_tok server_url co = match co.nickname with - | None -> Lwt_io.print "You must provide a nickname\n" - >|= fun () -> 2 + | None -> Lwt_io.print "You must provide a nickname\n" >|= fun () -> 2 | Some nickname -> - get_config_option server_url None - >>= fun config -> - let server = - match config with - | Some c -> c.ConfigFile.server - | None -> get_server server_url - in - get_nonce_and_create_token server (Some nickname) co.secret - >>= fun tok -> - Lwt_io.print (Token.to_string tok ^ "\n") - >|= fun () -> 0 - - let man = man "Create a token on the server with the desired nickname.\ - Prodiving a token will test if it exists on the server." + get_config_option server_url None + >>= fun config -> + let server = + match config with + | Some c -> c.ConfigFile.server + | None -> get_server server_url + in + get_nonce_and_create_token server (Some nickname) co.secret + >>= fun tok -> + Lwt_io.print (Token.to_string tok ^ "\n") >|= fun () -> 0 + + let man = + man + "Create a token on the server with the desired nickname.Prodiving a \ + token will test if it exists on the server." let cmd = - Term.( - const (fun go co -> Pervasives.exit (Lwt_main.run (create_tok go co))) - $ Args_global.term_server $ Args_create_token.term), - Term.info ~man - ~doc:"Create a token." - "create-token" + ( Term.( + const (fun go co -> Pervasives.exit (Lwt_main.run (create_tok go co))) + $ Args_global.term_server $ Args_create_token.term) + , Term.info ~man ~doc:"Create a token." "create-token" ) end module Template = struct @@ -902,81 +904,70 @@ module Template = struct let template o exercise_id = match exercise_id with - | None -> Lwt.fail_with "You must provide an exercise id" - >|= fun () -> 2 - | Some exercise_id -> - get_config_o ~allow_static:true o - >>= fun { server; token } -> - fetch_exercise server token exercise_id - >>= fun (_meta, exercise, _deadline) -> - write_exercise_file - exercise_id - Learnocaml_exercise.(access File.template exercise) - >|= function - | true -> 0 - | false -> 3 + | None -> Lwt.fail_with "You must provide an exercise id" >|= fun () -> 2 + | Some exercise_id -> ( + get_config_o ~allow_static:true o + >>= fun {server; token} -> + fetch_exercise server token exercise_id + >>= fun (_meta, exercise, _deadline) -> + write_exercise_file exercise_id + Learnocaml_exercise.(access File.template exercise) + >|= function true -> 0 | false -> 3 ) let man = man "Get the template of a given exercise" let cmd = - Term.( - const (fun o id -> Pervasives.exit (Lwt_main.run (template o id))) - $ Args_global.term $ Args_exercise_id.term), - Term.info ~man - ~doc:"Get the template of a given exercise." - "template" + ( Term.( + const (fun o id -> Pervasives.exit (Lwt_main.run (template o id))) + $ Args_global.term $ Args_exercise_id.term) + , Term.info ~man ~doc:"Get the template of a given exercise." "template" ) end - + module Exercise_list = struct - let doc= "Get a structured json containing a list of the exercises of the server" + let doc = + "Get a structured json containing a list of the exercises of the server" - let exercise_list o = + let exercise_list o = get_config_o ~allow_static:true o - >>= fun {ConfigFile.server;token} -> + >>= fun {ConfigFile.server; token} -> fetch server (Learnocaml_api.Exercise_index token) - >>= (fun index-> + >>= fun index -> let open Json_encoding in - let ezjsonm = (Json_encoding.construct - (tup2 Exercise.Index.enc (assoc float)) - index) + let ezjsonm = + Json_encoding.construct (tup2 Exercise.Index.enc (assoc float)) index in let json = - match ezjsonm with - | `O _ | `A _ as json -> json - | _ -> assert false + match ezjsonm with (`O _ | `A _) as json -> json | _ -> assert false in Ezjsonm.to_channel ~minify:false stdout json; - Lwt.return 0;) + Lwt.return 0 let man = man doc - - let cmd = - use_global exercise_list, - Term.info ~man ~doc:doc "exercise-list" + + let cmd = (use_global exercise_list, Term.info ~man ~doc "exercise-list") end - + module Main = struct - let man = - man - "Learn-ocaml-client, default command is grade." + let man = man "Learn-ocaml-client, default command is grade." - let cmd = fst Grade.cmd, - Term.info ~version ~man - ~doc:"Learn-ocaml grading client." - "learn-ocaml-client" + let cmd = + ( fst Grade.cmd + , Term.info ~version ~man ~doc:"Learn-ocaml grading client." + "learn-ocaml-client" ) end let () = - match Term.eval_choice ~catch:false Main.cmd - [ Init.cmd - ; Grade.cmd - ; Print_token.cmd - ; Set_options.cmd - ; Fetch.cmd - ; Print_server.cmd - ; Template.cmd - ; Create_token.cmd - ; Exercise_list.cmd] + match + Term.eval_choice ~catch:false Main.cmd + [ Init.cmd + ; Grade.cmd + ; Print_token.cmd + ; Set_options.cmd + ; Fetch.cmd + ; Print_server.cmd + ; Template.cmd + ; Create_token.cmd + ; Exercise_list.cmd ] with | exception Failure msg -> Printf.eprintf "[ERROR] %s\n" msg; diff --git a/src/main/learnocaml_main.ml b/src/main/learnocaml_main.ml index 859cfaad6..dab19c88c 100644 --- a/src/main/learnocaml_main.ml +++ b/src/main/learnocaml_main.ml @@ -7,8 +7,7 @@ * included LICENSE file for details. *) open Lwt.Infix - -module StringSet = Set.Make(String) +module StringSet = Set.Make (String) let ( / ) = Filename.concat @@ -20,8 +19,9 @@ let readlink f = Filename.concat (Sys.getcwd ()) (Filename.basename f) with Sys_error _ -> f in - try Sys.chdir cwd; f - with Sys_error _ -> Sys.chdir (Filename.get_temp_dir_name ()); f + try Sys.chdir cwd; f with Sys_error _ -> + Sys.chdir (Filename.get_temp_dir_name ()); + f module Args = struct open Cmdliner @@ -30,99 +30,116 @@ module Args = struct type command = Grade | Build | Serve let commands = - value & pos_all (Arg.enum [ - "grade", Grade; - "build", Build; - "serve", Serve; - ]) [Build; Serve] & - info [] ~docs:"COMMANDS" ~docv:"COMMAND" + value + & pos_all + (Arg.enum [("grade", Grade); ("build", Build); ("serve", Serve)]) + [Build; Serve] + & info [] ~docs:"COMMANDS" ~docv:"COMMAND" let repo_dir = - value & opt dir "." & info ["repo"] ~docv:"DIR" ~doc: - "The path to the repository containing the exercises, lessons and \ - tutorials." + value & opt dir "." + & info ["repo"] ~docv:"DIR" + ~doc: + "The path to the repository containing the exercises, lessons and \ + tutorials." let app_dir = - value & opt string "./www" & info ["app-dir"; "o"] ~docv:"DIR" ~doc: - "Directory where the app should be generated for the $(i,build) command, \ - and from where it is served by the $(i,serve) command." + value & opt string "./www" + & info ["app-dir"; "o"] ~docv:"DIR" + ~doc: + "Directory where the app should be generated for the $(i,build) \ + command, and from where it is served by the $(i,serve) command." let base_url = - value & opt string "" & - info ["base-url"] ~docv:"BASE_URL" ~env:(Arg.env_var "LEARNOCAML_BASE_URL") ~doc: - "Set the base URL of the website. \ - Should not end with a trailing slash. \ - Currently, this has no effect on the backend - '$(b,learn-ocaml serve)'. \ - Mandatory for '$(b,learn-ocaml build)' if the site is not hosted in path '/', \ - which typically occurs for static deployment." + value & opt string "" + & info ["base-url"] ~docv:"BASE_URL" + ~env:(Arg.env_var "LEARNOCAML_BASE_URL") + ~doc: + "Set the base URL of the website. Should not end with a trailing \ + slash. Currently, this has no effect on the backend - \ + '$(b,learn-ocaml serve)'. Mandatory for '$(b,learn-ocaml build)' \ + if the site is not hosted in path '/', which typically occurs for \ + static deployment." module Grader = struct let info = info ~docs:"GRADER OPTIONS" let exercises = - value & opt_all (list dir) [["."]] & info ["exercises";"e"] ~docv:"DIRS" ~doc: - "Directories where to find the exercises to be graded \ - (comma-separated). Can be repeated." + value + & opt_all (list dir) [["."]] + & info ["exercises"; "e"] ~docv:"DIRS" + ~doc: + "Directories where to find the exercises to be graded \ + (comma-separated). Can be repeated." let output_json = - value & opt (some dir) None & info ["output-json"] ~docv:"DIR" ~doc: - "save the processed exercises in JSON format in `.json` files, \ - in the given directory." + value + & opt (some dir) None + & info ["output-json"] ~docv:"DIR" + ~doc: + "save the processed exercises in JSON format in `.json` files, in \ + the given directory." let grade_student = - value & opt (some file) None & info ["grade-student";"s"] ~docv:"FILE" ~doc: - "grade the given student file instead of 'solution.ml'" + value + & opt (some file) None + & info ["grade-student"; "s"] ~docv:"FILE" + ~doc:"grade the given student file instead of 'solution.ml'" let display_outcomes = - value & flag & info ["display-outcomes"] ~doc: - "display the toplevel's outcomes" + value & flag + & info ["display-outcomes"] ~doc:"display the toplevel's outcomes" let quiet = - value & flag & info ["quiet";"q"] ~doc: - "Don't display grading progression messages" + value & flag + & info ["quiet"; "q"] ~doc:"Don't display grading progression messages" let display_std_outputs = - value & flag & info ["display-stdouts"] ~doc: - "display the toplevel's standard outputs" + value & flag + & info ["display-stdouts"] ~doc:"display the toplevel's standard outputs" let dump_outputs = - value & opt (some string) None & info ["dump-outputs"] ~docv:"PREFIX" ~doc: - "save the outputs in files with the given prefix" + value + & opt (some string) None + & info ["dump-outputs"] ~docv:"PREFIX" + ~doc:"save the outputs in files with the given prefix" let dump_reports = - value & opt (some string) None & info ["dump-reports"] ~docv:"PREFIX" ~doc: - "save the reports in files with the given prefix" + value + & opt (some string) None + & info ["dump-reports"] ~docv:"PREFIX" + ~doc:"save the reports in files with the given prefix" let timeout = - value & opt (some int) None & info ["timeout"] ~docv:"SECONDS" ~doc: - "Limit every test to the given timeout" + value + & opt (some int) None + & info ["timeout"] ~docv:"SECONDS" + ~doc:"Limit every test to the given timeout" let verbose = - value & flag & info ["verbose"; "v"] ~doc: - "Display detailed grading reports to stdout" + value & flag + & info ["verbose"; "v"] ~doc:"Display detailed grading reports to stdout" let dump_dot = - value & opt (some string) None & info ["dump-dot"] ~doc: - "Generates a dependency graph of the repository and dumps it into the \ - given file" + value + & opt (some string) None + & info ["dump-dot"] + ~doc: + "Generates a dependency graph of the repository and dumps it into \ + the given file" - type t = { - exercises: string list; - output_json: string option; - } + type t = {exercises : string list; output_json : string option} let grader_conf = let apply exercises output_json = let exercises = List.flatten exercises in - { exercises; output_json } + {exercises; output_json} in - Term.(const apply $exercises $output_json) + Term.(const apply $ exercises $ output_json) let grader_cli = - let apply - grade_student display_outcomes quiet display_std_outputs - dump_outputs dump_reports timeout verbose dump_dot - = + let apply grade_student display_outcomes quiet display_std_outputs + dump_outputs dump_reports timeout verbose dump_dot = Grader_cli.grade_student := grade_student; Grader_cli.display_outcomes := display_outcomes; Grader_cli.display_callback := not quiet; @@ -136,12 +153,14 @@ module Args = struct Learnocaml_process_exercise_repository.dump_reports := dump_reports; () in - Term.(const apply $grade_student $display_outcomes $quiet $display_std_outputs - $dump_outputs $dump_reports $timeout $verbose $dump_dot) + Term.( + const apply $ grade_student $ display_outcomes $ quiet + $ display_std_outputs $ dump_outputs $ dump_reports $ timeout $ verbose + $ dump_dot) let term = let apply conf () = conf in - Term.(const apply $grader_conf $grader_cli) + Term.(const apply $ grader_conf $ grader_cli) end module Builder = struct @@ -149,103 +168,122 @@ module Args = struct let contents_dir = let default = - readlink (Filename.dirname (Filename.dirname (Sys.executable_name)) - /"share"/"learn-ocaml"/"www") + readlink + ( Filename.dirname (Filename.dirname Sys.executable_name) + / "share" / "learn-ocaml" / "www" ) in - value & opt dir default & info ["contents-dir"] ~docv:"DIR" ~doc: - "directory containing the base learn-ocaml app contents" + value & opt dir default + & info ["contents-dir"] ~docv:"DIR" + ~doc:"directory containing the base learn-ocaml app contents" let enable opt doc = - value & vflag None [ - Some true, info ["enable-"^opt] ~doc:("Enable "^doc); - Some false, info ["disable-"^opt] ~doc:("Disable "^doc); - ] + value + & vflag None + [ (Some true, info ["enable-" ^ opt] ~doc:("Enable " ^ doc)) + ; (Some false, info ["disable-" ^ opt] ~doc:("Disable " ^ doc)) ] - let try_ocaml = enable "tryocaml" + let try_ocaml = + enable "tryocaml" "the 'TryOCaml' tab (enabled by default if the repository contains a \ $(i,tutorials) directory)" - let playground = enable "playground" - "the 'Playground' tab (enabled by default if the repository contains a \ - $(i,playground) directory)" + let playground = + enable "playground" + "the 'Playground' tab (enabled by default if the repository contains \ + a $(i,playground) directory)" - let lessons = enable "lessons" + let lessons = + enable "lessons" "the 'Lessons' tab (enabled by default if the repository contains a \ $(i,lessons) directory)" - let exercises = enable "exercises" - "the 'Exercises' tab (enabled by default if the repository contains an \ - $(i,exercises) directory)" + let exercises = + enable "exercises" + "the 'Exercises' tab (enabled by default if the repository contains \ + an $(i,exercises) directory)" - let toplevel = enable "toplevel" - "the 'Toplevel' tab (enabled by default)" + let toplevel = enable "toplevel" "the 'Toplevel' tab (enabled by default)" let exercises_filtered = - value & opt_all (list string) [[]] & info ["exercises-filtered"; "f"] ~docv:"DIRS" ~doc: - "Exercises to build (comma-separated), instead of taking \ - the entire repository. Can be repeated." + value + & opt_all (list string) [[]] + & info + ["exercises-filtered"; "f"] + ~docv:"DIRS" + ~doc: + "Exercises to build (comma-separated), instead of taking the \ + entire repository. Can be repeated." let jobs = - value & opt int 1 & info ["jobs";"j"] ~docv:"INT" ~doc: - "Number of building jobs to run in parallel" - - type t = { - contents_dir: string; - try_ocaml: bool option; - lessons: bool option; - exercises: bool option; - playground: bool option; - toplevel: bool option; - base_url: string - } + value & opt int 1 + & info ["jobs"; "j"] ~docv:"INT" + ~doc:"Number of building jobs to run in parallel" + + type t = + { contents_dir : string + ; try_ocaml : bool option + ; lessons : bool option + ; exercises : bool option + ; playground : bool option + ; toplevel : bool option + ; base_url : string } let builder_conf = - let apply - contents_dir try_ocaml lessons exercises playground toplevel base_url - = { contents_dir; try_ocaml; lessons; exercises; playground; toplevel; base_url } + let apply contents_dir try_ocaml lessons exercises playground toplevel + base_url = + { contents_dir + ; try_ocaml + ; lessons + ; exercises + ; playground + ; toplevel + ; base_url } in - Term.(const apply $contents_dir $try_ocaml $lessons $exercises $playground $toplevel $base_url) + Term.( + const apply $ contents_dir $ try_ocaml $ lessons $ exercises + $ playground $ toplevel $ base_url) let repo_conf = let apply repo_dir exercises_filtered jobs = Learnocaml_process_exercise_repository.exercises_dir := - repo_dir/"exercises"; + repo_dir / "exercises"; Learnocaml_process_exercise_repository.exercises_filtered := Learnocaml_data.SSet.of_list (List.flatten exercises_filtered); Learnocaml_process_tutorial_repository.tutorials_dir := - repo_dir/"tutorials"; + repo_dir / "tutorials"; Learnocaml_process_playground_repository.playground_dir := - repo_dir/"playground"; + repo_dir / "playground"; Learnocaml_process_exercise_repository.n_processes := jobs; () in - Term.(const apply $repo_dir $exercises_filtered $jobs) + Term.(const apply $ repo_dir $ exercises_filtered $ jobs) let term = let apply conf () = conf in - Term.(const apply $builder_conf $repo_conf) + Term.(const apply $ builder_conf $ repo_conf) end module Server = struct include Learnocaml_server_args + let info = info ~docs:"SERVER OPTIONS" end - type t = { - commands: command list; - app_dir: string; - repo_dir: string; - grader: Grader.t; - builder: Builder.t; - server: Server.t; - } + type t = + { commands : command list + ; app_dir : string + ; repo_dir : string + ; grader : Grader.t + ; builder : Builder.t + ; server : Server.t } let term = let apply commands app_dir repo_dir grader builder server = - { commands; app_dir; repo_dir; grader; builder; server } + {commands; app_dir; repo_dir; grader; builder; server} in - Term.(const apply $commands $app_dir $repo_dir - $Grader.term $Builder.term $Server.term app_dir base_url) + Term.( + const apply $ commands $ app_dir $ repo_dir $ Grader.term $ Builder.term + $ Server.term app_dir base_url) end open Args @@ -254,136 +292,151 @@ let process_html_file orig_file dest_file base_url no_secret = let transform_tag e tag attrs attr = let attr_pair = ("", attr) in match List.assoc_opt attr_pair attrs with - | Some url -> `Start_element ((e, tag), (attr_pair, base_url ^ url) :: (List.remove_assoc attr_pair attrs)) - | None -> `Start_element ((e, tag), attrs) in - Lwt_io.open_file ~mode:Lwt_io.Input orig_file >>= fun ofile -> - Lwt_io.open_file ~mode:Lwt_io.Output dest_file >>= fun wfile -> + | Some url -> + `Start_element + ( (e, tag) + , (attr_pair, base_url ^ url) :: List.remove_assoc attr_pair attrs ) + | None -> `Start_element ((e, tag), attrs) + in + Lwt_io.open_file ~mode:Lwt_io.Input orig_file + >>= fun ofile -> + Lwt_io.open_file ~mode:Lwt_io.Output dest_file + >>= fun wfile -> let document = Markup_lwt.lwt_stream (Lwt_io.read_chars ofile) in - Markup.parse_html document - |> Markup.signals + Markup.parse_html document |> Markup.signals |> Markup.map (function - | `Start_element ((e, "link"), attrs) -> transform_tag e "link" attrs "href" - | `Start_element ((e, "script"), attrs) -> transform_tag e "script" attrs "src" - | `Start_element ((e, "img"), attrs) -> transform_tag e "img" attrs "src" - | `Start_element ((e, "a"), attrs) -> transform_tag e "a" attrs "href" - | `Start_element ((e, "div"),attrs) - when no_secret && List.mem (("", "id"), "secret-section") attrs -> - `Start_element ((e, "div"), (("", "style"), "display:none")::attrs) - | t -> t) - |> Markup.pretty_print - |> Markup.write_html - |> Markup_lwt.to_lwt_stream - |> Lwt_io.write_chars wfile >>= fun () -> - Lwt_io.close ofile >>= fun () -> - Lwt_io.close wfile + | `Start_element ((e, "link"), attrs) -> + transform_tag e "link" attrs "href" + | `Start_element ((e, "script"), attrs) -> + transform_tag e "script" attrs "src" + | `Start_element ((e, "img"), attrs) -> + transform_tag e "img" attrs "src" + | `Start_element ((e, "a"), attrs) -> transform_tag e "a" attrs "href" + | `Start_element ((e, "div"), attrs) + when no_secret && List.mem (("", "id"), "secret-section") attrs -> + `Start_element ((e, "div"), (("", "style"), "display:none") :: attrs) + | t -> t ) + |> Markup.pretty_print |> Markup.write_html |> Markup_lwt.to_lwt_stream + |> Lwt_io.write_chars wfile + >>= fun () -> Lwt_io.close ofile >>= fun () -> Lwt_io.close wfile let main o = Printf.printf "Learnocaml v.%s running.\n" Learnocaml_api.version; let grade () = - if List.mem Grade o.commands then - (if List.mem Build o.commands || List.mem Serve o.commands then - failwith "The 'grade' command is incompatible with 'build' and \ - 'serve'"; - Lwt_list.fold_left_s (fun i ex -> - let json_output = match o.grader.Grader.output_json with - | None -> None - | Some o -> - Some (Filename.concat o - (String.map (function '/' -> '_' | c -> c) ex - ^ ".json")) - in - Lwt.catch - (fun () -> - Grader_cli.grade_from_dir ~print_result:true ex json_output - >|= function Ok () -> i | Error _ -> 1) - (fun e -> - Printf.ksprintf failwith - "Could not load exercise at %s: %s" ex (Printexc.to_string e))) - 0 o.grader.Grader.exercises - >|= fun i -> Some i) + if List.mem Grade o.commands then ( + if List.mem Build o.commands || List.mem Serve o.commands then + failwith "The 'grade' command is incompatible with 'build' and 'serve'"; + Lwt_list.fold_left_s + (fun i ex -> + let json_output = + match o.grader.Grader.output_json with + | None -> None + | Some o -> + Some + (Filename.concat o + (String.map (function '/' -> '_' | c -> c) ex ^ ".json")) + in + Lwt.catch + (fun () -> + Grader_cli.grade_from_dir ~print_result:true ex json_output + >|= function Ok () -> i | Error _ -> 1 ) + (fun e -> + Printf.ksprintf failwith "Could not load exercise at %s: %s" ex + (Printexc.to_string e) ) ) + 0 o.grader.Grader.exercises + >|= fun i -> Some i ) else Lwt.return_none in let generate () = - if List.mem Build o.commands then - (Printf.printf "Updating app at %s\n%!" o.app_dir; - Lwt.catch - (fun () -> Lwt_utils.copy_tree o.builder.Builder.contents_dir o.app_dir) - (function - | Failure _ -> - Lwt.fail_with @@ Printf.sprintf - "Failed to copy base app contents from %s" - (readlink o.builder.Builder.contents_dir) - | e -> Lwt.fail e) - >>= fun () -> - let server_config = o.repo_dir/"server_config.json" - and www_server_config = o.app_dir/"server_config.json" in - let module ServerData = Learnocaml_data.Server in - Random.self_init (); - Lwt.catch - (fun () -> - Learnocaml_store.get_from_file ServerData.preconfig_enc server_config) - (function - | Unix.Unix_error (Unix.ENOENT, _, _) -> Lwt.return ServerData.empty_preconfig - | exn -> Lwt.fail exn) - >>= fun preconfig -> - let json_config = ServerData.build_config preconfig in - Learnocaml_store.write_to_file ServerData.config_enc json_config www_server_config - >>= fun () -> - if o.builder.Builder.base_url <> "" then - Printf.printf "Base URL: %s\n%!" o.builder.Builder.base_url; - Lwt_unix.files_of_directory o.builder.Builder.contents_dir - |> Lwt_stream.iter_s (fun file -> - let config_secret = json_config.ServerData.secret in - if Filename.extension file = ".html" then - process_html_file (o.builder.Builder.contents_dir/file) - (o.app_dir/file) o.builder.Builder.base_url (config_secret = None) - else - Lwt.return_unit) >>= fun () -> - let if_enabled opt dir f = (match opt with - | None -> - Lwt.catch (fun () -> - Lwt_unix.stat dir >|= fun st -> st.Unix.st_kind = Unix.S_DIR) - (function Unix.Unix_error _ -> Lwt.return_false - | e -> Lwt.fail e) - | Some opt -> Lwt.return opt) - >>= fun enabled -> - if enabled then f dir >>= Lwt.return_some else Lwt.return_none - in - if_enabled o.builder.Builder.lessons (o.repo_dir/"lessons") - (fun dir -> - Lwt_utils.copy_tree dir (o.app_dir/"lessons") >>= fun () -> - Lwt_unix.rename (o.app_dir/"lessons"/"lessons.json") (o.app_dir/"lessons.json") - >|= fun () -> true) - >>= fun lessons_ret -> - if_enabled o.builder.Builder.try_ocaml (o.repo_dir/"tutorials") - (fun _ -> Learnocaml_process_tutorial_repository.main (o.app_dir)) - >>= fun tutorials_ret -> - if_enabled o.builder.Builder.playground (o.repo_dir/"playground") - (fun _ -> Learnocaml_process_playground_repository.main (o.app_dir)) - >>= fun playground_ret -> - if_enabled o.builder.Builder.exercises (o.repo_dir/"exercises") - (fun _ -> Learnocaml_process_exercise_repository.main (o.app_dir)) - >>= fun exercises_ret -> - Lwt_io.with_file ~mode:Lwt_io.Output (o.app_dir/"js"/"learnocaml-config.js") - (fun oc -> - Lwt_io.fprintf oc - "var learnocaml_config = {\n\ - \ enableTryocaml: %b,\n\ - \ enablePlayground: %b,\n\ - \ enableLessons: %b,\n\ - \ enableExercises: %b,\n\ - \ enableToplevel: %b,\n\ - \ baseUrl: \"%s\"\n\ - }\n" - (tutorials_ret <> None) - (playground_ret <> None) - (lessons_ret <> None) - (exercises_ret <> None) - (o.builder.Builder.toplevel <> Some false) - o.builder.Builder.base_url >>= fun () -> - Lwt.return (tutorials_ret <> Some false && exercises_ret <> Some false))) - else - Lwt.return true + if List.mem Build o.commands then ( + Printf.printf "Updating app at %s\n%!" o.app_dir; + Lwt.catch + (fun () -> Lwt_utils.copy_tree o.builder.Builder.contents_dir o.app_dir) + (function + | Failure _ -> + Lwt.fail_with + @@ Printf.sprintf "Failed to copy base app contents from %s" + (readlink o.builder.Builder.contents_dir) + | e -> Lwt.fail e) + >>= fun () -> + let server_config = o.repo_dir / "server_config.json" + and www_server_config = o.app_dir / "server_config.json" in + let module ServerData = Learnocaml_data.Server in + Random.self_init (); + Lwt.catch + (fun () -> + Learnocaml_store.get_from_file ServerData.preconfig_enc server_config + ) + (function + | Unix.Unix_error (Unix.ENOENT, _, _) -> + Lwt.return ServerData.empty_preconfig + | exn -> Lwt.fail exn) + >>= fun preconfig -> + let json_config = ServerData.build_config preconfig in + Learnocaml_store.write_to_file ServerData.config_enc json_config + www_server_config + >>= fun () -> + if o.builder.Builder.base_url <> "" then + Printf.printf "Base URL: %s\n%!" o.builder.Builder.base_url; + Lwt_unix.files_of_directory o.builder.Builder.contents_dir + |> Lwt_stream.iter_s (fun file -> + let config_secret = json_config.ServerData.secret in + if Filename.extension file = ".html" then + process_html_file + (o.builder.Builder.contents_dir / file) + (o.app_dir / file) o.builder.Builder.base_url + (config_secret = None) + else Lwt.return_unit ) + >>= fun () -> + let if_enabled opt dir f = + ( match opt with + | None -> + Lwt.catch + (fun () -> + Lwt_unix.stat dir >|= fun st -> st.Unix.st_kind = Unix.S_DIR ) + (function + | Unix.Unix_error _ -> Lwt.return_false | e -> Lwt.fail e) + | Some opt -> Lwt.return opt ) + >>= fun enabled -> + if enabled then f dir >>= Lwt.return_some else Lwt.return_none + in + if_enabled o.builder.Builder.lessons (o.repo_dir / "lessons") (fun dir -> + Lwt_utils.copy_tree dir (o.app_dir / "lessons") + >>= fun () -> + Lwt_unix.rename + (o.app_dir / "lessons" / "lessons.json") + (o.app_dir / "lessons.json") + >|= fun () -> true ) + >>= fun lessons_ret -> + if_enabled o.builder.Builder.try_ocaml (o.repo_dir / "tutorials") + (fun _ -> Learnocaml_process_tutorial_repository.main o.app_dir ) + >>= fun tutorials_ret -> + if_enabled o.builder.Builder.playground (o.repo_dir / "playground") + (fun _ -> Learnocaml_process_playground_repository.main o.app_dir ) + >>= fun playground_ret -> + if_enabled o.builder.Builder.exercises (o.repo_dir / "exercises") + (fun _ -> Learnocaml_process_exercise_repository.main o.app_dir ) + >>= fun exercises_ret -> + Lwt_io.with_file ~mode:Lwt_io.Output + (o.app_dir / "js" / "learnocaml-config.js") + (fun oc -> + Lwt_io.fprintf oc + "var learnocaml_config = {\n\ + \ enableTryocaml: %b,\n\ + \ enablePlayground: %b,\n\ + \ enableLessons: %b,\n\ + \ enableExercises: %b,\n\ + \ enableToplevel: %b,\n\ + \ baseUrl: \"%s\"\n\ + }\n" + (tutorials_ret <> None) (playground_ret <> None) + (lessons_ret <> None) (exercises_ret <> None) + (o.builder.Builder.toplevel <> Some false) + o.builder.Builder.base_url + >>= fun () -> + Lwt.return + (tutorials_ret <> Some false && exercises_ret <> Some false) ) ) + else Lwt.return true in let run_server () = if List.mem Serve o.commands then @@ -391,78 +444,79 @@ let main o = if Sys.file_exists native_server then let server_args = let open Server in - ("--app-dir="^o.app_dir) :: - ("--sync-dir="^o.server.sync_dir) :: - ("--base-url="^o.builder.Builder.base_url) :: - ("--port="^string_of_int o.server.port) :: - (match o.server.cert with None -> [] | Some c -> ["--cert="^c]) + ("--app-dir=" ^ o.app_dir) + :: ("--sync-dir=" ^ o.server.sync_dir) + :: ("--base-url=" ^ o.builder.Builder.base_url) + :: ("--port=" ^ string_of_int o.server.port) + :: (match o.server.cert with None -> [] | Some c -> ["--cert=" ^ c]) in - Unix.execv native_server (Array.of_list (native_server::server_args)) - else begin - Printf.printf "Starting server on port %d\n%!" - !Learnocaml_server.port; - if o.builder.Builder.base_url <> "" then - Printf.printf "Base URL: %s\n%!" o.builder.Builder.base_url; - Learnocaml_server.launch () - end - else - Lwt.return true + Unix.execv native_server (Array.of_list (native_server :: server_args)) + else ( + Printf.printf "Starting server on port %d\n%!" !Learnocaml_server.port; + if o.builder.Builder.base_url <> "" then + Printf.printf "Base URL: %s\n%!" o.builder.Builder.base_url; + Learnocaml_server.launch () ) + else Lwt.return true in let ret = Lwt_main.run - (grade () >>= function - | Some i -> Lwt.return i - | None -> - generate () >>= fun success -> - if success then - run_server () >>= fun r -> - if r then Lwt.return 0 else Lwt.return 10 - else - Lwt.return 1) + ( grade () + >>= function + | Some i -> Lwt.return i + | None -> + generate () + >>= fun success -> + if success then + run_server () + >>= fun r -> if r then Lwt.return 0 else Lwt.return 10 + else Lwt.return 1 ) in exit ret -let man = [ - `S "DESCRIPTION"; - `P "This program performs various tasks related to generating, serving and \ - administrating a learn-ocaml web-app."; - `S "COMMANDS"; - `P "The $(i,COMMAND) argument may be one or more of the following. If no \ - command is specified, '$(b,build) $(b,serve)' is assumed."; - `I ("$(b,grade)", "Runs the automatic grader on exercise solutions."); - `I ("$(b,build)", "Generates the application based on a repository \ - containing the lessons, tutorials, playground and exercises (see \ - $(b,REPOSITORY FORMAT))."); - `I ("$(b,serve)", "Run a web-server providing access to the learn-ocaml app, \ - as well as user file synchronisation."); - `S "OPTIONS"; - `S "GRADER OPTIONS"; - `S "BUILDER OPTIONS"; - `S "SERVER OPTIONS"; - `S "REPOSITORY FORMAT"; - `P "The repository specified by $(b,--repo) is expected to contain \ - sub-directories $(b,lessons), $(b,tutorials), $(b,playground) and $(b,exercises)."; - `S "AUTHORS"; - `P "The original authors are Benjamin Canou, \ - Çağdaş Bozman, Grégoire Henry and Louis Gesbert. It is licensed under \ - the MIT License."; - `S "BUGS"; - `P "Bugs should be reported to \ - $(i,https://github.com/ocaml-sf/learn-ocaml/issues)"; -] +let man = + [ `S "DESCRIPTION" + ; `P + "This program performs various tasks related to generating, serving and \ + administrating a learn-ocaml web-app." + ; `S "COMMANDS" + ; `P + "The $(i,COMMAND) argument may be one or more of the following. If no \ + command is specified, '$(b,build) $(b,serve)' is assumed." + ; `I ("$(b,grade)", "Runs the automatic grader on exercise solutions.") + ; `I + ( "$(b,build)" + , "Generates the application based on a repository containing the \ + lessons, tutorials, playground and exercises (see $(b,REPOSITORY \ + FORMAT))." ) + ; `I + ( "$(b,serve)" + , "Run a web-server providing access to the learn-ocaml app, as well as \ + user file synchronisation." ) + ; `S "OPTIONS" + ; `S "GRADER OPTIONS" + ; `S "BUILDER OPTIONS" + ; `S "SERVER OPTIONS" + ; `S "REPOSITORY FORMAT" + ; `P + "The repository specified by $(b,--repo) is expected to contain \ + sub-directories $(b,lessons), $(b,tutorials), $(b,playground) and \ + $(b,exercises)." + ; `S "AUTHORS" + ; `P + "The original authors are Benjamin Canou, Çağdaş Bozman, Grégoire \ + Henry and Louis Gesbert. It is licensed under the MIT License." + ; `S "BUGS" + ; `P + "Bugs should be reported to \ + $(i,https://github.com/ocaml-sf/learn-ocaml/issues)" ] let main_cmd = - Cmdliner.Term.(const main $ Args.term), - Cmdliner.Term.info - ~man - ~doc:"Learn-ocaml web-app manager" - ~version:Learnocaml_api.version - "learn-ocaml" + ( Cmdliner.Term.(const main $ Args.term) + , Cmdliner.Term.info ~man ~doc:"Learn-ocaml web-app manager" + ~version:Learnocaml_api.version "learn-ocaml" ) let () = - match - Cmdliner.Term.eval ~catch:false main_cmd - with + match Cmdliner.Term.eval ~catch:false main_cmd with | exception Failure msg -> Printf.eprintf "[ERROR] %s\n" msg; exit 1 diff --git a/src/main/learnocaml_server_args.ml b/src/main/learnocaml_server_args.ml index 7cbd5e74b..623c66848 100644 --- a/src/main/learnocaml_server_args.ml +++ b/src/main/learnocaml_server_args.ml @@ -10,51 +10,56 @@ open Cmdliner open Arg let sync_dir = - value & opt string "./sync" & info ["sync-dir"] ~docv:"DIR" ~doc: - "Directory where to store user sync tokens" + value & opt string "./sync" + & info ["sync-dir"] ~docv:"DIR" + ~doc:"Directory where to store user sync tokens" let default_http_port = 8080 + let default_https_port = 8443 let cert = - value & opt (some string) None & - info ["cert"] ~docv:"BASENAME" ~env:(Arg.env_var "LEARNOCAML_CERT") ~doc: - "HTTPS certificate: this option turns on HTTPS, and requires files \ - $(i,BASENAME.pem) and $(i,BASENAME.key) to be present. They will be \ - used as the server certificate and key, respectively. A passphrase \ - may be asked on the terminal if the key file is encrypted." + value + & opt (some string) None + & info ["cert"] ~docv:"BASENAME" + ~env:(Arg.env_var "LEARNOCAML_CERT") + ~doc: + "HTTPS certificate: this option turns on HTTPS, and requires files \ + $(i,BASENAME.pem) and $(i,BASENAME.key) to be present. They will be \ + used as the server certificate and key, respectively. A passphrase \ + may be asked on the terminal if the key file is encrypted." let port = - value & opt (some int) None & - info ["port";"p"] ~docv:"PORT" ~env:(Arg.env_var "LEARNOCAML_PORT") ~doc: - (Printf.sprintf - "The TCP port on which to run the server. Defaults to %d, or %d if \ - HTTPS is enabled." - default_http_port default_https_port) - -type t = { - sync_dir: string; - base_url: string; - port: int; - cert: string option; -} + value + & opt (some int) None + & info ["port"; "p"] ~docv:"PORT" + ~env:(Arg.env_var "LEARNOCAML_PORT") + ~doc: + (Printf.sprintf + "The TCP port on which to run the server. Defaults to %d, or %d if \ + HTTPS is enabled." + default_http_port default_https_port) + +type t = + {sync_dir : string; base_url : string; port : int; cert : string option} let term app_dir base_url = let apply app_dir sync_dir base_url port cert = Learnocaml_store.static_dir := app_dir; Learnocaml_store.sync_dir := sync_dir; - let port = match port, cert with + let port = + match (port, cert) with | Some p, _ -> p | None, Some _ -> default_https_port | None, None -> default_http_port in - Learnocaml_server.cert_key_files := - (match cert with - | Some base -> Some (base ^ ".pem", base ^ ".key"); + (Learnocaml_server.cert_key_files := + match cert with + | Some base -> Some (base ^ ".pem", base ^ ".key") | None -> None); Learnocaml_server.port := port; Learnocaml_server.base_url := base_url; - { sync_dir; base_url; port; cert } + {sync_dir; base_url; port; cert} in (* warning: if you add any options here, remember to pass them through when calling the native server from learn-ocaml main *) diff --git a/src/main/learnocaml_server_args.mli b/src/main/learnocaml_server_args.mli index 863e7d63c..187c39007 100644 --- a/src/main/learnocaml_server_args.mli +++ b/src/main/learnocaml_server_args.mli @@ -6,11 +6,8 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -type t = { - sync_dir: string; - base_url: string; - port: int; - cert: string option; -} +type t = + {sync_dir : string; base_url : string; port : int; cert : string option} -val term: string Cmdliner.Term.t -> string Cmdliner.Term.t -> t Cmdliner.Term.t +val term : + string Cmdliner.Term.t -> string Cmdliner.Term.t -> t Cmdliner.Term.t diff --git a/src/main/learnocaml_server_main.ml b/src/main/learnocaml_server_main.ml index e3a4e3c3e..ddfe74515 100644 --- a/src/main/learnocaml_server_main.ml +++ b/src/main/learnocaml_server_main.ml @@ -13,10 +13,11 @@ let signal_waiter = let waiter, wakener = Lwt.wait () in let handler signum = Format.eprintf "%s caught: stopping@." - (if signum = Sys.sigint then "SIGINT" else - if signum = Sys.sigterm then "SIGTERM" else - "Signal"); - Lwt.wakeup_later wakener (128 - signum) in + ( if signum = Sys.sigint then "SIGINT" + else if signum = Sys.sigterm then "SIGTERM" + else "Signal" ); + Lwt.wakeup_later wakener (128 - signum) + in let _ = Lwt_unix.on_signal Sys.sigint handler in let _ = Lwt_unix.on_signal Sys.sigterm handler in waiter @@ -24,74 +25,75 @@ let signal_waiter = let main o = Printf.printf "Learnocaml server v.%s starting on port %d\n%!" Learnocaml_api.version o.port; - if o.base_url <> "" then - Printf.printf "Base URL: %s\n%!" o.base_url; + if o.base_url <> "" then Printf.printf "Base URL: %s\n%!" o.base_url; let rec run () = let minimum_duration = 15. in let t0 = Unix.time () in try - Lwt_main.run @@ Lwt.pick [ - (Learnocaml_server.launch () >|= function true -> 0 | false -> 10); - signal_waiter - ] + Lwt_main.run + @@ Lwt.pick + [ ( Learnocaml_server.launch () + >|= function true -> 0 | false -> 10 ) + ; signal_waiter ] with Unix.Unix_error (err, fn, arg) -> - Format.eprintf "SERVER CRASH in %s(%s):@ @[%s@]@." - fn arg (Unix.error_message err); + Format.eprintf "SERVER CRASH in %s(%s):@ @[%s@]@." fn arg + (Unix.error_message err); let dt = Unix.time () -. t0 in - if dt < minimum_duration then - (Format.eprintf "Live time was only %.0fs, aborting (<%fs)@." - dt minimum_duration; - exit 20) - else - (Format.eprintf "Server was live %.0f seconds. Respawning@." dt; - run ()) + if dt < minimum_duration then ( + Format.eprintf "Live time was only %.0fs, aborting (<%fs)@." dt + minimum_duration; + exit 20 ) + else ( + Format.eprintf "Server was live %.0f seconds. Respawning@." dt; + run () ) in exit (run ()) -let man = [ - `S "DESCRIPTION"; - `P "This is the server for learn-ocaml. It is equivalent to running \ - $(b,learn-ocaml serve), but may be faster if compiled to native code. It \ - requires the learn-ocaml app to have been built using $(b,learn-ocaml \ - build) beforehand."; - `S "SERVER OPTIONS"; - `S "AUTHORS"; - `P "Learn OCaml is written by OCamlPro. Its main authors are Benjamin Canou, \ - Çağdaş Bozman, Grégoire Henry and Louis Gesbert. It is licensed under \ - the MIT License."; - `S "BUGS"; - `P "Bugs should be reported to \ - $(i,https://github.com/ocaml-sf/learn-ocaml/issues)"; -] +let man = + [ `S "DESCRIPTION" + ; `P + "This is the server for learn-ocaml. It is equivalent to running \ + $(b,learn-ocaml serve), but may be faster if compiled to native code. \ + It requires the learn-ocaml app to have been built using \ + $(b,learn-ocaml build) beforehand." + ; `S "SERVER OPTIONS" + ; `S "AUTHORS" + ; `P + "Learn OCaml is written by OCamlPro. Its main authors are Benjamin \ + Canou, Çağdaş Bozman, Grégoire Henry and Louis Gesbert. It is \ + licensed under the MIT License." + ; `S "BUGS" + ; `P + "Bugs should be reported to \ + $(i,https://github.com/ocaml-sf/learn-ocaml/issues)" ] let app_dir = let open Cmdliner.Arg in - value & opt string "./www" & info ["app-dir"; "o"] ~docv:"DIR" ~doc: - "Directory where the app has been generated by the $(b,learn-ocaml build) \ - command, and from where it will be served." + value & opt string "./www" + & info ["app-dir"; "o"] ~docv:"DIR" + ~doc: + "Directory where the app has been generated by the $(b,learn-ocaml \ + build) command, and from where it will be served." let base_url = let open Cmdliner.Arg in - value & opt string "" & - info ["base-url"] ~docv:"BASE_URL" ~env:(env_var "LEARNOCAML_BASE_URL") ~doc: - "Set the base URL of the website. \ - Should not end with a trailing slash. \ - Currently, this has no effect on the backend - '$(b,learn-ocaml serve)'. \ - Mandatory for '$(b,learn-ocaml build)' if the site is not hosted in path '/', \ - which typically occurs for static deployment." + value & opt string "" + & info ["base-url"] ~docv:"BASE_URL" + ~env:(env_var "LEARNOCAML_BASE_URL") + ~doc: + "Set the base URL of the website. Should not end with a trailing \ + slash. Currently, this has no effect on the backend - \ + '$(b,learn-ocaml serve)'. Mandatory for '$(b,learn-ocaml build)' if \ + the site is not hosted in path '/', which typically occurs for \ + static deployment." let main_cmd = - Cmdliner.Term.(const main $ Learnocaml_server_args.term app_dir base_url), - Cmdliner.Term.info - ~man - ~doc:"Learn-ocaml web-app manager" - ~version:Learnocaml_api.version - "learn-ocaml" + ( Cmdliner.Term.(const main $ Learnocaml_server_args.term app_dir base_url) + , Cmdliner.Term.info ~man ~doc:"Learn-ocaml web-app manager" + ~version:Learnocaml_api.version "learn-ocaml" ) let () = - match - Cmdliner.Term.eval ~catch:false main_cmd - with + match Cmdliner.Term.eval ~catch:false main_cmd with | exception Failure msg -> Printf.eprintf "[ERROR] %s\n" msg; exit 1 diff --git a/src/ppx-metaquot/fun_ty.ml b/src/ppx-metaquot/fun_ty.ml index 2c2580d00..82b15d2ba 100644 --- a/src/ppx-metaquot/fun_ty.ml +++ b/src/ppx-metaquot/fun_ty.ml @@ -13,14 +13,17 @@ type (_, _, _) args = | Arg : 'a * ('b, 'c, 'r) args -> ('a -> 'b, 'a -> 'c, 'r) args let last x = Last x + let arg x r = Arg (x, r) -let (!!) = last -let (@:) = arg -let (@:!!) a b = a @: !! b +let ( !! ) = last + +let ( @: ) = arg + +let ( @:!! ) a b = a @: !!b -let rec apply - : type p a c r. (p -> a) -> (p -> a, p -> c, r) args -> r = fun f x -> +let rec apply : type p a c r. (p -> a) -> (p -> a, p -> c, r) args -> r = + fun f x -> match x with | Last x -> f x | Arg (x, Last r) -> (f x) r @@ -31,78 +34,82 @@ let rec apply *) type (_, _, _) fun_ty = | Last_ty : 'a Ty.ty * 'r Ty.ty -> (('a -> 'r) Ty.ty, 'a -> unit, 'r) fun_ty - | Arg_ty : 'a Ty.ty * (('b -> 'c) Ty.ty, 'b -> 'd, 'r) fun_ty -> - (('a -> 'b -> 'c) Ty.ty, 'a -> 'b -> 'd, 'r) fun_ty + | Arg_ty : + 'a Ty.ty * (('b -> 'c) Ty.ty, 'b -> 'd, 'r) fun_ty + -> (('a -> 'b -> 'c) Ty.ty, 'a -> 'b -> 'd, 'r) fun_ty let last_ty x r = Last_ty (x, r) + let arg_ty x r = Arg_ty (x, r) -let rec ty_of_fun_ty - : type p a c r. ((p -> a) Ty.ty, p -> c, r) fun_ty -> (p -> a) Ty.ty = - function +let rec ty_of_fun_ty : type p a c r. + ((p -> a) Ty.ty, p -> c, r) fun_ty -> (p -> a) Ty.ty = function | Last_ty (a, b) -> Ty.curry a b | Arg_ty (x, Last_ty (l, r)) -> Ty.curry x (Ty.curry l r) | Arg_ty (x, Arg_ty (y, r)) -> Ty.curry x (ty_of_fun_ty (Arg_ty (y, r))) -let rec get_ret_ty - : type p a c r. (p -> a) Ty.ty -> (p -> a, p -> c, r) args -> r Ty.ty = - fun ty x -> +let rec get_ret_ty : type p a c r. + (p -> a) Ty.ty -> (p -> a, p -> c, r) args -> r Ty.ty = + fun ty x -> match x with | Last _ -> - let _, ret_ty = Ty.domains ty in - ret_ty + let _, ret_ty = Ty.domains ty in + ret_ty | Arg (_, Last r) -> - let _, ret_ty = Ty.domains ty in - get_ret_ty ret_ty (Last r) + let _, ret_ty = Ty.domains ty in + get_ret_ty ret_ty (Last r) | Arg (_, Arg (y, r)) -> - let _, ret_ty = Ty.domains ty in - get_ret_ty ret_ty (Arg (y, r)) + let _, ret_ty = Ty.domains ty in + get_ret_ty ret_ty (Arg (y, r)) module type S = sig val typed_printer : 'a Ty.ty -> Format.formatter -> 'a -> unit + val typed_sampler : 'a Ty.ty -> unit -> 'a end module Make (M : S) = struct - let rec print - : type p a c r. ((p -> a) Ty.ty, p -> c, r) fun_ty -> - Format.formatter -> (p -> a, p -> c, r) args -> unit = - fun t ppf x -> - match t, x with + let rec print : type p a c r. + ((p -> a) Ty.ty, p -> c, r) fun_ty + -> Format.formatter + -> (p -> a, p -> c, r) args + -> unit = + fun t ppf x -> + match (t, x) with | Last_ty (arg_ty, _), Last x -> - Format.fprintf ppf "@ %a" - (M.typed_printer arg_ty) x + Format.fprintf ppf "@ %a" (M.typed_printer arg_ty) x | Arg_ty (arg_ty, ret_ty), Arg (x, Last r) -> - Format.fprintf ppf "@ %a%a" - (M.typed_printer arg_ty) x - (print ret_ty) (Last r) + Format.fprintf ppf "@ %a%a" (M.typed_printer arg_ty) x (print ret_ty) + (Last r) | Arg_ty (arg_ty, ret_ty), Arg (x, Arg (y, r)) -> - Format.fprintf ppf "@ %a%a" - (M.typed_printer arg_ty) x - (print ret_ty) (Arg (y, r)) + Format.fprintf ppf "@ %a%a" (M.typed_printer arg_ty) x (print ret_ty) + (Arg (y, r)) | Last_ty (_, _), Arg (_, _) -> . - let rec get_sampler - : type p a c r. ((p -> a) Ty.ty, p -> c, r) fun_ty -> unit -> - (p -> a, p -> c, r) args = - fun wit -> + let rec get_sampler : type p a c r. + ((p -> a) Ty.ty, p -> c, r) fun_ty -> unit -> (p -> a, p -> c, r) args = + fun wit -> match wit with | Last_ty (x, _) -> - let arg_sampler = M.typed_sampler x in - (fun () -> Last (arg_sampler ())) + let arg_sampler = M.typed_sampler x in + fun () -> Last (arg_sampler ()) | Arg_ty (x, Last_ty (l, r)) -> - let arg_sampler = M.typed_sampler x in - let ret_sampler = get_sampler (Last_ty (l, r)) in - (fun () -> let arg = arg_sampler () in Arg (arg, ret_sampler ())) + let arg_sampler = M.typed_sampler x in + let ret_sampler = get_sampler (Last_ty (l, r)) in + fun () -> + let arg = arg_sampler () in + Arg (arg, ret_sampler ()) | Arg_ty (x, Arg_ty (y, r)) -> - let arg_sampler = M.typed_sampler x in - let ret_sampler = get_sampler (Arg_ty (y, r)) in - (fun () -> let arg = arg_sampler () in Arg (arg, ret_sampler ())) + let arg_sampler = M.typed_sampler x in + let ret_sampler = get_sampler (Arg_ty (y, r)) in + fun () -> + let arg = arg_sampler () in + Arg (arg, ret_sampler ()) end let apply_args_1 f = function Last x -> f x -let apply_args_2 f = function | Arg (x, Last y) -> f x y +let apply_args_2 f = function Arg (x, Last y) -> f x y let apply_args_3 f = function Arg (w, Arg (x, Last y)) -> f w x y diff --git a/src/ppx-metaquot/fun_ty.mli b/src/ppx-metaquot/fun_ty.mli index 8347fd52a..d972a1ae9 100644 --- a/src/ppx-metaquot/fun_ty.mli +++ b/src/ppx-metaquot/fun_ty.mli @@ -27,38 +27,30 @@ Alternatively: [3 @: "word" @:!! false] *) type ('arrow, 'uarrow, 'ret) args +val last : 'a -> ('a -> 'ret, 'a -> unit, 'ret) args (** [last e], or equivalently [!! e], builds a one-element argument list. *) -val last : - 'a -> - ('a -> 'ret, 'a -> unit, 'ret) args +val arg : + 'a + -> ('ar -> 'row, 'ar -> 'urow, 'ret) args + -> ('a -> 'ar -> 'row, 'a -> 'ar -> 'urow, 'ret) args (** [arg a l], or equivalently [a @: l], adds [a] in front of the argument list [l]. *) -val arg : - 'a -> - ('ar -> 'row, 'ar -> 'urow, 'ret) args -> - ('a -> 'ar -> 'row, 'a -> 'ar -> 'urow, 'ret) args +val ( !! ) : 'a -> ('a -> 'ret, 'a -> unit, 'ret) args (** Helper notation for [last]. *) -val (!!) : - 'a -> - ('a -> 'ret, 'a -> unit, 'ret) args +val ( @: ) : + 'a + -> ('ar -> 'row, 'ar -> 'urow, 'ret) args + -> ('a -> 'ar -> 'row, 'a -> 'ar -> 'urow, 'ret) args (** Helper notation for [arg]. *) -val (@:) : - 'a -> - ('ar -> 'row, 'ar -> 'urow, 'ret) args -> - ('a -> 'ar -> 'row, 'a -> 'ar -> 'urow, 'ret) args +val ( @:!! ) : 'a -> 'b -> ('a -> 'b -> 'ret, 'a -> 'b -> unit, 'ret) args (** [a @:!! l] is another notation for [a @: !! l] (with a space). *) -val (@:!!) : - 'a -> 'b -> - ('a -> 'b -> 'ret, 'a -> 'b -> unit, 'ret) args +val apply : ('ar -> 'row) -> ('ar -> 'row, 'ar -> 'urow, 'ret) args -> 'ret (** [apply f l] applies a n-ary function [f] to the arguments from [l]. *) -val apply : - ('ar -> 'row) -> ('ar -> 'row, 'ar -> 'urow, 'ret) args -> - 'ret (** GADT for function types. @@ -100,60 +92,63 @@ val apply : where the co-domain type [bool] is now explicit. *) type ('arrow, 'uarrow, 'ret) fun_ty -(** [last_ty [%ty: a] [%ty: r]] builds a function type for [a -> r]. *) val last_ty : - 'a Ty.ty -> - 'ret Ty.ty -> - (('a -> 'ret) Ty.ty, 'a -> unit, 'ret) fun_ty + 'a Ty.ty -> 'ret Ty.ty -> (('a -> 'ret) Ty.ty, 'a -> unit, 'ret) fun_ty +(** [last_ty [%ty: a] [%ty: r]] builds a function type for [a -> r]. *) +val arg_ty : + 'a Ty.ty + -> (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty + -> (('a -> 'ar -> 'row) Ty.ty, 'a -> 'ar -> 'urow, 'ret) fun_ty (** [arg_ty [%ty: a] [%funty: b ->...-> r]] builds a function type for [a -> b ->...-> r]. *) -val arg_ty : - 'a Ty.ty -> - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - (('a -> 'ar -> 'row) Ty.ty, ('a -> 'ar -> 'urow), 'ret) fun_ty +val ty_of_fun_ty : + (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> ('ar -> 'row) Ty.ty (** [ty_of_fun_ty funty] returns a term of type [('ar -> 'row) Ty.ty], assuming [funty : (('ar -> 'row) Ty.ty, _, _) fun_ty]. *) -val ty_of_fun_ty : - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - ('ar -> 'row) Ty.ty +val get_ret_ty : + ('ar -> 'row) Ty.ty -> ('ar -> 'row, 'ar -> 'urow, 'ret) args -> 'ret Ty.ty (** [get_ret_ty ty l] returns a term of type ['ret Ty.ty] such that if [ty : ('ar -> 'row) Ty.ty] and [l] contains n arguments, ['ar -> 'row] is the arrow type of an n-argument function with co-domain ['ret]. *) -val get_ret_ty : - ('ar -> 'row) Ty.ty -> ('ar -> 'row, 'ar -> 'urow, 'ret) args -> 'ret Ty.ty module type S = sig val typed_printer : 'a Ty.ty -> Format.formatter -> 'a -> unit + val typed_sampler : 'a Ty.ty -> unit -> 'a end (** [Make], used in [Test_lib], provides a generic printer and sampler for argument lists of n-ary functions, depending on their type. *) -module Make : functor (M : S) -> sig +module Make (M : S) : sig val print : - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - Format.formatter -> ('ar -> 'row, 'ar -> 'urow, 'ret) args -> unit + (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty + -> Format.formatter + -> ('ar -> 'row, 'ar -> 'urow, 'ret) args + -> unit + val get_sampler : - (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty -> - unit -> ('ar -> 'row, 'ar -> 'urow, 'ret) args + (('ar -> 'row) Ty.ty, 'ar -> 'urow, 'ret) fun_ty + -> unit + -> ('ar -> 'row, 'ar -> 'urow, 'ret) args end +val apply_args_1 : ('a -> 'b) -> ('a -> 'c, 'a -> unit, 'c) args -> 'b (** [apply_args_1], [apply_args_2], [apply_args3], [apply_args_4] are variants of the [apply] function, assuming a fixed number of args; they have thus a more precise type and are used in [Test_lib]. *) -val apply_args_1 : - ('a -> 'b) -> ('a -> 'c, 'a -> unit, 'c) args -> 'b val apply_args_2 : ('a -> 'b -> 'c) -> ('a -> 'b -> 'd, 'a -> 'b -> unit, 'd) args -> 'c val apply_args_3 : - ('a -> 'b -> 'c -> 'd) -> - ('a -> 'b -> 'c -> 'e, 'a -> 'b -> 'c -> unit, 'e) args -> 'd + ('a -> 'b -> 'c -> 'd) + -> ('a -> 'b -> 'c -> 'e, 'a -> 'b -> 'c -> unit, 'e) args + -> 'd val apply_args_4 : - ('a -> 'b -> 'c -> 'd -> 'e) -> - ('a -> 'b -> 'c -> 'd -> 'f, 'a -> 'b -> 'c -> 'd -> unit, 'f) args -> 'e + ('a -> 'b -> 'c -> 'd -> 'e) + -> ('a -> 'b -> 'c -> 'd -> 'f, 'a -> 'b -> 'c -> 'd -> unit, 'f) args + -> 'e diff --git a/src/ppx-metaquot/genlifter.ml b/src/ppx-metaquot/genlifter.ml index 4c380aa97..45dbde8f0 100644 --- a/src/ppx-metaquot/genlifter.ml +++ b/src/ppx-metaquot/genlifter.ml @@ -2,24 +2,22 @@ (* under the terms of the MIT license (see LICENSE file). *) (* Copyright 2013 Alain Frisch and LexiFi *) - (* Generate code to lift values of a certain type. This illustrates how to build fragments of Parsetree through Ast_helper and more local helper functions. *) module Main : sig end = struct - open Location open Types open Asttypes open Ast_helper open Ast_convenience - let selfcall ?(this = "this") m args = app (Exp.send (evar this) (mknoloc m)) args + let selfcall ?(this = "this") m args = + app (Exp.send (evar this) (mknoloc m)) args (*************************************************************************) - let env = Env.initial_safe_string let clean s = @@ -32,26 +30,31 @@ module Main : sig end = struct let print_fun s = "lift_" ^ clean s let printed = Hashtbl.create 16 + let meths = ref [] + let use_existentials = ref false + let use_arrows = ref false let existential_method = - Cf.(method_ (mknoloc "existential") Public - (virtual_ Typ.(poly [mknoloc "a"] (arrow Nolabel (var "a") (var "res")))) - ) + Cf.( + method_ (mknoloc "existential") Public + (virtual_ + Typ.(poly [mknoloc "a"] (arrow Nolabel (var "a") (var "res"))))) let arrow_method = - Cf.(method_ (mknoloc "arrow") Public - (virtual_ Typ.(poly [mknoloc "a"] (arrow Nolabel (var "a") (var "res")))) - ) + Cf.( + method_ (mknoloc "arrow") Public + (virtual_ + Typ.(poly [mknoloc "a"] (arrow Nolabel (var "a") (var "res"))))) let rec gen ty = if Hashtbl.mem printed ty then () - else let tylid = Longident.parse ty in + else + let tylid = Longident.parse ty in let td = - try Env.find_type (Env.lookup_type tylid env) env - with Not_found -> + try Env.find_type (Env.lookup_type tylid env) env with Not_found -> Format.eprintf "** Cannot resolve type %s@." ty; exit 2 in @@ -63,32 +66,47 @@ module Main : sig end = struct | Lapply _ -> assert false in Hashtbl.add printed ty (); - let sparams = List.mapi (fun i _ -> Printf.sprintf "f%i" i) td.type_params in + let sparams = + List.mapi (fun i _ -> Printf.sprintf "f%i" i) td.type_params + in let params = List.map mknoloc sparams in - let env = List.map2 (fun s t -> t.id, evar s.txt) params td.type_params in - let make_result_t tyargs = Typ.(arrow Asttypes.Nolabel (constr (lid ty) tyargs) (var "res")) in + let env = + List.map2 (fun s t -> (t.id, evar s.txt)) params td.type_params + in + let make_result_t tyargs = + Typ.(arrow Asttypes.Nolabel (constr (lid ty) tyargs) (var "res")) + in let make_t tyargs = List.fold_right (fun arg t -> - Typ.(arrow Asttypes.Nolabel (arrow Asttypes.Nolabel arg (var "res")) t)) + Typ.( + arrow Asttypes.Nolabel (arrow Asttypes.Nolabel arg (var "res")) t) + ) tyargs (make_result_t tyargs) in let tyargs = List.map (fun t -> Typ.var t.txt) params in let t = Typ.poly params (make_t tyargs) in let concrete e = - let e = List.fold_right (fun x e -> lam x e) (List.map (fun x -> pvar x.txt) params) e in + let e = + List.fold_right + (fun x e -> lam x e) + (List.map (fun x -> pvar x.txt) params) + e + in let tyargs = List.map (fun t -> Typ.constr (lid t.txt) []) params in let e = Exp.constraint_ e (make_t tyargs) in let e = List.fold_right (fun x e -> Exp.newtype x e) params e in let body = Exp.poly e (Some t) in - meths := Cf.(method_ (mknoloc (print_fun ty)) Public (concrete Fresh body)) :: !meths + meths := + Cf.(method_ (mknoloc (print_fun ty)) Public (concrete Fresh body)) + :: !meths in let field ld = let s = Ident.name ld.ld_id in - (lid (prefix ^ s), pvar s), - tuple[str s; tyexpr env ld.ld_type (evar s)] + ( (lid (prefix ^ s), pvar s) + , tuple [str s; tyexpr env ld.ld_type (evar s)] ) in - match td.type_kind, td.type_manifest with + match (td.type_kind, td.type_manifest) with | Type_record (l, _), _ -> let l = List.map field l in concrete @@ -100,39 +118,43 @@ module Main : sig end = struct let c = Ident.name cd.cd_id in let qc = prefix ^ c in match cd.cd_args with - | Cstr_tuple (tys) -> + | Cstr_tuple tys -> let p, args = gentuple env tys in - pconstr qc p, selfcall "constr" [str ty; tuple[str c; list args]] - | Cstr_record (l) -> + ( pconstr qc p + , selfcall "constr" [str ty; tuple [str c; list args]] ) + | Cstr_record l -> let l = List.map field l in - pconstr qc [Pat.record (List.map fst l) Closed], - selfcall "constr" [str ty; tuple [str c; - selfcall "record" [str (ty ^ "." ^ c); list (List.map snd l)]]] + ( pconstr qc [Pat.record (List.map fst l) Closed] + , selfcall "constr" + [ str ty + ; tuple + [ str c + ; selfcall "record" + [str (ty ^ "." ^ c); list (List.map snd l)] ] ] ) in concrete (func (List.map case l)) - | Type_abstract, Some t -> - concrete (tyexpr_fun env t) + | Type_abstract, Some t -> concrete (tyexpr_fun env t) | Type_abstract, None -> (* Generate an abstract method to lift abstract types *) - meths := Cf.(method_ (mknoloc (print_fun ty)) Public (virtual_ t)) :: !meths - | Type_open, _ -> - failwith "Open types are not yet supported." + meths := + Cf.(method_ (mknoloc (print_fun ty)) Public (virtual_ t)) :: !meths + | Type_open, _ -> failwith "Open types are not yet supported." and gentuple env tl = let arg i t = let x = Printf.sprintf "x%i" i in - pvar x, tyexpr env t (evar x) + (pvar x, tyexpr env t (evar x)) in List.split (List.mapi arg tl) and tyexpr env ty x = match ty.desc with - | Tvar _ -> - (match List.assoc ty.id env with - | f -> app f [x] - | exception Not_found -> - use_existentials := true; - selfcall "existential" [x]) + | Tvar _ -> ( + match List.assoc ty.id env with + | f -> app f [x] + | exception Not_found -> + use_existentials := true; + selfcall "existential" [x] ) | Ttuple tl -> let p, e = gentuple env tl in let_in [Vb.mk (Pat.tuple p) x] (selfcall "tuple" [list e]) @@ -163,8 +185,7 @@ module Main : sig end = struct Format.eprintf "** Cannot deal with type %a@." Printtyp.type_expr ty; exit 2 - and tyexpr_fun env ty = - lam (pvar "x") (tyexpr env ty (evar "x")) + and tyexpr_fun env ty = lam (pvar "x") (tyexpr env ty (evar "x")) let simplify = (* (fun x -> x) ====> *) @@ -176,53 +197,51 @@ module Main : sig end = struct let open Parsetree in match e.pexp_desc with | Pexp_fun - (Asttypes.Nolabel, None, - {ppat_desc = Ppat_var{txt=id;_};_}, - {pexp_desc = - Pexp_apply - (f, - [Asttypes.Nolabel - ,{pexp_desc= Pexp_ident{txt=Lident id2;_};_}]);_}) - when id = id2 -> f + ( Asttypes.Nolabel + , None + , {ppat_desc = Ppat_var {txt = id; _}; _} + , { pexp_desc = + Pexp_apply + ( f + , [ ( Asttypes.Nolabel + , {pexp_desc = Pexp_ident {txt = Lident id2; _}; _} ) ] ); _ + } ) + when id = id2 -> + f | _ -> e in {super with expr} let args = let open Arg in - [ - "-I", String (fun s -> Config.load_path := Misc.expand_directory Config.standard_library s :: !Config.load_path), - " Add to the list of include directories"; - ] + [ ( "-I" + , String + (fun s -> + Config.load_path := + Misc.expand_directory Config.standard_library s + :: !Config.load_path ) + , " Add to the list of include directories" ) ] - let usage = - Printf.sprintf "%s [options] \n" Sys.argv.(0) + let usage = Printf.sprintf "%s [options] \n" Sys.argv.(0) let main () = Config.load_path := [Config.standard_library]; Arg.parse (Arg.align args) gen usage; let meths = !meths in let meths = - if !use_existentials then - existential_method :: meths - else - meths - in - let meths = - if !use_arrows then - arrow_method :: meths - else - meths + if !use_existentials then existential_method :: meths else meths in + let meths = if !use_arrows then arrow_method :: meths else meths in let cl = Cstr.mk (pvar "this") meths in - let params = [Typ.var "res", Invariant] in - let cl = Ci.mk ~virt:Virtual ~params (mknoloc "lifter") (Cl.structure cl) in + let params = [(Typ.var "res", Invariant)] in + let cl = + Ci.mk ~virt:Virtual ~params (mknoloc "lifter") (Cl.structure cl) + in let s = [Str.class_ [cl]] in - Format.printf "%a@." Pprintast.structure (simplify.Ast_mapper.structure simplify s) + Format.printf "%a@." Pprintast.structure + (simplify.Ast_mapper.structure simplify s) let () = - try main () - with exn -> + try main () with exn -> Printf.eprintf "** fatal error: %s\n%!" (Printexc.to_string exn) - end diff --git a/src/ppx-metaquot/ppx_metaquot.ml b/src/ppx-metaquot/ppx_metaquot.ml index 0be96060c..e50535a88 100644 --- a/src/ppx-metaquot/ppx_metaquot.ml +++ b/src/ppx-metaquot/ppx_metaquot.ml @@ -56,7 +56,9 @@ *) -module Main : sig val expander: string list -> Ast_mapper.mapper end = struct +module Main : sig + val expander : string list -> Ast_mapper.mapper +end = struct open Asttypes open Parsetree open Ast_helper @@ -65,123 +67,152 @@ module Main : sig val expander: string list -> Ast_mapper.mapper end = struct let prefix ty s = let open Longident in match parse ty with - | Ldot(m, _) -> String.concat "." (Longident.flatten m) ^ "." ^ s + | Ldot (m, _) -> String.concat "." (Longident.flatten m) ^ "." ^ s | _ -> s let append ?loc ?attrs e e' = - let fn = Location.mknoloc (Longident.(Ldot (Lident "List", "append"))) in - Exp.apply ?loc ?attrs (Exp.ident fn) [Nolabel, e; Nolabel, e'] + let fn = Location.mknoloc Longident.(Ldot (Lident "List", "append")) in + Exp.apply ?loc ?attrs (Exp.ident fn) [(Nolabel, e); (Nolabel, e')] class exp_builder = object - method record ty x = record (List.map (fun (l, e) -> prefix ty l, e) x) + method record ty x = record (List.map (fun (l, e) -> (prefix ty l, e)) x) + method constr ty (c, args) = constr (prefix ty c) args + method list l = list l + method tuple l = tuple l + method int i = int i + method string s = str s + method char c = char c + method int32 x = Exp.constant (Const.int32 x) + method int64 x = Exp.constant (Const.int64 x) + method nativeint x = Exp.constant (Const.nativeint x) end class pat_builder = object - method record ty x = precord ~closed:Closed (List.map (fun (l, e) -> prefix ty l, e) x) + method record ty x = + precord ~closed:Closed (List.map (fun (l, e) -> (prefix ty l, e)) x) + method constr ty (c, args) = pconstr (prefix ty c) args + method list l = plist l + method tuple l = ptuple l + method int i = pint i + method string s = pstr s + method char c = pchar c + method int32 x = Pat.constant (Const.int32 x) + method int64 x = Pat.constant (Const.int64 x) + method nativeint x = Pat.constant (Const.nativeint x) end - let get_exp loc = function - | PStr [ {pstr_desc=Pstr_eval (e, _); _} ] -> e + | PStr [{pstr_desc = Pstr_eval (e, _); _}] -> e | _ -> - Format.eprintf "%aExpression expected@." - Location.print_error loc; + Format.eprintf "%aExpression expected@." Location.print_error loc; exit 2 let get_typ loc = function | PTyp t -> t | _ -> - Format.eprintf "%aType expected@." - Location.print_error loc; + Format.eprintf "%aType expected@." Location.print_error loc; exit 2 let get_pat loc = function | PPat (t, None) -> t | _ -> - Format.eprintf "%aPattern expected@." - Location.print_error loc; + Format.eprintf "%aPattern expected@." Location.print_error loc; exit 2 let exp_lifter loc map = let map = map.Ast_mapper.expr map in object inherit [_] Ast_lifter.lifter as super + inherit exp_builder (* Special support for location in the generated AST *) method! lift_Location_t _ = loc (* Support for antiquotations *) - method! lift_Parsetree_expression = function - | {pexp_desc=Pexp_extension({txt="e";loc}, e); _} -> map (get_exp loc e) - | x -> super # lift_Parsetree_expression x - - method! lift_Parsetree_pattern = function - | {ppat_desc=Ppat_extension({txt="p";loc}, e); _} -> map (get_exp loc e) - | x -> super # lift_Parsetree_pattern x + method! lift_Parsetree_expression = + function + | {pexp_desc = Pexp_extension ({txt = "e"; loc}, e); _} -> + map (get_exp loc e) + | x -> super#lift_Parsetree_expression x + + method! lift_Parsetree_pattern = + function + | {ppat_desc = Ppat_extension ({txt = "p"; loc}, e); _} -> + map (get_exp loc e) + | x -> super#lift_Parsetree_pattern x method! lift_Parsetree_structure str = List.fold_right (function - | {pstr_desc=Pstr_extension(({txt="s";loc}, e), _); _} -> - append (get_exp loc e) - | x -> - cons (super # lift_Parsetree_structure_item x)) + | {pstr_desc = Pstr_extension (({txt = "s"; loc}, e), _); _} -> + append (get_exp loc e) + | x -> cons (super#lift_Parsetree_structure_item x)) str (nil ()) - method! lift_Parsetree_core_type = function - | {ptyp_desc=Ptyp_extension({txt="t";loc}, e); _} -> map (get_exp loc e) - | x -> super # lift_Parsetree_core_type x + method! lift_Parsetree_core_type = + function + | {ptyp_desc = Ptyp_extension ({txt = "t"; loc}, e); _} -> + map (get_exp loc e) + | x -> super#lift_Parsetree_core_type x end let pat_lifter map = let map = map.Ast_mapper.pat map in object inherit [_] Ast_lifter.lifter as super + inherit pat_builder (* Special support for location and attributes in the generated AST *) method! lift_Location_t _ = Pat.any () + method! lift_Parsetree_attributes _ = Pat.any () (* Support for antiquotations *) - method! lift_Parsetree_expression = function - | {pexp_desc=Pexp_extension({txt="e";loc}, e); _} -> map (get_pat loc e) - | x -> super # lift_Parsetree_expression x - - method! lift_Parsetree_pattern = function - | {ppat_desc=Ppat_extension({txt="p";loc}, e); _} -> map (get_pat loc e) - | x -> super # lift_Parsetree_pattern x - - method! lift_Parsetree_core_type = function - | {ptyp_desc=Ptyp_extension({txt="t";loc}, e); _} -> map (get_pat loc e) - | x -> super # lift_Parsetree_core_type x + method! lift_Parsetree_expression = + function + | {pexp_desc = Pexp_extension ({txt = "e"; loc}, e); _} -> + map (get_pat loc e) + | x -> super#lift_Parsetree_expression x + + method! lift_Parsetree_pattern = + function + | {ppat_desc = Ppat_extension ({txt = "p"; loc}, e); _} -> + map (get_pat loc e) + | x -> super#lift_Parsetree_pattern x + + method! lift_Parsetree_core_type = + function + | {ptyp_desc = Ptyp_extension ({txt = "t"; loc}, e); _} -> + map (get_pat loc e) + | x -> super#lift_Parsetree_core_type x end let loc = ref (app (evar "Pervasives.!") [evar "Ast_helper.default_loc"]) let handle_attr = function - | {txt="metaloc";loc=l}, e -> loc := get_exp l e + | {txt = "metaloc"; loc = l}, e -> loc := get_exp l e | _ -> () let with_loc ?(attrs = []) f = @@ -193,126 +224,108 @@ module Main : sig val expander: string list -> Ast_mapper.mapper end = struct (* ------ ------ *) let ty_of this cty = - let obj_id = - Location.mknoloc - Longident.(Ldot (Lident "Ty", "repr")) in - let ty_id = - Location.mknoloc - Longident.(Ldot (Lident "Ty", "ty")) in - let tyexpr = - ((exp_lifter !loc this) # lift_Parsetree_core_type cty) in + let obj_id = Location.mknoloc Longident.(Ldot (Lident "Ty", "repr")) in + let ty_id = Location.mknoloc Longident.(Ldot (Lident "Ty", "ty")) in + let tyexpr = (exp_lifter !loc this)#lift_Parsetree_core_type cty in Exp.constraint_ - (Exp.apply - (Exp.ident obj_id) - [Nolabel, tyexpr]) + (Exp.apply (Exp.ident obj_id) [(Nolabel, tyexpr)]) (Typ.constr ty_id [cty]) let fun_ty_of this l e = (* Naming convention: ty:=Ty.ty, cty:=core_type, ety:=expression *) let glob_cty = get_typ l e in match glob_cty with - | { Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (_, arg0, next) ; _ } -> - (* OK: The type expression is an arrow type *) - (* Recursion over [core_type]s: *) - let rec get_fun_ty_of arg0 next = - match next with - | { Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (_, arg', next') ; _ } -> - let fun_ty_next, ucty, ret = get_fun_ty_of arg' next' in - (* Arg_ty (arg0, fun_ty_next) : (('a -> 'b -> 'c) Ty.ty, 'a -> 'b -> 'd, 'r) *) - let cons_arg_id = Location.mknoloc Longident.(Ldot (Lident "Fun_ty", "arg_ty")) in - let arg0_ety = ty_of this arg0 in - let ucty = Typ.arrow Nolabel arg0 ucty in - (Exp.apply - (Exp.apply - (Exp.ident cons_arg_id) - [Nolabel, arg0_ety]) - [Nolabel, fun_ty_next], - ucty, ret) - | _ -> - (* Last_ty (arg0, next) : (('a -> 'r) Ty.ty, 'a -> unit, 'r) fun_ty *) - let cons_last_id = Location.mknoloc Longident.(Ldot (Lident "Fun_ty", "last_ty")) in - let arg0_ety = ty_of this arg0 in - let next_ety = ty_of this next in - let unit_id = Location.mknoloc (Longident.Lident "unit") in - let unit_cty = Typ.constr unit_id [] in - let ucty = Typ.arrow Nolabel arg0 unit_cty in - (Exp.apply - (Exp.apply - (Exp.ident cons_last_id) - [Nolabel, arg0_ety]) - [Nolabel, next_ety], - ucty, next) - in - (* Main branch *) - let fun_ty_next, ucty, ret = get_fun_ty_of arg0 next in - (* fun_ty_next : (('a -> 'b -> 'r) Ty.ty, 'a -> 'b -> unit, 'r) [typecast] *) - let fun_ty_id = Location.mknoloc Longident.(Ldot (Lident "Fun_ty", "fun_ty")) in - let ty_id = Location.mknoloc Longident.(Ldot (Lident "Ty", "ty")) in - let glob_cty_ty = Typ.constr ty_id [glob_cty] in - Exp.constraint_ - fun_ty_next - (Typ.constr fun_ty_id [glob_cty_ty; ucty; ret]) + | {Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (_, arg0, next); _} -> + (* OK: The type expression is an arrow type *) + (* Recursion over [core_type]s: *) + let rec get_fun_ty_of arg0 next = + match next with + | {Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (_, arg', next'); _} -> + let fun_ty_next, ucty, ret = get_fun_ty_of arg' next' in + (* Arg_ty (arg0, fun_ty_next) : (('a -> 'b -> 'c) Ty.ty, 'a -> 'b -> 'd, 'r) *) + let cons_arg_id = + Location.mknoloc Longident.(Ldot (Lident "Fun_ty", "arg_ty")) + in + let arg0_ety = ty_of this arg0 in + let ucty = Typ.arrow Nolabel arg0 ucty in + ( Exp.apply + (Exp.apply (Exp.ident cons_arg_id) [(Nolabel, arg0_ety)]) + [(Nolabel, fun_ty_next)] + , ucty + , ret ) + | _ -> + (* Last_ty (arg0, next) : (('a -> 'r) Ty.ty, 'a -> unit, 'r) fun_ty *) + let cons_last_id = + Location.mknoloc Longident.(Ldot (Lident "Fun_ty", "last_ty")) + in + let arg0_ety = ty_of this arg0 in + let next_ety = ty_of this next in + let unit_id = Location.mknoloc (Longident.Lident "unit") in + let unit_cty = Typ.constr unit_id [] in + let ucty = Typ.arrow Nolabel arg0 unit_cty in + ( Exp.apply + (Exp.apply (Exp.ident cons_last_id) [(Nolabel, arg0_ety)]) + [(Nolabel, next_ety)] + , ucty + , next ) + in + (* Main branch *) + let fun_ty_next, ucty, ret = get_fun_ty_of arg0 next in + (* fun_ty_next : (('a -> 'b -> 'r) Ty.ty, 'a -> 'b -> unit, 'r) [typecast] *) + let fun_ty_id = + Location.mknoloc Longident.(Ldot (Lident "Fun_ty", "fun_ty")) + in + let ty_id = Location.mknoloc Longident.(Ldot (Lident "Ty", "ty")) in + let glob_cty_ty = Typ.constr ty_id [glob_cty] in + Exp.constraint_ fun_ty_next + (Typ.constr fun_ty_id [glob_cty_ty; ucty; ret]) | _ -> invalid_arg "fun_ty_of: not an arrow type" + (* ------ ------ *) let expander _args = let open Ast_mapper in let super = default_mapper in let expr this e = - with_loc ~attrs:e.pexp_attributes - (fun () -> - match e.pexp_desc with - | Pexp_extension({txt="expr";loc=l}, e) -> - (exp_lifter !loc this) # lift_Parsetree_expression (get_exp l e) - | Pexp_extension({txt="pat";loc=l}, e) -> - (exp_lifter !loc this) # lift_Parsetree_pattern (get_pat l e) - | Pexp_extension({txt="str";_}, PStr e) -> - (exp_lifter !loc this) # lift_Parsetree_structure e - | Pexp_extension({txt="stri";_}, PStr [e]) -> - (exp_lifter !loc this) # lift_Parsetree_structure_item e - | Pexp_extension({txt="type";loc=l}, e) -> - (exp_lifter !loc this) # lift_Parsetree_core_type (get_typ l e) -(* ------ ------ *) - | Pexp_extension({txt="ty";loc=l}, e) -> + with_loc ~attrs:e.pexp_attributes (fun () -> + match e.pexp_desc with + | Pexp_extension ({txt = "expr"; loc = l}, e) -> + (exp_lifter !loc this)#lift_Parsetree_expression (get_exp l e) + | Pexp_extension ({txt = "pat"; loc = l}, e) -> + (exp_lifter !loc this)#lift_Parsetree_pattern (get_pat l e) + | Pexp_extension ({txt = "str"; _}, PStr e) -> + (exp_lifter !loc this)#lift_Parsetree_structure e + | Pexp_extension ({txt = "stri"; _}, PStr [e]) -> + (exp_lifter !loc this)#lift_Parsetree_structure_item e + | Pexp_extension ({txt = "type"; loc = l}, e) -> + (exp_lifter !loc this)#lift_Parsetree_core_type (get_typ l e) + (* ------ ------ *) + | Pexp_extension ({txt = "ty"; loc = l}, e) -> let ty = get_typ l e in ty_of this ty - | Pexp_extension({txt="funty";loc=l}, e) -> - fun_ty_of this l e -(* ------ ------ *) - | _ -> - super.expr this e - ) + | Pexp_extension ({txt = "funty"; loc = l}, e) -> fun_ty_of this l e + (* ------ ------ *) + | _ -> super.expr this e ) and pat this p = - with_loc ~attrs:p.ppat_attributes - (fun () -> - match p.ppat_desc with - | Ppat_extension({txt="expr";loc=l}, e) -> - (pat_lifter this) # lift_Parsetree_expression (get_exp l e) - | Ppat_extension({txt="pat";loc=l}, e) -> - (pat_lifter this) # lift_Parsetree_pattern (get_pat l e) - | Ppat_extension({txt="str";_}, PStr e) -> - (pat_lifter this) # lift_Parsetree_structure e - | Ppat_extension({txt="stri";_}, PStr [e]) -> - (pat_lifter this) # lift_Parsetree_structure_item e - | Ppat_extension({txt="type";loc=l}, e) -> - (pat_lifter this) # lift_Parsetree_core_type (get_typ l e) - | _ -> - super.pat this p - ) - and structure this l = - with_loc - (fun () -> super.structure this l) - + with_loc ~attrs:p.ppat_attributes (fun () -> + match p.ppat_desc with + | Ppat_extension ({txt = "expr"; loc = l}, e) -> + (pat_lifter this)#lift_Parsetree_expression (get_exp l e) + | Ppat_extension ({txt = "pat"; loc = l}, e) -> + (pat_lifter this)#lift_Parsetree_pattern (get_pat l e) + | Ppat_extension ({txt = "str"; _}, PStr e) -> + (pat_lifter this)#lift_Parsetree_structure e + | Ppat_extension ({txt = "stri"; _}, PStr [e]) -> + (pat_lifter this)#lift_Parsetree_structure_item e + | Ppat_extension ({txt = "type"; loc = l}, e) -> + (pat_lifter this)#lift_Parsetree_core_type (get_typ l e) + | _ -> super.pat this p ) + and structure this l = with_loc (fun () -> super.structure this l) and structure_item this x = - begin match x.pstr_desc with - | Pstr_attribute x -> handle_attr x - | _ -> () - end; + (match x.pstr_desc with Pstr_attribute x -> handle_attr x | _ -> ()); super.structure_item this x - in {super with expr; pat; structure; structure_item} - end let expander = Main.expander diff --git a/src/ppx-metaquot/ppx_metaquot_main.ml b/src/ppx-metaquot/ppx_metaquot_main.ml index 5d65692bf..691130e0e 100644 --- a/src/ppx-metaquot/ppx_metaquot_main.ml +++ b/src/ppx-metaquot/ppx_metaquot_main.ml @@ -1,3 +1,4 @@ let () = - Migrate_parsetree.Driver.register ~name:"ppx_metaquot" (module Migrate_parsetree.OCaml_405) + Migrate_parsetree.Driver.register ~name:"ppx_metaquot" + (module Migrate_parsetree.OCaml_405) (fun _config _cookies -> Ppx_metaquot.expander []) diff --git a/src/ppx-metaquot/ty.ml b/src/ppx-metaquot/ty.ml index d8b85e182..51576da56 100644 --- a/src/ppx-metaquot/ty.ml +++ b/src/ppx-metaquot/ty.ml @@ -7,44 +7,48 @@ * included LICENSE file for details. *) type repr = Parsetree.core_type + type 'a ty = Ty of repr let obj (Ty ty) = ty -let repr ty = Ty (ty) +let repr ty = Ty ty -let print (Ty ty) = - Format.asprintf "%a%!" Pprintast.core_type ty +let print (Ty ty) = Format.asprintf "%a%!" Pprintast.core_type ty let domains = function - | Ty { Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (_, arg, ret) ; _ } -> + | Ty {Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (_, arg, ret); _} -> (Ty arg, Ty ret) | _ -> invalid_arg "Ty.domains" let curry (Ty arg) (Ty ret) = - Ty { Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (Asttypes.Nolabel, arg, ret) ; - ptyp_loc = Location.none ; - ptyp_attributes = [] } + Ty + { Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (Asttypes.Nolabel, arg, ret) + ; ptyp_loc = Location.none + ; ptyp_attributes = [] } let pair2 (Ty t1) (Ty t2) = - Ty {Parsetree.ptyp_desc = Parsetree.Ptyp_tuple [t1; t2]; - ptyp_loc = Location.none; - ptyp_attributes = []} + Ty + { Parsetree.ptyp_desc = Parsetree.Ptyp_tuple [t1; t2] + ; ptyp_loc = Location.none + ; ptyp_attributes = [] } let pair3 (Ty t1) (Ty t2) (Ty t3) = - Ty {Parsetree.ptyp_desc = Parsetree.Ptyp_tuple [t1; t2; t3]; - ptyp_loc = Location.none; - ptyp_attributes = []} + Ty + { Parsetree.ptyp_desc = Parsetree.Ptyp_tuple [t1; t2; t3] + ; ptyp_loc = Location.none + ; ptyp_attributes = [] } let pair4 (Ty t1) (Ty t2) (Ty t3) (Ty t4) = - Ty {Parsetree.ptyp_desc = Parsetree.Ptyp_tuple [t1; t2; t3; t4]; - ptyp_loc = Location.none; - ptyp_attributes = []} + Ty + { Parsetree.ptyp_desc = Parsetree.Ptyp_tuple [t1; t2; t3; t4] + ; ptyp_loc = Location.none + ; ptyp_attributes = [] } let lst (Ty ty) = - Ty {Parsetree.ptyp_desc = - Parsetree.Ptyp_constr ({Asttypes.txt = Longident.Lident "list"; - loc = Location.none}, - [ty]); - ptyp_loc = Location.none; - ptyp_attributes = []} + Ty + { Parsetree.ptyp_desc = + Parsetree.Ptyp_constr + ({Asttypes.txt = Longident.Lident "list"; loc = Location.none}, [ty]) + ; ptyp_loc = Location.none + ; ptyp_attributes = [] } diff --git a/src/ppx-metaquot/ty.mli b/src/ppx-metaquot/ty.mli index e000dff50..8ff467bfa 100644 --- a/src/ppx-metaquot/ty.mli +++ b/src/ppx-metaquot/ty.mli @@ -13,14 +13,23 @@ *) type repr + type 'a ty = Ty of repr -val obj: 'a ty -> Parsetree.core_type -val repr: Parsetree.core_type -> 'a ty -val print: 'a ty -> string -val domains: ('a -> 'b) ty -> 'a ty * 'b ty -val curry: 'a ty -> 'b ty -> ('a -> 'b) ty -val pair2: 'a ty -> 'b ty -> ('a * 'b) ty -val pair3: 'a ty -> 'b ty -> 'c ty -> ('a * 'b * 'c) ty -val pair4: 'a ty -> 'b ty -> 'c ty -> 'd ty -> ('a * 'b * 'c * 'd) ty -val lst: 'a ty -> ('a list) ty +val obj : 'a ty -> Parsetree.core_type + +val repr : Parsetree.core_type -> 'a ty + +val print : 'a ty -> string + +val domains : ('a -> 'b) ty -> 'a ty * 'b ty + +val curry : 'a ty -> 'b ty -> ('a -> 'b) ty + +val pair2 : 'a ty -> 'b ty -> ('a * 'b) ty + +val pair3 : 'a ty -> 'b ty -> 'c ty -> ('a * 'b * 'c) ty + +val pair4 : 'a ty -> 'b ty -> 'c ty -> 'd ty -> ('a * 'b * 'c * 'd) ty + +val lst : 'a ty -> 'a list ty diff --git a/src/repo/learnocaml_exercise.ml b/src/repo/learnocaml_exercise.ml index 74f329e2a..8ce3063cd 100644 --- a/src/repo/learnocaml_exercise.ml +++ b/src/repo/learnocaml_exercise.ml @@ -9,35 +9,64 @@ type id = string type t = - { id : id ; - prelude : string ; - template : string ; - descr : (string * string) list ; - prepare : string ; - test : string ; - solution : string ; - max_score : int ; - depend : string option ; - dependencies : string list; - } + { id : id + ; prelude : string + ; template : string + ; descr : (string * string) list + ; prepare : string + ; test : string + ; solution : string + ; max_score : int + ; depend : string option + ; dependencies : string list } let encoding = let open Json_encoding in conv - (fun { id ; prelude ; template ; descr ; prepare ; test ; solution ; max_score ; depend ; dependencies} -> - id, prelude, template, descr, prepare, test, solution, max_score,depend, dependencies) - (fun (id, prelude, template, descr, prepare, test, solution, max_score,depend, dependencies) -> - { id ; prelude ; template ; descr ; prepare ; test ; solution ; max_score ; depend ; dependencies}) - (obj10 - (req "id" string) - (req "prelude" string) - (req "template" string) + (fun { id + ; prelude + ; template + ; descr + ; prepare + ; test + ; solution + ; max_score + ; depend + ; dependencies } -> + ( id + , prelude + , template + , descr + , prepare + , test + , solution + , max_score + , depend + , dependencies ) ) + (fun ( id + , prelude + , template + , descr + , prepare + , test + , solution + , max_score + , depend + , dependencies ) -> + { id + ; prelude + ; template + ; descr + ; prepare + ; test + ; solution + ; max_score + ; depend + ; dependencies } ) + (obj10 (req "id" string) (req "prelude" string) (req "template" string) (req "descr" (list (tup2 string string))) - (req "prepare" string) - (req "test" string) - (req "solution" string) - (req "max-score" int) - (opt "depend" (string)) + (req "prepare" string) (req "test" string) (req "solution" string) + (req "max-score" int) (opt "depend" string) (dft "dependencies" (list string) [])) (* let meta_from_string m = @@ -57,78 +86,79 @@ let descrs_from_string descrs = let descrs_to_string s = Json_encoding.(construct (list (tup2 string string))) s - |> (function - | `A _ | `O _ as d -> d - | v -> `A [ v ]) + |> (function (`A _ | `O _) as d -> d | v -> `A [v]) |> Ezjsonm.to_string ~minify:true module type Concur = sig type 'a t - val (>>=) : 'a t -> ('a -> 'b t) -> 'b t + + val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t + val return : 'a -> 'a t + val fail : exn -> 'a t + val join : unit t list -> unit t end module Seq = struct type 'a t = 'a - let (>>=) x f = f x + + let ( >>= ) x f = f x + let return x = x + let fail = raise + let join _ = () end module File = struct - module StringMap = Map.Make (String) + type files = string StringMap.t type 'a file = - { key : string ; - ciphered : bool ; - decode : string -> 'a ; - encode : 'a -> string ; - field : t -> 'a ; - update : 'a -> t -> t ; - } + { key : string + ; ciphered : bool + ; decode : string -> 'a + ; encode : 'a -> string + ; field : t -> 'a + ; update : 'a -> t -> t } exception Missing_file of string - let get { key ; ciphered ; decode ; _ } ex = + let get {key; ciphered; decode; _} ex = try let raw = StringMap.find key ex in if ciphered then - let prefix = - Digest.string (StringMap.find "id" ex ^ "_" ^ key) in + let prefix = Digest.string (StringMap.find "id" ex ^ "_" ^ key) in decode (Learnocaml_xor.decode ~prefix raw) - else - decode raw + else decode raw with Not_found -> raise (Missing_file ("get " ^ key)) let get_opt file ex = - try (* a missing file here is necessarily [file] *) - get file ex - with Missing_file _ -> None + try (* a missing file here is necessarily [file] *) + get file ex with Missing_file _ -> None - let has { key ; _ } ex = - StringMap.mem key ex + let has {key; _} ex = StringMap.mem key ex - let set { key ; ciphered ; encode ; _ } raw ex = + let set {key; ciphered; encode; _} raw ex = if ciphered then - let prefix = - Digest.string (StringMap.find "id" ex ^ "_" ^ key) in + let prefix = Digest.string (StringMap.find "id" ex ^ "_" ^ key) in StringMap.add key (Learnocaml_xor.encode ~prefix (encode raw)) ex - else - StringMap.add key (encode raw) ex + else StringMap.add key (encode raw) ex let key file = file.key let id = - { key = "id" ; ciphered = false ; - decode = (fun v -> v) ; encode = (fun v -> v) ; - field = (fun ex -> ex.id) ; - update = (fun id ex -> { ex with id }) - } + { key = "id" + ; ciphered = false + ; decode = (fun v -> v) + ; encode = (fun v -> v) + ; field = (fun ex -> ex.id) + ; update = (fun id ex -> {ex with id}) } + (* let meta = * { key = "meta.json" ; ciphered = false ; * decode = meta_from_string ; encode = meta_to_string ; @@ -148,57 +178,70 @@ module File = struct * } *) let max_score = let key = "max_score.txt" in - { key ; ciphered = false ; - decode = (fun v -> int_of_string v) ; encode = (fun v -> string_of_int v) ; - field = (fun ex -> ex.max_score); - update = (fun max_score ex -> { ex with max_score }); - } + { key + ; ciphered = false + ; decode = (fun v -> int_of_string v) + ; encode = (fun v -> string_of_int v) + ; field = (fun ex -> ex.max_score) + ; update = (fun max_score ex -> {ex with max_score}) } + let prelude = - { key = "prelude.ml" ; ciphered = false ; - decode = (fun v -> v) ; encode = (fun v -> v) ; - field = (fun ex -> ex.prelude) ; - update = (fun prelude ex -> { ex with prelude }) - } + { key = "prelude.ml" + ; ciphered = false + ; decode = (fun v -> v) + ; encode = (fun v -> v) + ; field = (fun ex -> ex.prelude) + ; update = (fun prelude ex -> {ex with prelude}) } + let template = - { key = "template.ml" ; ciphered = false ; - decode = (fun v -> v) ; encode = (fun v -> v) ; - field = (fun ex -> ex.template) ; - update = (fun template ex -> { ex with template }) - } + { key = "template.ml" + ; ciphered = false + ; decode = (fun v -> v) + ; encode = (fun v -> v) + ; field = (fun ex -> ex.template) + ; update = (fun template ex -> {ex with template}) } + let descr : (string * string) list file = - { key = "descr.html" ; ciphered = false ; - decode = descrs_from_string ; encode = descrs_to_string ; - field = (fun ex -> ex.descr) ; - update = (fun descr ex -> { ex with descr }) - } + { key = "descr.html" + ; ciphered = false + ; decode = descrs_from_string + ; encode = descrs_to_string + ; field = (fun ex -> ex.descr) + ; update = (fun descr ex -> {ex with descr}) } + let prepare = - { key = "prepare.ml" ; ciphered = true ; - decode = (fun v -> v) ; encode = (fun v -> v) ; - field = (fun ex -> ex.prepare) ; - update = (fun prepare ex -> { ex with prepare }) - } + { key = "prepare.ml" + ; ciphered = true + ; decode = (fun v -> v) + ; encode = (fun v -> v) + ; field = (fun ex -> ex.prepare) + ; update = (fun prepare ex -> {ex with prepare}) } + let test = - { key = "test.ml" ; ciphered = true ; - decode = (fun v -> v) ; encode = (fun v -> v) ; - field = (fun ex -> ex.test) ; - update = (fun test ex -> { ex with test }) - } + { key = "test.ml" + ; ciphered = true + ; decode = (fun v -> v) + ; encode = (fun v -> v) + ; field = (fun ex -> ex.test) + ; update = (fun test ex -> {ex with test}) } + let solution = - { key = "solution.ml" ; ciphered = true ; - decode = (fun v -> v) ; encode = (fun v -> v) ; - field = (fun ex -> ex.solution) ; - update = (fun solution ex -> { ex with solution }) - } + { key = "solution.ml" + ; ciphered = true + ; decode = (fun v -> v) + ; encode = (fun v -> v) + ; field = (fun ex -> ex.solution) + ; update = (fun solution ex -> {ex with solution}) } let depend = - { key = "depend.txt" ; ciphered = false ; - decode = (fun v -> Some v) ; - encode = (function - | None -> "" (* no `depend` ~ empty `depend` *) - | Some txt -> txt) ; - field = (fun ex -> ex.depend) ; - update = (fun depend ex -> { ex with depend }) - } + { key = "depend.txt" + ; ciphered = false + ; decode = (fun v -> Some v) + ; encode = + (function + | None -> "" (* no `depend` ~ empty `depend` *) | Some txt -> txt) + ; field = (fun ex -> ex.depend) + ; update = (fun depend ex -> {ex with depend}) } (* [parse_dependencies txt] extracts dependencies from the string [txt]. Dependencies are file names separated by at least one line break. @@ -206,41 +249,51 @@ module File = struct and ending by a line break. *) let parse_dependencies txt = let remove_comment ~start:c line = - match String.index_opt line c with - | None -> line - | Some index -> String.sub line 0 index in + match String.index_opt line c with + | None -> line + | Some index -> String.sub line 0 index + in let lines = String.split_on_char '\n' txt in - List.filter ((<>) "") @@ - List.map (fun line -> String.trim (remove_comment ~start:'#' line)) lines + List.filter (( <> ) "") + @@ List.map + (fun line -> String.trim (remove_comment ~start:'#' line)) + lines let dependencies = function | None -> [] | Some txt -> - let filenames = parse_dependencies txt in - List.mapi - (fun pos filename -> - { key = filename ; ciphered = true ; - decode = (fun v -> v) ; encode = (fun v -> v) ; - field = (fun ex -> List.nth ex.dependencies pos) ; - update = (fun v ex -> - let dependencies = - List.mapi (fun i v' -> if i = pos then v else v') - ex.dependencies in { ex with dependencies }) }) - filenames - + let filenames = parse_dependencies txt in + List.mapi + (fun pos filename -> + { key = filename + ; ciphered = true + ; decode = (fun v -> v) + ; encode = (fun v -> v) + ; field = (fun ex -> List.nth ex.dependencies pos) + ; update = + (fun v ex -> + let dependencies = + List.mapi + (fun i v' -> if i = pos then v else v') + ex.dependencies + in + {ex with dependencies} ) } ) + filenames + module MakeReader (Concur : Concur) = struct - let read ~read_field ?id: ex_id ?(decipher = true) () = + let read ~read_field ?id:ex_id ?(decipher = true) () = let open Concur in let ex = ref StringMap.empty in - read_field id.key >>= fun pr_id -> - begin match ex_id, pr_id with - | None, None -> fail (Failure "Exercise.read: missing id") - | Some id, None | None, Some id -> return id - | Some id, Some id' -> - if id = id' then return id else - fail (Failure "Exercise.read: conficting ids") - end >>= fun ex_id -> - ex := set id ex_id !ex ; + read_field id.key + >>= fun pr_id -> + ( match (ex_id, pr_id) with + | None, None -> fail (Failure "Exercise.read: missing id") + | Some id, None | None, Some id -> return id + | Some id, Some id' -> + if id = id' then return id + else fail (Failure "Exercise.read: conficting ids") ) + >>= fun ex_id -> + ex := set id ex_id !ex; (* read_field meta.key >>= * begin function * None -> fail (Missing_file meta.key) @@ -248,20 +301,21 @@ module File = struct * return (meta_from_string meta_json) * end >>= fun meta_json -> * ex := set meta meta_json !ex; *) - let read_file ({ key ; ciphered ; decode ; _ } as field) = - read_field key >>= function + let read_file ({key; ciphered; decode; _} as field) = + read_field key + >>= function | Some raw -> let deciphered = if ciphered && decipher then - let prefix = - Digest.string (ex_id ^ "_" ^ key) in + let prefix = Digest.string (ex_id ^ "_" ^ key) in Learnocaml_xor.decode ~prefix raw - else - raw in + else raw + in (* decode / encode now to catch malformed fields earlier *) - ex := set field (decode deciphered) !ex ; + ex := set field (decode deciphered) !ex; return () - | None -> return () in + | None -> return () + in (* let read_title () = * match meta_json.meta_title with * Some t -> @@ -292,140 +346,143 @@ module File = struct let descrs = ref [] in let rec read_descr lang = function | [] -> - (* If there are no extensions to try, we just give up. *) - return () - | (ext, f) :: other_exts -> - (* If there are, we create the [filename] containing the + (* If there are no extensions to try, we just give up. *) + return () + | (ext, f) :: other_exts -> ( + (* If there are, we create the [filename] containing the given [lang] and the first [ext] of the list (as well as the function to apply to its result [f]. *) - let filename = - (Filename.remove_extension descr.key) - ^ (match lang with - | None -> "" - | Some lang -> "." ^ lang) - ^ ext - in - (* And we try to read that file. *) - read_field filename - >>= function - | None -> - (* If it does not work, we go on and try other extensions. *) - read_descr lang other_exts - | Some raw -> - (* If it does, we apply the function, add the + let filename = + Filename.remove_extension descr.key + ^ (match lang with None -> "" | Some lang -> "." ^ lang) + ^ ext + in + (* And we try to read that file. *) + read_field filename + >>= function + | None -> + (* If it does not work, we go on and try other extensions. *) + read_descr lang other_exts + | Some raw -> + (* If it does, we apply the function, add the description to [!descrs] and return. *) - descrs := (lang, f raw) :: !descrs; - return () + descrs := (lang, f raw) :: !descrs; + return () ) + in + let override_url = function + | Omd_representation.Url (href, s, title) -> + if String.length href > 0 then + if Char.equal href.[0] '#' then None + else + let title_url = + if title <> "" then + Printf.sprintf {| title="%s"|} + (Omd_utils.htmlentities ~md:true title) + else "" + in + let html = + Printf.sprintf + {|%s|} + (Omd_utils.htmlentities ~md:true href) + title_url (Omd_backend.html_of_md s) + in + Some html + else None + | _ -> None in - let override_url = function - | Omd_representation.Url(href,s,title) -> - if String.length href > 0 then - if Char.equal (String.get href 0) '#' then - None - else - let title_url = - if title <> "" then Printf.sprintf {| title="%s"|} - (Omd_utils.htmlentities ~md:true title) else "" in - let html = - Printf.sprintf - {|%s|} - (Omd_utils.htmlentities ~md:true href) title_url - (Omd_backend.html_of_md s) in - Some html - else None - | _ -> None in let markdown_to_html md = Omd.(md |> of_string |> to_html ~override:override_url) in let read_descrs () = let langs = [] in - let exts = [ - (Filename.extension descr.key, fun h -> h) ; - (".md", markdown_to_html) - ] in - join (read_descr None exts :: List.map (fun l -> read_descr (Some l) exts) langs) + let exts = + [ (Filename.extension descr.key, fun h -> h) + ; (".md", markdown_to_html) ] + in + join + ( read_descr None exts + :: List.map (fun l -> read_descr (Some l) exts) langs ) >>= fun () -> - ex := set descr - (List.map (function (None, v) -> "", v | (Some l, v) -> l, v) !descrs) - !ex; + ex := + set descr + (List.map + (function None, v -> ("", v) | Some l, v -> (l, v)) + !descrs) + !ex; return () in join [ (* read_title () ; *) - read_file prelude ; - read_file template ; - read_descrs () ; - read_file prepare ; - read_file solution ; - read_file test ; - read_file depend ; - (* read_max_score () *) ] >>= fun () -> - join (List.map read_file (dependencies (get_opt depend !ex))) >>= fun () -> - return !ex + read_file prelude + ; read_file template + ; read_descrs () + ; read_file prepare + ; read_file solution + ; read_file test + ; read_file depend + (* read_max_score () *) ] + >>= fun () -> + join (List.map read_file (dependencies (get_opt depend !ex))) + >>= fun () -> return !ex end include MakeReader (Seq) end -let access f ex = - f.File.field ex +let access f ex = f.File.field ex let decipher f ex = let open File in let raw = f.field ex in if f.ciphered then - let prefix = - Digest.string (ex.id ^ "_" ^ f.key) in + let prefix = Digest.string (ex.id ^ "_" ^ f.key) in f.decode (Learnocaml_xor.decode ~prefix raw) - else - f.decode raw + else f.decode raw -let update f v ex = - f.File.update v ex +let update f v ex = f.File.update v ex let cipher f v ex = let open File in if f.ciphered then - let prefix = - Digest.string (ex.id ^ "_" ^ f.key) in + let prefix = Digest.string (ex.id ^ "_" ^ f.key) in f.update (Learnocaml_xor.encode ~prefix (f.encode v)) ex - else - f.update (f.encode v) ex + else f.update (f.encode v) ex let field_from_file file files = - try File.(StringMap.find file.key files |> file.decode) - with Not_found -> raise File.(Missing_file file.key) + try File.(StringMap.find file.key files |> file.decode) with Not_found -> + raise File.(Missing_file file.key) module MakeReaderAnddWriter (Concur : Concur) = struct - - module FileReader = File.MakeReader(Concur) + module FileReader = File.MakeReader (Concur) let read ~read_field ?id ?decipher () = let open Concur in - FileReader.read ~read_field ?id ?decipher () >>= fun ex -> + FileReader.read ~read_field ?id ?decipher () + >>= fun ex -> try let depend = File.get_opt File.depend ex in return - { id = field_from_file File.id ex; - (* meta = field_from_file File.meta ex; *) - prelude = field_from_file File.prelude ex ; - template = field_from_file File.template ex ; - descr = field_from_file File.descr ex ; - prepare = field_from_file File.prepare ex ; - test = field_from_file File.test ex ; - solution = field_from_file File.solution ex ; - max_score = 0 ; - depend ; - dependencies = - let field_from_dependency file = - try field_from_file file ex - with File.Missing_file msg - -> let msg' = msg ^ ": dependency declared in " - ^ File.(key depend) ^ ", but not found" in - raise (File.Missing_file msg') - in - List.map field_from_dependency (File.dependencies depend) - } + { id = field_from_file File.id ex + ; (* meta = field_from_file File.meta ex; *) + prelude = field_from_file File.prelude ex + ; template = field_from_file File.template ex + ; descr = field_from_file File.descr ex + ; prepare = field_from_file File.prepare ex + ; test = field_from_file File.test ex + ; solution = field_from_file File.solution ex + ; max_score = 0 + ; depend + ; dependencies = + (let field_from_dependency file = + try field_from_file file ex with File.Missing_file msg -> + let msg' = + msg ^ ": dependency declared in " + ^ File.(key depend) + ^ ", but not found" + in + raise (File.Missing_file msg') + in + List.map field_from_dependency (File.dependencies depend)) } with File.Missing_file _ as e -> fail e let write ~write_field ex ?(cipher = true) acc = @@ -433,40 +490,42 @@ module MakeReaderAnddWriter (Concur : Concur) = struct let open File in let acc = ref acc in let ex_id = ex.id in - let write_field { key ; ciphered ; encode ; field ; _ } = + let write_field {key; ciphered; encode; field; _} = try let raw = field ex |> encode in - let ciphered = if ciphered && (not cipher) then - let prefix = - Digest.string (ex_id ^ "_" ^ key) in + let ciphered = + if ciphered && not cipher then + let prefix = Digest.string (ex_id ^ "_" ^ key) in Learnocaml_xor.decode ~prefix raw - else - raw in - write_field key ciphered !acc >>= fun nacc -> - acc := nacc ; + else raw + in + write_field key ciphered !acc + >>= fun nacc -> + acc := nacc; return () - with Not_found -> Concur.return () in + with Not_found -> Concur.return () + in join - ([ write_field id ; - (* write_field meta ; + ( [ write_field id + ; (* write_field meta ; * write_field title ; *) - write_field prelude ; - write_field template ; - write_field descr ; - write_field prepare ; - write_field solution ; - write_field test ; - write_field depend ; - (* write_field max_score *) ] - @ (List.map write_field (dependencies (access depend ex))) ) - >>= fun () -> - return !acc + write_field prelude + ; write_field template + ; write_field descr + ; write_field prepare + ; write_field solution + ; write_field test + ; write_field depend + (* write_field max_score *) ] + @ List.map write_field (dependencies (access depend ex)) ) + >>= fun () -> return !acc end include MakeReaderAnddWriter (Seq) - module LwtReaderAnddWriter = MakeReaderAnddWriter (Lwt) + let read_lwt = LwtReaderAnddWriter.read + let write_lwt = LwtReaderAnddWriter.write let enc = encoding diff --git a/src/repo/learnocaml_exercise.mli b/src/repo/learnocaml_exercise.mli index 1bb8809a7..fbeeeecb7 100644 --- a/src/repo/learnocaml_exercise.mli +++ b/src/repo/learnocaml_exercise.mli @@ -15,11 +15,10 @@ type id = string (* JSON encoding of the exercise representation. Includes cipher and decipher at at encoding and decoding. *) -val encoding: t Json_encoding.encoding +val encoding : t Json_encoding.encoding (** Intermediate representation of files, resulting of reading the exercise directory *) module File : sig - (** The exercise parts required by the exercise page and the grader. Not the metadata from the repository. *) type files @@ -30,96 +29,100 @@ module File : sig (** Get was called on a missing undefaulted field *) exception Missing_file of string + val get : 'a file -> files -> 'a (** Access a field in an exercise, may raise [Missing_file] *) - val get: 'a file -> files -> 'a - (** Access an optional field in an exercise *) val get_opt : 'a option file -> files -> 'a option + (** Access an optional field in an exercise *) + val has : 'a file -> files -> bool (** Check the existence of a field in an exercise *) - val has: 'a file -> files -> bool + val set : 'a file -> 'a -> files -> files (** Access a field in an exercise *) - val set: 'a file -> 'a -> files -> files + val key : 'a file -> string (** Returns the key (i.e. then name) of a file *) - val key: 'a file -> string + val id : id file (** Learnocaml_exercise id accessor *) - val id: id file (* (\** Learnocaml_exercise title / name accessor *\) * val title: string file *) + val max_score : int file (** Maximum score for the exercise *) - val max_score: int file + val prepare : string file (** Returns the (private, already deciphered) [prepare.ml] *) - val prepare: string file + val solution : string file (** Returns the (private, already deciphered) [solution.ml] *) - val solution: string file + val test : string file (** Returns the (private, already deciphered) [test.ml] *) - val test: string file + val prelude : string file (** Returns the (public) [prelude.ml] *) - val prelude: string file + val template : string file (** Returns the (public) [template.ml] *) - val template: string file + val descr : (string * string) list file (** Returns the (public) [descr.html] *) - val descr: (string * string) list file + val depend : string option file (** Returns the (public) depend file *) - val depend: string option file + val dependencies : string option -> string file list (** [dependencies txt] create the (private, already deciphered) dependencies declared in [txt] *) - val dependencies: string option -> string file list end +val access : 'a File.file -> t -> 'a (** Access a field from the exercise, using the [t] representation, without ** deciphering it. May raise [Missing_file] if the field is optional and set to [None]. *) -val access: 'a File.file -> t -> 'a +val decipher : string File.file -> t -> string (** Access a string field from the exercise, using the [t] representation, and deciphers if necessary. May raise [Missing_file] if the field is optional and set to [None]. *) -val decipher: string File.file -> t -> string +val update : 'a File.file -> 'a -> t -> t (** Updates the value of a field of the exercise in its [t] representation. *) -val update: 'a File.file -> 'a -> t -> t +val cipher : string File.file -> string -> t -> t (** Updates the value of a field of the exercise in its [t] representation, and ciphers it. *) -val cipher: string File.file -> string -> t -> t +val read : + read_field:(string -> string option) + -> ?id:string + -> ?decipher:bool + -> unit + -> t (** Reader and decipherer *) -val read: - read_field:(string -> string option) -> - ?id:string -> ?decipher:bool -> unit -> - t +val write : + write_field:(string -> string -> 'a -> 'a) -> t -> ?cipher:bool -> 'a -> 'a (** Writer and cipherer, ['a] can be [unit] *) -val write: - write_field:(string -> string -> 'a -> 'a) -> - t -> ?cipher:bool -> 'a -> - 'a +val read_lwt : + read_field:(string -> string option Lwt.t) + -> ?id:string + -> ?decipher:bool + -> unit + -> t Lwt.t (** Reader and decipherer with {!Lwt} *) -val read_lwt: - read_field:(string -> string option Lwt.t) -> - ?id:string -> ?decipher:bool -> unit -> - t Lwt.t +val write_lwt : + write_field:(string -> string -> 'a -> 'a Lwt.t) + -> t + -> ?cipher:bool + -> 'a + -> 'a Lwt.t (** Writer and cipherer with {!Lwt}, ['a] can be [unit] *) -val write_lwt: - write_field:(string -> string -> 'a -> 'a Lwt.t) -> - t -> ?cipher:bool -> 'a -> - 'a Lwt.t -(** JSON serializer, with {!id} file included *) val enc : t Json_encoding.encoding +(** JSON serializer, with {!id} file included *) diff --git a/src/repo/learnocaml_index.mli b/src/repo/learnocaml_index.mli index 214d63897..b3884fcf1 100644 --- a/src/repo/learnocaml_index.mli +++ b/src/repo/learnocaml_index.mli @@ -6,9 +6,9 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) +val exercise_index_path : string (** the following are relative paths to the www root, using [/] as path separator *) -val exercise_index_path : string val exercises_dir : string diff --git a/src/repo/learnocaml_lesson.ml b/src/repo/learnocaml_lesson.ml index 8adea2c24..faaab062d 100644 --- a/src/repo/learnocaml_lesson.ml +++ b/src/repo/learnocaml_lesson.ml @@ -6,39 +6,34 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -type lesson = - { lesson_title : string ; - lesson_steps : step list } -and step = - { step_title : string ; - step_phrases : phrase list } -and phrase = - | Text of string - | Code of string +type lesson = {lesson_title : string; lesson_steps : step list} + +and step = {step_title : string; step_phrases : phrase list} + +and phrase = Text of string | Code of string open Json_encoding let lesson_enc = - Learnocaml_index.check_version_2 @@ - conv - (fun { lesson_title ; lesson_steps } -> (lesson_title, lesson_steps)) - (fun (lesson_title, lesson_steps) -> { lesson_title ; lesson_steps }) @@ - obj2 - (req "title" string) - (req "steps" - (list @@ - conv - (fun { step_title ; step_phrases } -> (step_title, step_phrases)) - (fun (step_title, step_phrases) -> { step_title ; step_phrases }) @@ - (obj2 - (req "title" string) - (req "contents" - (list @@ union - [ case - (obj1 (req "html" string)) - (function Text text -> Some text | Code _ -> None) - (fun text -> Text text) ; - case - (obj1 (req "code" string)) - (function Code code -> Some code | Text _ -> None) - (fun code -> Code code) ]))))) + Learnocaml_index.check_version_2 + @@ conv + (fun {lesson_title; lesson_steps} -> (lesson_title, lesson_steps)) + (fun (lesson_title, lesson_steps) -> {lesson_title; lesson_steps}) + @@ obj2 (req "title" string) + (req "steps" + ( list + @@ conv + (fun {step_title; step_phrases} -> (step_title, step_phrases)) + (fun (step_title, step_phrases) -> {step_title; step_phrases}) + @@ obj2 (req "title" string) + (req "contents" + ( list + @@ union + [ case + (obj1 (req "html" string)) + (function Text text -> Some text | Code _ -> None) + (fun text -> Text text) + ; case + (obj1 (req "code" string)) + (function Code code -> Some code | Text _ -> None) + (fun code -> Code code) ] )) )) diff --git a/src/repo/learnocaml_lesson.mli b/src/repo/learnocaml_lesson.mli index 3b5c2c8d5..b79c45e68 100644 --- a/src/repo/learnocaml_lesson.mli +++ b/src/repo/learnocaml_lesson.mli @@ -7,13 +7,16 @@ * included LICENSE file for details. *) type lesson = - { lesson_title : string (* may contains HTML formatting *) ; - lesson_steps : step list } + { lesson_title : string (* may contains HTML formatting *) + ; lesson_steps : step list } + and step = - { step_title : string (* may contains HTML formatting *) ; - step_phrases : phrase list } + { step_title : string (* may contains HTML formatting *) + ; step_phrases : phrase list } + and phrase = - | Text of string (* may contains HTML formatting *) + | Text of string + (* may contains HTML formatting *) | Code of string val lesson_enc : lesson Json_encoding.encoding diff --git a/src/repo/learnocaml_process_common.ml b/src/repo/learnocaml_process_common.ml index a6acf5a65..d14cd66d2 100644 --- a/src/repo/learnocaml_process_common.ml +++ b/src/repo/learnocaml_process_common.ml @@ -1,19 +1,19 @@ open Lwt.Infix -let (/) dir f = - String.concat Filename.dir_sep [ dir ; f ] +let ( / ) dir f = String.concat Filename.dir_sep [dir; f] let to_file encoding fn value = - Lwt_io.(with_file ~mode: Output) fn @@ fun chan -> + Lwt_io.(with_file ~mode:Output) fn + @@ fun chan -> let json = Json_encoding.construct encoding value in - let json = match json with - | `A _ | `O _ as d -> d - | v -> `A [ v ] in + let json = match json with (`A _ | `O _) as d -> d | v -> `A [v] in let str = Ezjsonm.to_string ~minify:false (json :> Ezjsonm.t) in Lwt_io.write chan str let from_file encoding fn = - Lwt_io.(with_file ~mode: Input) fn @@ fun chan -> - Lwt_io.read chan >>= fun str -> + Lwt_io.(with_file ~mode:Input) fn + @@ fun chan -> + Lwt_io.read chan + >>= fun str -> let json = Ezjsonm.from_string str in Lwt.return (Json_encoding.destruct encoding json) diff --git a/src/repo/learnocaml_process_common.mli b/src/repo/learnocaml_process_common.mli index 755df1387..32bd1fe0d 100644 --- a/src/repo/learnocaml_process_common.mli +++ b/src/repo/learnocaml_process_common.mli @@ -1,4 +1,5 @@ val ( / ) : string -> string -> string val to_file : 'a Json_encoding.encoding -> Lwt_io.file_name -> 'a -> unit Lwt.t + val from_file : 'a Json_encoding.encoding -> Lwt_io.file_name -> 'a Lwt.t diff --git a/src/repo/learnocaml_process_exercise_repository.ml b/src/repo/learnocaml_process_exercise_repository.ml index df2cbb627..bca6e60a2 100644 --- a/src/repo/learnocaml_process_exercise_repository.ml +++ b/src/repo/learnocaml_process_exercise_repository.ml @@ -9,19 +9,18 @@ open Learnocaml_process_common open Learnocaml_data open Exercise - open Lwt.Infix let read_exercise exercise_dir = let open Lwt.Infix in let read_field field = let fn = Filename.concat exercise_dir field in - Lwt_unix.file_exists fn >>= fun exists -> - if not exists then - Lwt.return None + Lwt_unix.file_exists fn + >>= fun exists -> + if not exists then Lwt.return None else - Lwt_io.with_file ~mode:Lwt_io.Input fn Lwt_io.read >>= fun content -> - Lwt.return (Some content) + Lwt_io.with_file ~mode:Lwt_io.Input fn Lwt_io.read + >>= fun content -> Lwt.return (Some content) in Learnocaml_exercise.read_lwt ~read_field ~id:(Filename.basename exercise_dir) @@ -39,228 +38,264 @@ let dump_reports = ref None let dump_dot exs = match !Grader_cli.dump_dot with - None -> Lwt.return () + | None -> Lwt.return () | Some filename -> let graph = Exercise.Graph.compute_graph ~filters:[] exs in - Lwt_io.with_file ~mode:Lwt_io.Output filename - (fun oc -> Lwt_io.write oc (Format.asprintf "%a" Exercise.Graph.dump_dot graph)) + Lwt_io.with_file ~mode:Lwt_io.Output filename (fun oc -> + Lwt_io.write oc (Format.asprintf "%a" Exercise.Graph.dump_dot graph) + ) let n_processes = ref 1 let print_grader_error exercise = function | Ok () -> () - | Error (-1) -> () + | Error -1 -> () | Error n -> Format.eprintf "[ERROR] %s: the solution has errors! (%d points%s)@." Learnocaml_exercise.(access File.id exercise) n - (if !Grader_cli.display_reports then "" - else ". Run with '-v' to see the report") + ( if !Grader_cli.display_reports then "" + else ". Run with '-v' to see the report" ) -let spawn_grader - dump_outputs dump_reports - ?print_result ?dirname meta exercise output_json = +let spawn_grader dump_outputs dump_reports ?print_result ?dirname meta exercise + output_json = let rec sleep () = - if !n_processes <= 0 then - Lwt_main.yield () >>= sleep - else ( - decr n_processes; Lwt.return_unit - ) + if !n_processes <= 0 then Lwt_main.yield () >>= sleep + else ( decr n_processes; Lwt.return_unit ) in - sleep () >>= fun () -> - Lwt_io.flush_all () >>= fun () -> + sleep () + >>= fun () -> + Lwt_io.flush_all () + >>= fun () -> match Lwt_unix.fork () with | 0 -> Grader_cli.dump_outputs := dump_outputs; Grader_cli.dump_reports := dump_reports; Grader_cli.display_callback := false; Lwt_main.run - (Lwt.catch (fun () -> + (Lwt.catch + (fun () -> Grader_cli.grade ?print_result ?dirname meta exercise output_json >|= fun r -> print_grader_error exercise r; - match r with - | Ok () -> exit 0 - | Error _ -> exit 1) - (fun e -> - Printf.eprintf "%!Grader error: %s\n%!" (Printexc.to_string e); - exit 10)) - | pid -> - Lwt_unix.waitpid [] pid >>= fun (_pid, ret) -> + match r with Ok () -> exit 0 | Error _ -> exit 1 ) + (fun e -> + Printf.eprintf "%!Grader error: %s\n%!" (Printexc.to_string e); + exit 10 )) + | pid -> ( + Lwt_unix.waitpid [] pid + >>= fun (_pid, ret) -> incr n_processes; match ret with | Unix.WEXITED 0 -> Lwt.return (Ok ()) - | _ -> Lwt.return (Error (-1)) + | _ -> Lwt.return (Error (-1)) ) let main dest_dir = let exercises_index = match !exercises_index with | Some exercises_index -> exercises_index - | None -> !exercises_dir / "index.json" in + | None -> !exercises_dir / "index.json" + in let exercises_dest_dir = dest_dir / Learnocaml_index.exercises_dir in - Lwt_utils.mkdir_p exercises_dest_dir >>= fun () -> + Lwt_utils.mkdir_p exercises_dest_dir + >>= fun () -> Lwt.catch (fun () -> - (if Sys.file_exists exercises_index then - from_file Exercise.Index.enc exercises_index - else if Sys.file_exists !exercises_dir then - let rec auto_index path = - let entries = Sys.readdir path in - Array.sort compare entries; - Array.fold_left (fun acc id -> - let f = path / id in - let full_id = - String.sub f (String.length !exercises_dir + 1) - (String.length f - String.length !exercises_dir - 1) - in - if Sys.file_exists (f / "meta.json") then - match acc with - | None -> Some (Index.Exercises [full_id, None]) - | Some (Index.Exercises e) -> - Some (Index.Exercises (e @ [full_id, None])) - | _ -> None - else if Sys.is_directory f then - match acc, auto_index f with - | None, None -> None - | None, Some contents -> - Some (Index.Groups - ([full_id, Index.{title = id; contents}])) - | Some (Index.Groups g), Some contents -> - Some (Index.Groups - (g @ [full_id, Index.{title = id; contents}])) - | Some _, None -> acc - | _ -> None - else acc) - None - entries - in - match auto_index !exercises_dir with - | None -> Lwt.fail_with "Missing index file and malformed repository" - | Some i -> - Format.eprintf "Missing index file, using all exercise directories.@." ; - Lwt.return i - else - (Format.eprintf "No index file, no exercise directory.@." ; - Format.eprintf "This does not look like a LearnOCaml exercise repository.@." ; - Lwt.fail (Failure "cannot continue"))) - >>= fun structure -> - - (* Exercises must be unique, since their id refer to the directory. *) - let rec fill_structure all_exercises = function - | Index.Groups groups -> - (* Ensures groups of a same parent are unique *) - Lwt_list.fold_left_s - (fun (all_exercises, subgroups, acc) (id, gr) -> - if SMap.mem id subgroups then - Lwt.return (all_exercises, subgroups, acc) - else - fill_structure all_exercises gr.Index.contents - >|= fun (all_exercises, contents) -> - all_exercises, - SMap.add id gr.Index.title subgroups, - ((id, Index.{ title = gr.title; contents }) :: acc)) - (all_exercises, SMap.empty, []) (List.rev groups) - >|= fun (all_exercises, _subgroups, groups) -> - all_exercises, Index.Groups groups - | Index.Exercises ids -> - let filtered id = - !exercises_filtered <> SSet.empty - && not (SSet.mem id !exercises_filtered) in - Lwt_list.fold_left_s - (fun (all_exercises, acc) (id, _) -> - if SMap.mem id all_exercises || filtered id then - Lwt.return (all_exercises, acc) - else - from_file Meta.enc - (!exercises_dir / id / "meta.json") - >>= fun meta -> - read_exercise (!exercises_dir / id) - >|= fun exercise -> - SMap.add id exercise all_exercises, - (id, Some meta) :: acc) - (all_exercises, []) (List.rev ids) - >>= fun (all_exercises, exercises) -> - Lwt.return (all_exercises, Index.Exercises exercises) - in - fill_structure SMap.empty structure >>= fun (all_exercises, index) -> - to_file Index.enc (dest_dir / Learnocaml_index.exercise_index_path) index >>= fun () -> - dump_dot index >>= fun () -> - Learnocaml_store.Exercise.Index.get_from_index index >>= fun index -> - to_file Json_encoding.(tup2 Learnocaml_store.Exercise.Index.enc (assoc float)) (dest_dir / "exercise-index.json") (index, []) - >>= fun () -> - SSet.iter (fun id -> - if not (SMap.mem id all_exercises) then - Format.printf "[Warning] Filtered exercise '%s' not found.@." id) - !exercises_filtered; - - let processes_arguments = - List.rev @@ SMap.fold - (fun id exercise acc -> - let exercise_dir = !exercises_dir / id in - let json_path = dest_dir / Learnocaml_index.exercise_path id in - let changed = try - let { Unix.st_mtime = json_time ; _ } = Unix.stat json_path in - Sys.readdir exercise_dir |> - Array.to_list |> - List.map (fun f -> (Unix.stat (exercise_dir / f)).Unix.st_mtime ) |> - List.exists (fun t -> t >= json_time) - with _ -> true in - let dump_outputs = - match !dump_outputs with - | None -> None - | Some dir -> Some (dir / id) in - let dump_reports = - match !dump_reports with - | None -> None - | Some dir -> Some (dir / id) in - (id, exercise_dir, exercise, json_path, - changed, dump_outputs, dump_reports) :: acc) - all_exercises [] in - begin - let listmap, grade = - if !n_processes = 1 then - Lwt_list.map_s, - fun dump_outputs dump_reports ?print_result ?dirname - meta exercise json_path -> + ( if Sys.file_exists exercises_index then + from_file Exercise.Index.enc exercises_index + else if Sys.file_exists !exercises_dir then ( + let rec auto_index path = + let entries = Sys.readdir path in + Array.sort compare entries; + Array.fold_left + (fun acc id -> + let f = path / id in + let full_id = + String.sub f + (String.length !exercises_dir + 1) + (String.length f - String.length !exercises_dir - 1) + in + if Sys.file_exists (f / "meta.json") then + match acc with + | None -> Some (Index.Exercises [(full_id, None)]) + | Some (Index.Exercises e) -> + Some (Index.Exercises (e @ [(full_id, None)])) + | _ -> None + else if Sys.is_directory f then + match (acc, auto_index f) with + | None, None -> None + | None, Some contents -> + Some + (Index.Groups [(full_id, Index.{title = id; contents})]) + | Some (Index.Groups g), Some contents -> + Some + (Index.Groups + (g @ [(full_id, Index.{title = id; contents})])) + | Some _, None -> acc + | _ -> None + else acc ) + None entries + in + match auto_index !exercises_dir with + | None -> Lwt.fail_with "Missing index file and malformed repository" + | Some i -> + Format.eprintf + "Missing index file, using all exercise directories.@."; + Lwt.return i ) + else ( + Format.eprintf "No index file, no exercise directory.@."; + Format.eprintf + "This does not look like a LearnOCaml exercise repository.@."; + Lwt.fail (Failure "cannot continue") ) ) + >>= fun structure -> + (* Exercises must be unique, since their id refer to the directory. *) + let rec fill_structure all_exercises = function + | Index.Groups groups -> + (* Ensures groups of a same parent are unique *) + Lwt_list.fold_left_s + (fun (all_exercises, subgroups, acc) (id, gr) -> + if SMap.mem id subgroups then + Lwt.return (all_exercises, subgroups, acc) + else + fill_structure all_exercises gr.Index.contents + >|= fun (all_exercises, contents) -> + ( all_exercises + , SMap.add id gr.Index.title subgroups + , (id, Index.{title = gr.title; contents}) :: acc ) ) + (all_exercises, SMap.empty, []) + (List.rev groups) + >|= fun (all_exercises, _subgroups, groups) -> + (all_exercises, Index.Groups groups) + | Index.Exercises ids -> + let filtered id = + !exercises_filtered <> SSet.empty + && not (SSet.mem id !exercises_filtered) + in + Lwt_list.fold_left_s + (fun (all_exercises, acc) (id, _) -> + if SMap.mem id all_exercises || filtered id then + Lwt.return (all_exercises, acc) + else + from_file Meta.enc (!exercises_dir / id / "meta.json") + >>= fun meta -> + read_exercise (!exercises_dir / id) + >|= fun exercise -> + (SMap.add id exercise all_exercises, (id, Some meta) :: acc) + ) + (all_exercises, []) (List.rev ids) + >>= fun (all_exercises, exercises) -> + Lwt.return (all_exercises, Index.Exercises exercises) + in + fill_structure SMap.empty structure + >>= fun (all_exercises, index) -> + to_file Index.enc (dest_dir / Learnocaml_index.exercise_index_path) index + >>= fun () -> + dump_dot index + >>= fun () -> + Learnocaml_store.Exercise.Index.get_from_index index + >>= fun index -> + to_file + Json_encoding.(tup2 Learnocaml_store.Exercise.Index.enc (assoc float)) + (dest_dir / "exercise-index.json") + (index, []) + >>= fun () -> + SSet.iter + (fun id -> + if not (SMap.mem id all_exercises) then + Format.printf "[Warning] Filtered exercise '%s' not found.@." id ) + !exercises_filtered; + let processes_arguments = + List.rev + @@ SMap.fold + (fun id exercise acc -> + let exercise_dir = !exercises_dir / id in + let json_path = dest_dir / Learnocaml_index.exercise_path id in + let changed = + try + let {Unix.st_mtime = json_time; _} = Unix.stat json_path in + Sys.readdir exercise_dir |> Array.to_list + |> List.map (fun f -> + (Unix.stat (exercise_dir / f)).Unix.st_mtime ) + |> List.exists (fun t -> t >= json_time) + with _ -> true + in + let dump_outputs = + match !dump_outputs with + | None -> None + | Some dir -> Some (dir / id) + in + let dump_reports = + match !dump_reports with + | None -> None + | Some dir -> Some (dir / id) + in + ( id + , exercise_dir + , exercise + , json_path + , changed + , dump_outputs + , dump_reports ) + :: acc ) + all_exercises [] + in + (let listmap, grade = + if !n_processes = 1 then ( + ( Lwt_list.map_s + , fun dump_outputs dump_reports ?print_result ?dirname meta exercise + json_path -> Grader_cli.dump_outputs := dump_outputs; Grader_cli.dump_reports := dump_reports; Grader_cli.grade ?print_result ?dirname meta exercise json_path - >|= fun r -> print_grader_error exercise r; r + >|= fun r -> + print_grader_error exercise r; + r ) ) + else (Lwt_list.map_p, spawn_grader) + in + listmap + (fun ( id + , ex_dir + , exercise + , json_path + , changed + , dump_outputs + , dump_reports ) -> + let dst_ex_dir = + String.concat Filename.dir_sep [dest_dir; "static"; id] + in + Lwt_utils.mkdir_p dst_ex_dir + >>= fun () -> + Lwt_stream.iter_p + (fun base -> + let d = Filename.concat ex_dir base in + let dst = String.concat Filename.dir_sep [dst_ex_dir; base] in + if Sys.is_directory d && base.[0] <> '.' then + Lwt_utils.copy_tree d dst + else Lwt.return_unit ) + (Lwt_unix.files_of_directory ex_dir) + >>= fun () -> + if not changed then ( + Format.printf "%-24s (no changes)@." id; + Lwt.return true ) else - Lwt_list.map_p, - spawn_grader - in - listmap (fun (id, ex_dir, exercise, json_path, changed, dump_outputs,dump_reports) -> - let dst_ex_dir = String.concat Filename.dir_sep [dest_dir; "static"; id] in - Lwt_utils.mkdir_p dst_ex_dir >>= fun () -> - Lwt_stream.iter_p (fun base -> - let d = Filename.concat ex_dir base in - let dst = String.concat Filename.dir_sep [dst_ex_dir; base] in - if Sys.is_directory d && base.[0] <> '.' then - Lwt_utils.copy_tree d dst - else Lwt.return_unit) - (Lwt_unix.files_of_directory ex_dir) >>= fun () -> - if not changed then begin - Format.printf "%-24s (no changes)@." id ; + grade dump_outputs dump_reports ~dirname:(!exercises_dir / id) + (Index.find index id) exercise (Some json_path) + >>= function + | Ok () -> + Format.printf "%-24s [OK]@." id; Lwt.return true - end else begin - grade dump_outputs dump_reports - ~dirname:(!exercises_dir / id) (Index.find index id) exercise (Some json_path) - >>= function - | Ok () -> - Format.printf "%-24s [OK]@." id ; - Lwt.return true - | Error _ -> - Format.printf "%-24s [FAILED]@." id ; - Lwt.return false - end) - processes_arguments - end >>= fun results -> - Lwt.return (List.for_all ((=) true) results)) + | Error _ -> + Format.printf "%-24s [FAILED]@." id; + Lwt.return false ) + processes_arguments) + >>= fun results -> Lwt.return (List.for_all (( = ) true) results) ) (fun exn -> - let print_unknown ppf = function - | Failure msg -> Format.fprintf ppf "Cannot process exercises: %s" msg - | exn -> Format.fprintf ppf "Cannot process exercises: %s" (Printexc.to_string exn) in - Json_encoding.print_error ~print_unknown Format.err_formatter exn ; - Format.eprintf "@." ; - Lwt.return false) + let print_unknown ppf = function + | Failure msg -> Format.fprintf ppf "Cannot process exercises: %s" msg + | exn -> + Format.fprintf ppf "Cannot process exercises: %s" + (Printexc.to_string exn) + in + Json_encoding.print_error ~print_unknown Format.err_formatter exn; + Format.eprintf "@."; + Lwt.return false ) diff --git a/src/repo/learnocaml_process_exercise_repository.mli b/src/repo/learnocaml_process_exercise_repository.mli index 54470acf5..045dc1ee2 100644 --- a/src/repo/learnocaml_process_exercise_repository.mli +++ b/src/repo/learnocaml_process_exercise_repository.mli @@ -8,14 +8,19 @@ (** Configuration options *) -val exercises_dir: string ref -val exercises_index: string option ref -val exercises_filtered: Learnocaml_data.SSet.t ref -val dump_outputs: string option ref -val dump_reports: string option ref -val n_processes: int ref +val exercises_dir : string ref + +val exercises_index : string option ref + +val exercises_filtered : Learnocaml_data.SSet.t ref + +val dump_outputs : string option ref + +val dump_reports : string option ref + +val n_processes : int ref (** Main *) +val main : string -> bool Lwt.t (** [dest_dir] -> success *) -val main: string -> bool Lwt.t diff --git a/src/repo/learnocaml_process_playground_repository.ml b/src/repo/learnocaml_process_playground_repository.ml index d73f2d808..9bbc20e41 100644 --- a/src/repo/learnocaml_process_playground_repository.ml +++ b/src/repo/learnocaml_process_playground_repository.ml @@ -8,89 +8,88 @@ open Learnocaml_process_common open Learnocaml_data - open Lwt.Infix let playground_dir = ref "./playground" + let playground_index = ref None let errored exn = let print_unknown ppf = function | Failure msg -> Format.fprintf ppf "Cannot process exercises: %s" msg - | exn -> Format.fprintf ppf "Cannot process exercises: %s" (Printexc.to_string exn) in - Json_encoding.print_error ~print_unknown Format.err_formatter exn ; - Format.eprintf "@." ; + | exn -> + Format.fprintf ppf "Cannot process exercises: %s" + (Printexc.to_string exn) + in + Json_encoding.print_error ~print_unknown Format.err_formatter exn; + Format.eprintf "@."; Lwt.return false let auto_index path = let entries = Sys.readdir path in Array.sort compare entries; - Array.fold_left (fun acc id -> + Array.fold_left + (fun acc id -> let f = path / id in let full_id = - String.sub f (String.length !playground_dir + 1) + String.sub f + (String.length !playground_dir + 1) (String.length f - String.length !playground_dir - 1) in if Sys.file_exists (f / "template.ml") then - let elem = full_id, Playground.Meta.default full_id in - match acc with - | None -> Some [elem] - | Some xs -> - Some (elem :: xs) - else acc) - None - entries + let elem = (full_id, Playground.Meta.default full_id) in + match acc with None -> Some [elem] | Some xs -> Some (elem :: xs) + else acc ) + None entries let get_structure exercises_index = if Sys.file_exists exercises_index then from_file Playground.Index.enc exercises_index - else if Sys.file_exists !playground_dir then + else if Sys.file_exists !playground_dir then ( match auto_index !playground_dir with | None -> Lwt.fail_with "Missing index file and malformed repository" | Some i -> - Format.eprintf "Missing index file, using all exercise directories.@." ; - Lwt.return i - else - (Format.eprintf "No index file, no exercise directory.@." ; - Format.eprintf "This does not look like a LearnOCaml exercise repository.@." ; - Lwt.fail (Failure "cannot continue")) + Format.eprintf "Missing index file, using all exercise directories.@."; + Lwt.return i ) + else ( + Format.eprintf "No index file, no exercise directory.@."; + Format.eprintf + "This does not look like a LearnOCaml exercise repository.@."; + Lwt.fail (Failure "cannot continue") ) -let fill_structure = +let fill_structure = let open Playground in - Lwt_list.map_s @@ fun (id,_) -> - Lwt_io.(with_file ~mode:Input (!playground_dir / id / "template.ml") read) - >>= fun template -> - let preludeml = !playground_dir / id / "prelude.ml" in - (if Sys.file_exists preludeml - then Lwt_io.(with_file ~mode:Input preludeml read) - else Lwt.return "") - >|= fun prelude -> - {id;template;prelude} + Lwt_list.map_s + @@ fun (id, _) -> + Lwt_io.(with_file ~mode:Input (!playground_dir / id / "template.ml") read) + >>= fun template -> + let preludeml = !playground_dir / id / "prelude.ml" in + ( if Sys.file_exists preludeml then + Lwt_io.(with_file ~mode:Input preludeml read) + else Lwt.return "" ) + >|= fun prelude -> {id; template; prelude} let write_structure dest_dir = let open Playground in - Lwt_list.iter_s @@ fun x -> - to_file enc (dest_dir / Learnocaml_index.playground_path x.id) x + Lwt_list.iter_s + @@ fun x -> to_file enc (dest_dir / Learnocaml_index.playground_path x.id) x let write_index dest_dir = - to_file Playground.Index.enc (dest_dir / Learnocaml_index.playground_index_path) + to_file Playground.Index.enc + (dest_dir / Learnocaml_index.playground_index_path) let catched playground_index dest_dir = get_structure playground_index >>= fun structure -> - fill_structure structure - >>= write_structure dest_dir - >>= fun () -> - write_index dest_dir structure - >|= fun () -> true + fill_structure structure >>= write_structure dest_dir + >>= fun () -> write_index dest_dir structure >|= fun () -> true let main dest_dir = let playground_index = match !playground_index with | Some playground_index -> playground_index - | None -> !playground_dir / "index.json" in + | None -> !playground_dir / "index.json" + in let playground_dest_dir = dest_dir / Learnocaml_index.playground_dir in - Lwt_utils.mkdir_p playground_dest_dir >>= fun () -> - Lwt.catch - (fun () -> catched playground_index dest_dir) - errored + Lwt_utils.mkdir_p playground_dest_dir + >>= fun () -> Lwt.catch (fun () -> catched playground_index dest_dir) errored diff --git a/src/repo/learnocaml_process_playground_repository.mli b/src/repo/learnocaml_process_playground_repository.mli index 667bcb4e7..bc0f486b4 100644 --- a/src/repo/learnocaml_process_playground_repository.mli +++ b/src/repo/learnocaml_process_playground_repository.mli @@ -8,10 +8,11 @@ (** Configuration options *) -val playground_dir: string ref -val playground_index: string option ref +val playground_dir : string ref + +val playground_index : string option ref (** Main *) +val main : string -> bool Lwt.t (** [dest_dir] -> success *) -val main: string -> bool Lwt.t diff --git a/src/repo/learnocaml_process_tutorial_repository.ml b/src/repo/learnocaml_process_tutorial_repository.ml index e6305b518..a77ee4bc3 100644 --- a/src/repo/learnocaml_process_tutorial_repository.ml +++ b/src/repo/learnocaml_process_tutorial_repository.ml @@ -9,82 +9,98 @@ open Learnocaml_process_common open Learnocaml_index open Learnocaml_data - open Lwt.Infix let tutorials_dir = ref "./tutorials" let tutorials_index = ref None -let args = Arg.align @@ - [ "-tutorials-dir", Arg.Set_string tutorials_dir, - "PATH path to the tutorial repository (default: [./tutorials])" ; - "-tutorials-index", Arg.String (fun fn -> tutorials_index := Some fn), - "PATH path to the tutorials index (default: [/index.json])" ] +let args = + Arg.align + @@ [ ( "-tutorials-dir" + , Arg.Set_string tutorials_dir + , "PATH path to the tutorial repository (default: [./tutorials])" ) + ; ( "-tutorials-index" + , Arg.String (fun fn -> tutorials_index := Some fn) + , "PATH path to the tutorials index (default: \ + [/index.json])" ) ] let main dest_dir = let tutorials_index = match !tutorials_index with | Some tutorials_index -> tutorials_index - | None -> !tutorials_dir / "index.json" in - let tutorials_dest_dir = - dest_dir / Learnocaml_index.tutorials_dir in - Lwt_utils.mkdir_p tutorials_dest_dir >>= fun () -> + | None -> !tutorials_dir / "index.json" + in + let tutorials_dest_dir = dest_dir / Learnocaml_index.tutorials_dir in + Lwt_utils.mkdir_p tutorials_dest_dir + >>= fun () -> Lwt.catch (fun () -> - (if Sys.file_exists tutorials_index then - from_file Tutorial.Index.enc tutorials_index >|= - List.map Tutorial.Index.(fun (id, s) -> - id, (s.series_title, - List.map (fun e -> e.name) s.series_tutorials)) + ( if Sys.file_exists tutorials_index then + from_file Tutorial.Index.enc tutorials_index + >|= List.map + Tutorial.Index.( + fun (id, s) -> + ( id + , ( s.series_title + , List.map (fun e -> e.name) s.series_tutorials ) )) + else + match + Array.to_list (Sys.readdir !tutorials_dir) + |> List.filter (fun file -> + (not (Sys.is_directory file)) + && ( Filename.check_suffix file ".md" + || Filename.check_suffix file ".html" ) ) + with + | [] -> + Format.eprintf "No index file, no .md or .html file.@."; + Format.eprintf + "This does not look like a LearnOCaml tutorial repository.@."; + Lwt.fail_with "cannot continue" + | files -> + Format.eprintf + "Missing index file, using all .md and .html files.@."; + Lwt.return [("tutorials", ("All tutorials", files))] ) + >>= fun series -> + let retrieve_tutorial tutorial_name = + let base_name = !tutorials_dir / tutorial_name in + let md_file = base_name ^ ".md" in + if Sys.file_exists md_file then + Learnocaml_tutorial_parser.parse_md_tutorial ~tutorial_name + ~file_name:md_file else - match - Array.to_list (Sys.readdir !tutorials_dir) |> - List.filter (fun file -> - not (Sys.is_directory file) && - (Filename.check_suffix file ".md" || - Filename.check_suffix file ".html")) - with - | [] -> - Format.eprintf "No index file, no .md or .html file.@." ; - Format.eprintf "This does not look like a LearnOCaml tutorial repository.@." ; - Lwt.fail_with "cannot continue" - | files -> - Format.eprintf "Missing index file, using all .md and .html files.@." ; - Lwt.return [ "tutorials", ("All tutorials", files) ]) >>= fun series -> - let retrieve_tutorial tutorial_name = - let base_name = !tutorials_dir / tutorial_name in - let md_file = base_name ^ ".md" in - if Sys.file_exists md_file then - Learnocaml_tutorial_parser.parse_md_tutorial - ~tutorial_name ~file_name: md_file - else - let html_file = base_name ^ ".html" in - if Sys.file_exists html_file then - Learnocaml_tutorial_parser.parse_html_tutorial - ~tutorial_name ~file_name: html_file - else - Lwt.fail_with (Format.asprintf "missing file %s.{html|md}" base_name ) in - List.fold_left - (fun acc (name, (series_title, tutorials)) -> - Lwt_list.map_p - (fun name -> - retrieve_tutorial name >>= fun (server_index_handle, tutorial) -> - let json_path = dest_dir / tutorial_path name in - to_file Tutorial.enc json_path tutorial >>= fun () -> - Lwt.return server_index_handle) - tutorials >>= fun series_tutorials -> - acc >>= fun acc -> - Lwt.return ((name, Tutorial.Index.{series_title; series_tutorials}) - :: acc)) - (Lwt.return []) - series >>= fun index -> - to_file Tutorial.Index.enc (dest_dir / tutorial_index_path) index >>= fun () -> - Lwt.return true) + let html_file = base_name ^ ".html" in + if Sys.file_exists html_file then + Learnocaml_tutorial_parser.parse_html_tutorial ~tutorial_name + ~file_name:html_file + else + Lwt.fail_with + (Format.asprintf "missing file %s.{html|md}" base_name) + in + List.fold_left + (fun acc (name, (series_title, tutorials)) -> + Lwt_list.map_p + (fun name -> + retrieve_tutorial name + >>= fun (server_index_handle, tutorial) -> + let json_path = dest_dir / tutorial_path name in + to_file Tutorial.enc json_path tutorial + >>= fun () -> Lwt.return server_index_handle ) + tutorials + >>= fun series_tutorials -> + acc + >>= fun acc -> + Lwt.return + ((name, Tutorial.Index.{series_title; series_tutorials}) :: acc) ) + (Lwt.return []) series + >>= fun index -> + to_file Tutorial.Index.enc (dest_dir / tutorial_index_path) index + >>= fun () -> Lwt.return true ) (fun exn -> - let print_unknown ppf = function - | Failure msg -> Format.fprintf ppf "Fatal: %s" msg - | exn -> Format.fprintf ppf "Fatal: %s" (Printexc.to_string exn) in - Json_encoding.print_error ~print_unknown Format.err_formatter exn ; - Format.eprintf "@." ; - Lwt.return false) + let print_unknown ppf = function + | Failure msg -> Format.fprintf ppf "Fatal: %s" msg + | exn -> Format.fprintf ppf "Fatal: %s" (Printexc.to_string exn) + in + Json_encoding.print_error ~print_unknown Format.err_formatter exn; + Format.eprintf "@."; + Lwt.return false ) diff --git a/src/repo/learnocaml_process_tutorial_repository.mli b/src/repo/learnocaml_process_tutorial_repository.mli index 102ea11ab..0cbd3be0e 100644 --- a/src/repo/learnocaml_process_tutorial_repository.mli +++ b/src/repo/learnocaml_process_tutorial_repository.mli @@ -8,12 +8,13 @@ (** Configuration options *) -val tutorials_dir: string ref -val tutorials_index: string option ref +val tutorials_dir : string ref -val args: (Arg.key * Arg.spec * Arg.doc) list +val tutorials_index : string option ref + +val args : (Arg.key * Arg.spec * Arg.doc) list (** Main *) +val main : string -> bool Lwt.t (** [dest_dir] -> success *) -val main: string -> bool Lwt.t diff --git a/src/repo/learnocaml_tutorial.ml b/src/repo/learnocaml_tutorial.ml index 1b428b08c..346795d58 100644 --- a/src/repo/learnocaml_tutorial.ml +++ b/src/repo/learnocaml_tutorial.ml @@ -7,11 +7,10 @@ * included LICENSE file for details. *) type tutorial = - { tutorial_title : Learnocaml_index.text ; - tutorial_steps : step list } -and step = - { step_title : Learnocaml_index.text ; - step_contents : phrase list } + {tutorial_title : Learnocaml_index.text; tutorial_steps : step list} + +and step = {step_title : Learnocaml_index.text; step_contents : phrase list} + and phrase = | Paragraph of Learnocaml_index.text | Enum of phrase list list diff --git a/src/repo/learnocaml_tutorial.mli b/src/repo/learnocaml_tutorial.mli index 731009b59..4933871ea 100644 --- a/src/repo/learnocaml_tutorial.mli +++ b/src/repo/learnocaml_tutorial.mli @@ -7,11 +7,10 @@ * included LICENSE file for details. *) type tutorial = - { tutorial_title : Learnocaml_index.text ; - tutorial_steps : step list } -and step = - { step_title : Learnocaml_index.text ; - step_contents : phrase list } + {tutorial_title : Learnocaml_index.text; tutorial_steps : step list} + +and step = {step_title : Learnocaml_index.text; step_contents : phrase list} + and phrase = | Paragraph of Learnocaml_index.text | Enum of phrase list list diff --git a/src/repo/learnocaml_tutorial_parser.ml b/src/repo/learnocaml_tutorial_parser.ml index 94b9e536c..d8de36dc5 100644 --- a/src/repo/learnocaml_tutorial_parser.ml +++ b/src/repo/learnocaml_tutorial_parser.ml @@ -14,9 +14,11 @@ let lines_margin lines = let rec line_margin start prev line = if start >= String.length line then start else if start >= prev then prev - else match String.get line start with + else + match line.[start] with | ' ' | '\t' -> line_margin (start + 1) prev line - | _ -> start in + | _ -> start + in match lines with | [] -> 0 | _ -> List.fold_left (line_margin 0) max_int lines @@ -24,9 +26,8 @@ let lines_margin lines = let trailing_whitespace margin line = let rec loop i = if i < margin then i - else match String.get line i with - | ' ' | '\t' -> loop (i - 1) - | _ -> i in + else match line.[i] with ' ' | '\t' -> loop (i - 1) | _ -> i + in let last = max (margin - 1) (String.length line - 1) in last - loop last @@ -37,24 +38,25 @@ let drop_padding_lines lines = let len = String.length line in let rec empty i = if i >= len then true - else match String.get line i with - | ' ' | '\t' -> empty (i + 1) - | _ -> false in - if empty 0 then drop_start rest else line :: rest in + else match line.[i] with ' ' | '\t' -> empty (i + 1) | _ -> false + in + if empty 0 then drop_start rest else line :: rest + in lines |> drop_start |> List.rev |> drop_start |> List.rev let cut_margin margin lines = let rec cut acc = function | [] -> List.rev acc | line :: lines -> - let len = String.length line - - margin - - trailing_whitespace margin line in + let len = + String.length line - margin - trailing_whitespace margin line + in cut (String.sub line margin len :: acc) lines - in cut [] lines + in + cut [] lines let reshape_code_block code = - let lines = Stringext.split code ~on: '\n' in + let lines = Stringext.split code ~on:'\n' in let lines = drop_padding_lines lines in let margin = lines_margin lines in let lines = cut_margin margin lines in @@ -64,207 +66,236 @@ let reshape_code_block code = let parse_md_code_notation code = let len = String.length code in if String.contains code '\n' then - let lines = Stringext.split code ~on: '\n' in + let lines = Stringext.split code ~on:'\n' in let lines = drop_padding_lines lines in let margin = lines_margin lines in let lines = cut_margin margin lines in let left_line lines = List.fold_left (fun acc line -> - if String.length line = 0 then - `None - else - match acc, String.get line 0 with - | `Init, c -> `Some c - | `Some c', c when c = c' -> `Some c - | _ -> `None) `Init lines |> function - | `Init | `None -> None - | `Some char -> Some char in + if String.length line = 0 then `None + else + match (acc, line.[0]) with + | `Init, c -> `Some c + | `Some c', c when c = c' -> `Some c + | _ -> `None ) + `Init lines + |> function `Init | `None -> None | `Some char -> Some char + in match left_line lines with - | Some ('$' | '|' as c) -> - let cut_first_char line = - String.sub line 1 (String.length line - 1) in + | Some (('$' | '|') as c) -> ( + let cut_first_char line = String.sub line 1 (String.length line - 1) in let lines = List.map cut_first_char lines in let lines = drop_padding_lines lines in let margin = lines_margin lines in let lines = cut_margin margin lines in let code = String.concat "\n" lines in - begin match c with - | '|' -> Code { code ; runnable = true } - | '$' -> Math code - | _ -> assert false end + match c with + | '|' -> Code {code; runnable = true} + | '$' -> Math code + | _ -> assert false ) | None | Some _ -> let code = String.concat "\n" lines in - Code { code ; runnable = false } - else if len > 2 - && (String.get code 0 = String.get code (len - 1)) - && (String.get code 0 <> String.get code 1) then - match String.get code 0 with + Code {code; runnable = false} + else if len > 2 && code.[0] = code.[len - 1] && code.[0] <> code.[1] then + match code.[0] with | '|' -> let code = String.trim (String.sub code 1 (len - 2)) in - Code { code ; runnable = true } + Code {code; runnable = true} | '$' -> let code = String.trim (String.sub code 1 (len - 2)) in Math code - | _ -> Code { code ; runnable = false } - else - Code { code ; runnable = false } + | _ -> Code {code; runnable = false} + else Code {code; runnable = false} let parse_html_tutorial ~tutorial_name ~file_name = let fail fmt = Format.kasprintf (fun res -> Lwt.fail_with (Format.sprintf "in file %s, %s" file_name res)) - fmt in - Lwt_io.(with_file ~mode: Input) file_name @@ fun chan -> - Lwt_io.read chan >>= fun contents -> + fmt + in + Lwt_io.(with_file ~mode:Input) file_name + @@ fun chan -> + Lwt_io.read chan + >>= fun contents -> let tree = let open Markup in - string contents |> parse_html |> signals |> tree - ~text: (fun text -> `Text (String.concat "" text)) - ~element: (fun (_, name) attribs children -> - let attribs = List.map (fun ((_, n), v) -> (n, v)) attribs in - `Elt (name, attribs, children)) in + string contents |> parse_html |> signals + |> tree + ~text:(fun text -> `Text (String.concat "" text)) + ~element:(fun (_, name) attribs children -> + let attribs = List.map (fun ((_, n), v) -> (n, v)) attribs in + `Elt (name, attribs, children) ) + in let rec strip = function | `Elt ("pre", _, _) as elt -> elt | `Elt (name, attribs, children) -> let rec skip_white_space acc = function | [] -> List.rev acc | `Text "" :: rest -> skip_white_space acc rest - | oth :: rest -> skip_white_space (oth :: acc) rest in + | oth :: rest -> skip_white_space (oth :: acc) rest + in `Elt (name, attribs, skip_white_space [] @@ List.map strip children) - | `Text text -> `Text (String.trim text) in + | `Text text -> `Text (String.trim text) + in let rec parse_code acc = function | [] -> Lwt.return (String.concat "" (List.rev acc)) - | `Text text :: rest -> - parse_code (text :: acc) rest - | `Elt (tag, _, _) :: _ -> - fail "unsupported markup <%s> in code" tag in + | `Text text :: rest -> parse_code (text :: acc) rest + | `Elt (tag, _, _) :: _ -> fail "unsupported markup <%s> in code" tag + in let rec parse_text acc = function | [] -> Lwt.return (List.rev acc) - | `Text t1 :: `Text t2 :: rest -> - parse_text acc (`Text (t1 ^ t2) :: rest) - | `Elt ("br", _, _) :: rest -> - parse_text acc rest + | `Text t1 :: `Text t2 :: rest -> parse_text acc (`Text (t1 ^ t2) :: rest) + | `Elt ("br", _, _) :: rest -> parse_text acc rest | `Text text :: rest -> - let text = String.trim (Str.(global_replace (regexp "[ \t\n]+")) " " text) in + let text = + String.trim (Str.(global_replace (regexp "[ \t\n]+")) " " text) + in parse_text (Text text :: acc) rest | `Elt (("code" | "quote"), [], children) :: rest -> - parse_code [] children >>= fun code -> - let code = String.trim (Str.(global_replace (regexp "\\( *\n[ \t]*\\)+")) " " code) in - parse_text (Code { code ; runnable = false } :: acc) rest - | `Elt (("code" | "quote"), [ "data-math", _ ], children) :: rest -> - parse_code [] children >>= fun code -> - let code = String.trim (Str.(global_replace (regexp "\\( *\n[ \t]*\\)+")) " " code) in + parse_code [] children + >>= fun code -> + let code = + String.trim + (Str.(global_replace (regexp "\\( *\n[ \t]*\\)+")) " " code) + in + parse_text (Code {code; runnable = false} :: acc) rest + | `Elt (("code" | "quote"), [("data-math", _)], children) :: rest -> + parse_code [] children + >>= fun code -> + let code = + String.trim + (Str.(global_replace (regexp "\\( *\n[ \t]*\\)+")) " " code) + in parse_text (Math code :: acc) rest - | `Elt (("code" | "quote"), [ "data-run", _ ], children) :: rest -> - parse_code [] children >>= fun code -> - let code = String.trim (Str.(global_replace (regexp "\\( *\n[ \t]*\\)+")) " " code) in - parse_text (Code { code ; runnable = true } :: acc) rest - | `Elt (("code" | "quote"), _ , _) :: _ -> - fail "the markup expects either \ - one data-math, one data-run or zero attribute" + | `Elt (("code" | "quote"), [("data-run", _)], children) :: rest -> + parse_code [] children + >>= fun code -> + let code = + String.trim + (Str.(global_replace (regexp "\\( *\n[ \t]*\\)+")) " " code) + in + parse_text (Code {code; runnable = true} :: acc) rest + | `Elt (("code" | "quote"), _, _) :: _ -> + fail + "the markup expects either one data-math, one data-run or \ + zero attribute" | `Elt (("strong" | "em" | "b"), _, children) :: rest -> - parse_text [] children >>= fun contents -> - parse_text (Emph contents :: acc) rest - | `Elt (tag, _, _) :: _ -> - fail "unsupported markup <%s> in text" tag in + parse_text [] children + >>= fun contents -> parse_text (Emph contents :: acc) rest + | `Elt (tag, _, _) :: _ -> fail "unsupported markup <%s> in text" tag + in let rec parse_contents ?(require_p = true) acc = function | `Elt ("p", _, children) :: rest -> - parse_text [] children >>= fun contents -> + parse_text [] children + >>= fun contents -> parse_contents ~require_p (Paragraph contents :: acc) rest - | `Elt ("ul" | "ol" as tag, _, children) :: rest -> + | `Elt ((("ul" | "ol") as tag), _, children) :: rest -> let rec parse_items tag acc = function | [] -> Lwt.return (List.rev acc) | `Elt ("li", _, children) :: rest -> - parse_contents ~require_p: false [] children >>= fun contents -> - parse_items tag (contents :: acc) rest - | _ -> fail "unexpected non
  • element in <%s>" tag in - parse_items tag [] children >>= fun items -> - parse_contents ~require_p (Enum items :: acc) rest + parse_contents ~require_p:false [] children + >>= fun contents -> parse_items tag (contents :: acc) rest + | _ -> fail "unexpected non
  • element in <%s>" tag + in + parse_items tag [] children + >>= fun items -> parse_contents ~require_p (Enum items :: acc) rest | `Elt ("pre", [], children) :: rest -> - parse_code [] children >>= fun code -> + parse_code [] children + >>= fun code -> let code = reshape_code_block code in - let code_block = Code_block { code ; runnable = false } in + let code_block = Code_block {code; runnable = false} in parse_contents ~require_p (code_block :: acc) rest - | `Elt ("pre", [ "data-run", _ ], children) :: rest -> - parse_code [] children >>= fun code -> + | `Elt ("pre", [("data-run", _)], children) :: rest -> + parse_code [] children + >>= fun code -> let code = reshape_code_block code in - let code_block = Code_block { code ; runnable = true } in + let code_block = Code_block {code; runnable = true} in parse_contents ~require_p (code_block :: acc) rest - | `Elt ("pre", [ "data-math", _ ], children) :: rest -> - parse_code [] children >>= fun code -> + | `Elt ("pre", [("data-math", _)], children) :: rest -> + parse_code [] children + >>= fun code -> let code = reshape_code_block code in - let contents = [ Math code ] in + let contents = [Math code] in parse_contents ~require_p (Paragraph contents :: acc) rest - | `Elt ("pre", _ , _) :: _ -> - fail "the
     markup expects either \
    -              one data-math, one data-run or zero attribute"
    +    | `Elt ("pre", _, _) :: _ ->
    +        fail
    +          "the 
     markup expects either one data-math, one data-run or \
    +           zero attribute"
         | `Elt (tag, _, _) :: _ as l ->
             if require_p || acc <> [] then
    -          fail "the only markups supported at toplevel are \
    -                

    ,

    ,

      and
      , <%s> is not allowed" tag
      -        else
      -          parse_text [] l >>= fun text ->
      -          Lwt.return [ Paragraph text ]
      +          fail
      +            "the only markups supported at toplevel are 

      ,

      ,

        and \ +
        , <%s> is not allowed"
        +            tag
        +        else parse_text [] l >>= fun text -> Lwt.return [Paragraph text]
             | `Text _ :: _ as l ->
                 if require_p || acc <> [] then
        -          fail "text is not allowed at the toplevel, \
        -                wrap it in a 

        markup" - else - parse_text [] l >>= fun text -> - Lwt.return [ Paragraph text ] - | [] -> Lwt.return (List.rev acc) in + fail "text is not allowed at the toplevel, wrap it in a

        markup" + else parse_text [] l >>= fun text -> Lwt.return [Paragraph text] + | [] -> Lwt.return (List.rev acc) + in let rec parse_steps = function - | acc, None, [] -> - Lwt.return (List.rev acc) + | acc, None, [] -> Lwt.return (List.rev acc) | acc, Some (step_title, sacc), [] -> - parse_contents [] (List.rev sacc) >>= fun step_contents -> - let acc = { step_title ; step_contents } :: acc in + parse_contents [] (List.rev sacc) + >>= fun step_contents -> + let acc = {step_title; step_contents} :: acc in Lwt.return (List.rev acc) | acc, None, `Elt ("h2", _, title) :: rest -> - parse_text [] title >>= fun step_title -> - parse_steps (acc, Some (step_title, []), rest) + parse_text [] title + >>= fun step_title -> parse_steps (acc, Some (step_title, []), rest) | _, None, _ :: _ -> - fail "step title (

        markup) expected \ - after the tutorial title (

        markup)" + fail + "step title (

        markup) expected after the tutorial title (

        \ + markup)" | acc, Some (step_title, sacc), (`Elt ("h2", _, _) :: _ as rest) -> - parse_contents [] (List.rev sacc) >>= fun step_contents -> - let acc = { step_title ; step_contents } :: acc in + parse_contents [] (List.rev sacc) + >>= fun step_contents -> + let acc = {step_title; step_contents} :: acc in parse_steps (acc, None, rest) | acc, Some (step_title, sacc), elt :: rest -> - parse_steps (acc, Some (step_title, elt :: sacc), rest) in + parse_steps (acc, Some (step_title, elt :: sacc), rest) + in match tree with | None -> fail "unparsable HTML file" - | Some tree -> match strip tree with - | `Elt ("html", _, [ `Elt ("head", _, _) ; `Elt ("body", _, contents) ]) - | `Elt ("html", _, [ `Elt ("body", _, contents) ]) -> - begin match contents with - | `Elt ("h1", _, title) :: rest -> - parse_text [] title >>= fun title -> - parse_steps ([], None, rest) >>= fun steps -> - Lwt.return - (Index.{ title ; name = tutorial_name }, - { title ; steps }) - | _ -> - fail "tutorial title (

        markup) expected \ - at the beginning of the " - end - | _ -> fail "wrong HTML structure, \ - expecting a standard with a " + | Some tree -> ( + match strip tree with + | `Elt ("html", _, [`Elt ("head", _, _); `Elt ("body", _, contents)]) + |`Elt ("html", _, [`Elt ("body", _, contents)]) -> ( + match contents with + | `Elt ("h1", _, title) :: rest -> + parse_text [] title + >>= fun title -> + parse_steps ([], None, rest) + >>= fun steps -> + Lwt.return (Index.{title; name = tutorial_name}, {title; steps}) + | _ -> + fail + "tutorial title (

        markup) expected at the beginning of the \ + " ) + | _ -> + fail "wrong HTML structure, expecting a standard with a " + ) let parse_md_tutorial ~tutorial_name ~file_name = let fail fmt = Format.kasprintf (fun res -> Lwt.fail_with (Format.sprintf "in file %s, %s" file_name res)) - fmt in - Lwt_io.(with_file ~mode: Input) file_name @@ fun chan -> - Lwt_io.read chan >>= fun str -> + fmt + in + Lwt_io.(with_file ~mode:Input) file_name + @@ fun chan -> + Lwt_io.read chan + >>= fun str -> let strip md = let rec strip acc = function | [] -> List.rev acc | Omd.NL :: rest -> strip acc rest - | oth :: rest -> strip (oth :: acc) rest in - strip [] md in + | oth :: rest -> strip (oth :: acc) rest + in + strip [] md + in let rec parse_text acc = function | [] -> Lwt.return (List.rev acc) | Omd.NL :: rest -> parse_text acc rest @@ -275,60 +306,63 @@ let parse_md_tutorial ~tutorial_name ~file_name = parse_text acc (Omd.Text (t1 ^ " " ^ t2) :: rest) | Omd.Text t1 :: Omd.Text t2 :: rest -> parse_text acc (Omd.Text (t1 ^ t2) :: rest) - | Omd.Text text :: rest -> - parse_text (Text text :: acc) rest + | Omd.Text text :: rest -> parse_text (Text text :: acc) rest | Omd.Emph t :: rest | Omd.Bold t :: rest -> - parse_text [] t >>= fun text -> - parse_text (Emph text :: acc) rest - | elt :: _ -> - fail "unexpected content in text (%s)" - (Omd.to_markdown [ elt ]) in + parse_text [] t >>= fun text -> parse_text (Emph text :: acc) rest + | elt :: _ -> + fail "unexpected content in text (%s)" (Omd.to_markdown [elt]) + in let rec parse_contents acc = function | (Omd.Ul l | Omd.Ol l | Omd.Ulp l | Omd.Olp l) :: rest -> - Lwt_list.map_p (parse_contents []) l >>= fun items -> - parse_contents (Enum items :: acc) rest + Lwt_list.map_p (parse_contents []) l + >>= fun items -> parse_contents (Enum items :: acc) rest | Omd.Paragraph children :: rest -> - parse_text [] children >>= fun contents -> - parse_contents (Paragraph contents :: acc) rest + parse_text [] children + >>= fun contents -> parse_contents (Paragraph contents :: acc) rest | Omd.Code_block (_, code) :: rest -> - let blocks = List.map (fun code -> - match parse_md_code_notation (code ^ "\n") with - | Code code -> Code_block code - | contents -> Paragraph [ contents ]) - (Re.split (Re.compile (Re.str "\n\n")) code) in + let blocks = + List.map + (fun code -> + match parse_md_code_notation (code ^ "\n") with + | Code code -> Code_block code + | contents -> Paragraph [contents] ) + (Re.split (Re.compile (Re.str "\n\n")) code) + in parse_contents (List.rev blocks @ acc) rest | elt :: _ -> - fail "unexpected content at toplevel (%s)" - (Omd.to_markdown [ elt ]) - | [] -> Lwt.return (List.rev acc) in + fail "unexpected content at toplevel (%s)" (Omd.to_markdown [elt]) + | [] -> Lwt.return (List.rev acc) + in let rec parse_steps = function - | acc, None, [] -> - Lwt.return (List.rev acc) + | acc, None, [] -> Lwt.return (List.rev acc) | acc, Some (step_title, sacc), [] -> - parse_contents [] (List.rev sacc) >>= fun step_contents -> - let acc = { step_title ; step_contents } :: acc in + parse_contents [] (List.rev sacc) + >>= fun step_contents -> + let acc = {step_title; step_contents} :: acc in Lwt.return (List.rev acc) | acc, None, Omd.H2 title :: rest -> - parse_text [] title >>= fun step_title -> - parse_steps (acc, Some (step_title, []), rest) + parse_text [] title + >>= fun step_title -> parse_steps (acc, Some (step_title, []), rest) | _, None, _ :: _ -> - fail "step title (

        markup) expected \ - after the tutorial title (

        markup)" + fail + "step title (

        markup) expected after the tutorial title (

        \ + markup)" | acc, Some (step_title, sacc), (Omd.H2 _ :: _ as rest) -> - parse_contents [] (List.rev sacc) >>= fun step_contents -> - let acc = { step_title ; step_contents } :: acc in + parse_contents [] (List.rev sacc) + >>= fun step_contents -> + let acc = {step_title; step_contents} :: acc in parse_steps (acc, None, rest) | acc, Some (step_title, sacc), elt :: rest -> - parse_steps (acc, Some (step_title, elt :: sacc), rest) in + parse_steps (acc, Some (step_title, elt :: sacc), rest) + in match Omd.of_string str |> strip with | Omd.H1 title :: rest -> - parse_text [] title >>= fun title -> - parse_steps ([], None, rest) >>= fun steps -> - Lwt.return - (Index.{ title ; name = tutorial_name }, - { title ; steps }) - | _ -> - fail "expecting a level 1 title at file beginning" + parse_text [] title + >>= fun title -> + parse_steps ([], None, rest) + >>= fun steps -> + Lwt.return (Index.{title; name = tutorial_name}, {title; steps}) + | _ -> fail "expecting a level 1 title at file beginning" let print_html_tutorial ~tutorial_name tutorial = let buffer = Buffer.create 10000 in @@ -336,86 +370,91 @@ let print_html_tutorial ~tutorial_name tutorial = let utf8_of_cp = let tmp = Buffer.create 513 in fun cp -> - Buffer.clear tmp ; - Uutf.Buffer.add_utf_8 tmp cp ; - Buffer.contents tmp in + Buffer.clear tmp; + Uutf.Buffer.add_utf_8 tmp cp; + Buffer.contents tmp + in let pp_escaped ppf t = - Uutf.String.fold_utf_8 (fun () _ cp -> + Uutf.String.fold_utf_8 + (fun () _ cp -> match cp with - | `Uchar c -> - begin match Uchar.to_int c with - | 0x20 -> Format.fprintf ppf "@ " - | 0x26 -> Format.fprintf ppf "&" - | 0x3C -> Format.fprintf ppf "<" - | 0x3E -> Format.fprintf ppf ">" - | 0xA0 -> Format.fprintf ppf " " - | _ -> Format.fprintf ppf "%s" (utf8_of_cp c) - end - | `Malformed _ -> ())() t in + | `Uchar c -> ( + match Uchar.to_int c with + | 0x20 -> Format.fprintf ppf "@ " + | 0x26 -> Format.fprintf ppf "&" + | 0x3C -> Format.fprintf ppf "<" + | 0x3E -> Format.fprintf ppf ">" + | 0xA0 -> Format.fprintf ppf " " + | _ -> Format.fprintf ppf "%s" (utf8_of_cp c) ) + | `Malformed _ -> () ) + () t + in let rec pp_text ppf = function | [] -> () - | Code { code ; runnable = false} :: rest -> - Format.fprintf ppf "%a" pp_escaped code ; - if rest <> [] then Format.fprintf ppf "@ " ; + | Code {code; runnable = false} :: rest -> + Format.fprintf ppf "%a" pp_escaped code; + if rest <> [] then Format.fprintf ppf "@ "; pp_text ppf rest - | Code { code ; runnable = true} :: rest -> - Format.fprintf ppf "%a" pp_escaped code ; - if rest <> [] then Format.fprintf ppf "@ " ; + | Code {code; runnable = true} :: rest -> + Format.fprintf ppf "%a" pp_escaped code; + if rest <> [] then Format.fprintf ppf "@ "; pp_text ppf rest - | Math code :: rest -> - Format.fprintf ppf "" pp_escaped code ; - if rest <> [] then Format.fprintf ppf "@ " ; + | Math code :: rest -> + Format.fprintf ppf "" + pp_escaped code; + if rest <> [] then Format.fprintf ppf "@ "; pp_text ppf rest - | Emph text :: rest -> - Format.fprintf ppf "%a" pp_text text ; - if rest <> [] then Format.fprintf ppf "@ " ; + | Emph text :: rest -> + Format.fprintf ppf "%a" pp_text text; + if rest <> [] then Format.fprintf ppf "@ "; pp_text ppf rest | Text t :: rest -> - pp_escaped ppf t ; - if rest <> [] then Format.fprintf ppf "@ " ; - pp_text ppf rest - | _ -> assert false in + pp_escaped ppf t; + if rest <> [] then Format.fprintf ppf "@ "; + pp_text ppf rest + | _ -> assert false + in let rec pp_content ppf = function - | Code_block { code ; runnable } -> - Format.fprintf ppf "@[@," (if runnable then " data-run" else "") ; - let code = reshape_code_block code in - Uutf.String.fold_utf_8 (fun () _ cp -> - match cp with - | `Uchar c -> - begin match Uchar.to_int c with + | Code_block {code; runnable} -> + Format.fprintf ppf "@[@," + (if runnable then " data-run" else ""); + let code = reshape_code_block code in + Uutf.String.fold_utf_8 + (fun () _ cp -> + match cp with + | `Uchar c -> ( + match Uchar.to_int c with | 0x26 -> Format.fprintf ppf "&" | 0x3C -> Format.fprintf ppf "<" | 0x3E -> Format.fprintf ppf ">" | 0xA0 -> Format.fprintf ppf " " - | _ -> Format.fprintf ppf "%s" (utf8_of_cp c) - end - | `Malformed _ -> ()) () code ; - Format.fprintf ppf "@]@,

        " - | Paragraph text -> - Format.fprintf ppf "@[

        %a@]

        " pp_text text + | _ -> Format.fprintf ppf "%s" (utf8_of_cp c) ) + | `Malformed _ -> () ) + () code; + Format.fprintf ppf "@]@,

      " + | Paragraph text -> Format.fprintf ppf "@[

      %a@]

      " pp_text text | Enum items -> - let pp_item ppf contents = - Format.fprintf ppf "@[
    • %a@]
    • " - (Format.pp_print_list pp_content) contents in - Format.fprintf ppf "@[
        %a@]
      " - (Format.pp_print_list pp_item) items in - let pp_step ppf { step_title ; step_contents } = - Format.fprintf ppf "@[

      %a

      @]@,%a" - pp_text step_title - (Format.pp_print_list pp_content) step_contents in - Format.fprintf ppf "@[@,\ - @[@,\ - @,\ - %s@]@,\ - @,\ - @[@,\ - @[

      %a

      @]@,\ - %a@]@,\ - @]@,\ - @." - tutorial_name - pp_text tutorial.title - (Format.pp_print_list pp_step) tutorial.steps ; + let pp_item ppf contents = + Format.fprintf ppf "@[
    • %a@]
    • " + (Format.pp_print_list pp_content) + contents + in + Format.fprintf ppf "@[
        %a@]
      " + (Format.pp_print_list pp_item) + items + in + let pp_step ppf {step_title; step_contents} = + Format.fprintf ppf "@[

      %a

      @]@,%a" pp_text step_title + (Format.pp_print_list pp_content) + step_contents + in + Format.fprintf ppf + "@[@,@[@,@,%s@]@,@,@[@,@[

      %a

      @]@,%a@]@,@]@,@." + tutorial_name pp_text tutorial.title + (Format.pp_print_list pp_step) + tutorial.steps; Buffer.contents buffer let print_md_tutorial tutorial = @@ -423,62 +462,67 @@ let print_md_tutorial tutorial = let ppf = Format.formatter_of_buffer buffer in let pp_sep ppf () = Format.fprintf ppf "@,@," in let drop_newlines code = - Stringext.split ~on:'\n' code - |> List.map String.trim - |> String.concat " " in + Stringext.split ~on:'\n' code |> List.map String.trim |> String.concat " " + in let rec pp_text ppf = function | [] -> () - | Code { code ; runnable = false} :: rest -> + | Code {code; runnable = false} :: rest -> let code = drop_newlines code in - let code = Omd_backend.markdown_of_md [ Omd.Code ("", code) ] in - Format.fprintf ppf "%s" code ; - if rest <> [] then Format.fprintf ppf "@ " ; + let code = Omd_backend.markdown_of_md [Omd.Code ("", code)] in + Format.fprintf ppf "%s" code; + if rest <> [] then Format.fprintf ppf "@ "; pp_text ppf rest - | Code { code ; runnable = true} :: rest -> + | Code {code; runnable = true} :: rest -> let code = drop_newlines code in - let code = Omd_backend.markdown_of_md [ Omd.Code ("", "| " ^ code ^ " |") ] in - Format.fprintf ppf "%s" code ; + let code = + Omd_backend.markdown_of_md [Omd.Code ("", "| " ^ code ^ " |")] + in + Format.fprintf ppf "%s" code; pp_text ppf rest - | Math code :: rest -> + | Math code :: rest -> let code = drop_newlines code in - let code = Omd_backend.markdown_of_md [ Omd.Code ("", "$ " ^ code ^ " $") ] in - Format.fprintf ppf "%s" code ; - if rest <> [] then Format.fprintf ppf "@ " ; + let code = + Omd_backend.markdown_of_md [Omd.Code ("", "$ " ^ code ^ " $")] + in + Format.fprintf ppf "%s" code; + if rest <> [] then Format.fprintf ppf "@ "; pp_text ppf rest - | Emph text :: rest -> - Format.fprintf ppf "*%a*@," pp_text text ; - if rest <> [] then Format.fprintf ppf "@ " ; + | Emph text :: rest -> + Format.fprintf ppf "*%a*@," pp_text text; + if rest <> [] then Format.fprintf ppf "@ "; pp_text ppf rest | Text t :: rest -> - Format.pp_print_text ppf - (Omd_backend.escape_markdown_characters t) ; - if rest <> [] then Format.fprintf ppf "@ " ; + Format.pp_print_text ppf (Omd_backend.escape_markdown_characters t); + if rest <> [] then Format.fprintf ppf "@ "; pp_text ppf rest - | _ -> assert false in + | _ -> assert false + in let rec pp_content ppf = function - | Code_block { code ; runnable} -> + | Code_block {code; runnable} -> let prefix = if runnable then "| " else "" in let lines = Stringext.split ~on:'\n' code in Format.pp_print_list (fun ppf -> Format.fprintf ppf " %s%s" prefix) ppf lines - | Paragraph text -> - Format.fprintf ppf "@[%a@]" pp_text text + | Paragraph text -> Format.fprintf ppf "@[%a@]" pp_text text | Enum items -> Format.pp_print_list ~pp_sep (fun ppf item -> - Format.fprintf ppf "@[ * %a@]" - (Format.pp_print_list ~pp_sep pp_content) item) - ppf items in - let pp_step ppf { step_title ; step_contents } = + Format.fprintf ppf "@[ * %a@]" + (Format.pp_print_list ~pp_sep pp_content) + item ) + ppf items + in + let pp_step ppf {step_title; step_contents} = let title = Format.asprintf "@[%a@]" pp_text step_title in - Format.fprintf ppf "%s@,%s@,@,%a" - title (String.make (String.length title) '-') - (Format.pp_print_list ~pp_sep pp_content) step_contents in + Format.fprintf ppf "%s@,%s@,@,%a" title + (String.make (String.length title) '-') + (Format.pp_print_list ~pp_sep pp_content) + step_contents + in let title = Format.asprintf "@[%a@]" pp_text tutorial.title in - Format.fprintf ppf "@[\ - %s@,%s@,@,\ - %a@." - title (String.make (String.length title) '=') - (Format.pp_print_list ~pp_sep pp_step) tutorial.steps ; + Format.fprintf ppf "@[%s@,%s@,@,%a@." title + (String.make (String.length title) '=') + (Format.pp_print_list ~pp_sep pp_step) + tutorial.steps; Buffer.contents buffer diff --git a/src/repo/learnocaml_tutorial_parser.mli b/src/repo/learnocaml_tutorial_parser.mli index 24052d52e..0ad983b5a 100644 --- a/src/repo/learnocaml_tutorial_parser.mli +++ b/src/repo/learnocaml_tutorial_parser.mli @@ -9,20 +9,15 @@ open Learnocaml_data val parse_md_tutorial : - tutorial_name: string -> - file_name: string -> - (Tutorial.Index.entry * Tutorial.t) Lwt.t + tutorial_name:string + -> file_name:string + -> (Tutorial.Index.entry * Tutorial.t) Lwt.t val parse_html_tutorial : - tutorial_name: string -> - file_name: string -> - (Tutorial.Index.entry * Tutorial.t) Lwt.t + tutorial_name:string + -> file_name:string + -> (Tutorial.Index.entry * Tutorial.t) Lwt.t -val print_html_tutorial : - tutorial_name: string -> - Tutorial.t -> - string +val print_html_tutorial : tutorial_name:string -> Tutorial.t -> string -val print_md_tutorial : - Tutorial.t -> - string +val print_md_tutorial : Tutorial.t -> string diff --git a/src/repo/learnocaml_tutorial_reader_main.ml b/src/repo/learnocaml_tutorial_reader_main.ml index 3e034fda6..b68ac6996 100644 --- a/src/repo/learnocaml_tutorial_reader_main.ml +++ b/src/repo/learnocaml_tutorial_reader_main.ml @@ -17,13 +17,17 @@ let output_html = ref None (* If and where to export the tutorial as Markdown. *) let output_md = ref None -let args = Arg.align @@ - [ "-output-json", Arg.String (fun s -> output_json := Some s), - "PATH export the tutorial in JSON format in the given file" ; - "-output-html", Arg.String (fun s -> output_html := Some s), - "PATH export the tutorial in HTML format in the given file" ; - "-output-md", Arg.String (fun s -> output_md := Some s), - "PATH export the tutorial in Markdown format in the given file" ] +let args = + Arg.align + @@ [ ( "-output-json" + , Arg.String (fun s -> output_json := Some s) + , "PATH export the tutorial in JSON format in the given file" ) + ; ( "-output-html" + , Arg.String (fun s -> output_html := Some s) + , "PATH export the tutorial in HTML format in the given file" ) + ; ( "-output-md" + , Arg.String (fun s -> output_md := Some s) + , "PATH export the tutorial in Markdown format in the given file" ) ] open Lwt.Infix @@ -31,75 +35,87 @@ let main () = let tutos = ref [] in let anon_fun tuto = tutos := tuto :: !tutos in let usage_msg = - "Usage: ./learnocaml-tutorial-reader [options] " in - Arg.parse args anon_fun usage_msg ; + "Usage: ./learnocaml-tutorial-reader [options] " + in + Arg.parse args anon_fun usage_msg; exit (Lwt_main.run (Lwt.catch (fun () -> - match !tutos with - | [] -> - Format.eprintf "A tutorial file is expected.@." ; - Lwt.return 1 - | _ :: _ :: _ -> - Format.eprintf "A single tutorial file is expected.@." ; - Lwt.return 1 - | [ file_name ] -> - let tutorial_name = - Filename.basename (Filename.chop_extension file_name) in - begin if Filename.check_suffix file_name ".html" then - Learnocaml_tutorial_parser.parse_html_tutorial ~tutorial_name ~file_name - else if Filename.check_suffix file_name ".md" then - Learnocaml_tutorial_parser.parse_md_tutorial ~tutorial_name ~file_name - else if Filename.check_suffix file_name ".json" then - Lwt_io.with_file ~mode: Lwt_io.Input file_name @@ fun chan -> - Lwt_io.read chan >>= fun text -> - let json = - Ezjsonm.from_string text in - let tutorial = - Json_encoding.destruct Tutorial.enc json in - let title = tutorial.Tutorial.title in - Lwt.return - (Tutorial.Index.{ name = tutorial_name ; title }, tutorial) - else - Lwt.fail_with "unrecognized file extension, expecting .md, .html or .json" - end >>= fun (_, tutorial) -> - Lwt.join - [ begin match !output_html with - | None -> Lwt.return () - | Some file_name -> - let text = - Learnocaml_tutorial_parser.print_html_tutorial - ~tutorial_name tutorial in - Lwt_io.with_file ~mode: Lwt_io.Output file_name @@ fun chan -> - Lwt_io.write chan text - end ; - begin match !output_md with - | None -> Lwt.return () - | Some file_name -> - let text = Learnocaml_tutorial_parser.print_md_tutorial tutorial in - Lwt_io.with_file ~mode: Lwt_io.Output file_name @@ fun chan -> - Lwt_io.write chan text - end ; - begin match !output_json with - | None -> Lwt.return () - | Some file_name -> - let json = - Json_encoding.construct Tutorial.enc tutorial in - match json with - | `O _ | `A _ as json -> - Lwt_io.with_file ~mode: Lwt_io.Output file_name @@ fun chan -> - let text = Ezjsonm.to_string json in - Lwt_io.write chan text - | _ -> assert false - end ] >>= fun () -> - Lwt.return 0) + match !tutos with + | [] -> + Format.eprintf "A tutorial file is expected.@."; + Lwt.return 1 + | _ :: _ :: _ -> + Format.eprintf "A single tutorial file is expected.@."; + Lwt.return 1 + | [file_name] -> + let tutorial_name = + Filename.basename (Filename.chop_extension file_name) + in + ( if Filename.check_suffix file_name ".html" then + Learnocaml_tutorial_parser.parse_html_tutorial ~tutorial_name + ~file_name + else if Filename.check_suffix file_name ".md" then + Learnocaml_tutorial_parser.parse_md_tutorial ~tutorial_name + ~file_name + else if Filename.check_suffix file_name ".json" then + Lwt_io.with_file ~mode:Lwt_io.Input file_name + @@ fun chan -> + Lwt_io.read chan + >>= fun text -> + let json = Ezjsonm.from_string text in + let tutorial = Json_encoding.destruct Tutorial.enc json in + let title = tutorial.Tutorial.title in + Lwt.return + (Tutorial.Index.{name = tutorial_name; title}, tutorial) + else + Lwt.fail_with + "unrecognized file extension, expecting .md, .html or .json" + ) + >>= fun (_, tutorial) -> + Lwt.join + [ ( match !output_html with + | None -> Lwt.return () + | Some file_name -> + let text = + Learnocaml_tutorial_parser.print_html_tutorial + ~tutorial_name tutorial + in + Lwt_io.with_file ~mode:Lwt_io.Output file_name + @@ fun chan -> Lwt_io.write chan text ) + ; ( match !output_md with + | None -> Lwt.return () + | Some file_name -> + let text = + Learnocaml_tutorial_parser.print_md_tutorial tutorial + in + Lwt_io.with_file ~mode:Lwt_io.Output file_name + @@ fun chan -> Lwt_io.write chan text ) + ; ( match !output_json with + | None -> Lwt.return () + | Some file_name -> ( + let json = + Json_encoding.construct Tutorial.enc tutorial + in + match json with + | (`O _ | `A _) as json -> + Lwt_io.with_file ~mode:Lwt_io.Output file_name + @@ fun chan -> + let text = Ezjsonm.to_string json in + Lwt_io.write chan text + | _ -> assert false ) ) ] + >>= fun () -> Lwt.return 0 ) (fun exn -> - let print_unknown ppf = function - | Failure msg -> Format.fprintf ppf "Cannot process tutorial: %s" msg - | exn -> Format.fprintf ppf "Cannot process tutorial: %s" (Printexc.to_string exn) in - Json_encoding.print_error ~print_unknown Format.err_formatter exn ; - Format.eprintf "@." ; - Lwt.return 1))) + let print_unknown ppf = function + | Failure msg -> + Format.fprintf ppf "Cannot process tutorial: %s" msg + | exn -> + Format.fprintf ppf "Cannot process tutorial: %s" + (Printexc.to_string exn) + in + Json_encoding.print_error ~print_unknown Format.err_formatter exn; + Format.eprintf "@."; + Lwt.return 1 ))) let () = main () diff --git a/src/server/learnocaml_server.ml b/src/server/learnocaml_server.ml index bcb01291d..82c94cd61 100644 --- a/src/server/learnocaml_server.ml +++ b/src/server/learnocaml_server.ml @@ -17,24 +17,26 @@ let log_channel = ref (Some stdout) let base_url = ref "" -let args = Arg.align @@ - [ "-static-dir", Arg.Set_string static_dir, - "PATH where static files should be found (./www)" ; - "-sync-dir", Arg.Set_string sync_dir, - "PATH where sync tokens are stored (./sync)" ; - "-base-url", Arg.Set_string base_url, - "BASE_URL of the website. \ - Should not end with a trailing slash. \ - Currently, this has no effect on the native backend. \ - Mandatory for 'learn-ocaml build' if the site is not hosted in path '/', \ - which typically occurs for static deployment." ; - "-port", Arg.Set_int port, - "PORT the TCP port (8080)" ] +let args = + Arg.align + @@ [ ( "-static-dir" + , Arg.Set_string static_dir + , "PATH where static files should be found (./www)" ) + ; ( "-sync-dir" + , Arg.Set_string sync_dir + , "PATH where sync tokens are stored (./sync)" ) + ; ( "-base-url" + , Arg.Set_string base_url + , "BASE_URL of the website. Should not end with a trailing slash. \ + Currently, this has no effect on the native backend. Mandatory for \ + 'learn-ocaml build' if the site is not hosted in path '/', which \ + typically occurs for static deployment." ) + ; ("-port", Arg.Set_int port, "PORT the TCP port (8080)") ] open Lwt.Infix let read_static_file path = - Lwt_io.(with_file ~mode: Input (sanitise_path !static_dir path) read) + Lwt_io.(with_file ~mode:Input (sanitise_path !static_dir path) read) exception Too_long_body @@ -43,23 +45,17 @@ let string_of_stream ?(max_size = 1024 * 1024) s = let pos = ref 0 in let add_string s = pos := !pos + String.length s; - if !pos > max_size then - Lwt.fail Too_long_body - else begin - Buffer.add_string b s; - Lwt.return_unit - end + if !pos > max_size then Lwt.fail Too_long_body + else ( Buffer.add_string b s; Lwt.return_unit ) in - Lwt.catch begin function () -> - Lwt_stream.iter_s add_string s >>= fun () -> - Lwt.return (Some (Buffer.contents b)) - end begin function - | Too_long_body -> Lwt.return None - | e -> Lwt.fail e - end + Lwt.catch + (function + | () -> + Lwt_stream.iter_s add_string s + >>= fun () -> Lwt.return (Some (Buffer.contents b))) + (function Too_long_body -> Lwt.return None | e -> Lwt.fail e) module Api = Learnocaml_api - open Cohttp_lwt_unix type cache_request_hash = string list @@ -67,88 +63,79 @@ type cache_request_hash = string list (** nocache, shortcache, longcache indicates the client-side caching. The associated key is used for server-side caching *) type caching = - | Nocache (* dynamic resources *) - | Shortcache of cache_request_hash option (* valid for the server lifetime *) - | Longcache of cache_request_hash (* static resources *) + | Nocache + (* dynamic resources *) + | Shortcache of cache_request_hash option + (* valid for the server lifetime *) + | Longcache of cache_request_hash + +(* static resources *) -type cached_response = { - body: string; - deflated_body: string option; - content_type: string; - caching: caching; -} +type cached_response = + { body : string + ; deflated_body : string option + ; content_type : string + ; caching : caching } type 'a response = - | Response of { contents: 'a; - content_type: string; - caching: caching } + | Response of {contents : 'a; content_type : string; caching : caching} | Cached of cached_response -type error = (Cohttp.Code.status_code * string) +type error = Cohttp.Code.status_code * string -let caching: type resp. resp Api.request -> caching = function +let caching : type resp. resp Api.request -> caching = function | Api.Version () -> Shortcache (Some ["version"; "server_id"]) - | Api.Static ("fonts"::_ | "icons"::_ | "js"::_::_::_ as p) -> Longcache p - | Api.Static ("css"::_ | "js"::_ | _ as p) -> Shortcache (Some p) - + | Api.Static (("fonts" :: _ | "icons" :: _ | "js" :: _ :: _ :: _) as p) -> + Longcache p + | Api.Static (("css" :: _ | "js" :: _ | _) as p) -> Shortcache (Some p) | Api.Exercise _ -> Nocache - | Api.Lesson_index () -> Shortcache (Some ["lessons"]) - | Api.Lesson id -> Shortcache (Some ["lesson";id]) + | Api.Lesson id -> Shortcache (Some ["lesson"; id]) | Api.Tutorial_index () -> Shortcache (Some ["tutorials"]) - | Api.Tutorial id -> Shortcache (Some ["tutorial";id]) - + | Api.Tutorial id -> Shortcache (Some ["tutorial"; id]) | _ -> Nocache let lwt_ok r = Lwt.return (Ok r) + let lwt_fail e = Lwt.return (Error e) -let ( >?= ) x f = - x >>= function - | Ok x -> f x - | Error x -> lwt_fail x +let ( >?= ) x f = x >>= function Ok x -> f x | Error x -> lwt_fail x -let lwt_catch_fail f e = - Lwt.catch f (fun exn -> lwt_fail @@ e exn) +let lwt_catch_fail f e = Lwt.catch f (fun exn -> lwt_fail @@ e exn) -let lwt_option_fail x e f = - match x with - | Some x -> f x - | None -> lwt_fail e +let lwt_option_fail x e f = match x with Some x -> f x | None -> lwt_fail e let respond_static caching path = lwt_catch_fail (fun () -> - read_static_file path >>= fun contents -> - let content_type = - Magic_mime.lookup (List.fold_left (fun _ r -> r) "" path) - in - lwt_ok @@ Response { contents; content_type; caching }) + read_static_file path + >>= fun contents -> + let content_type = + Magic_mime.lookup (List.fold_left (fun _ r -> r) "" path) + in + lwt_ok @@ Response {contents; content_type; caching} ) (fun e -> (`Not_found, Printexc.to_string e)) let respond_json caching contents = - lwt_ok @@ - Response { contents; - content_type = "application/json"; - caching } + lwt_ok @@ Response {contents; content_type = "application/json"; caching} let verify_teacher_token token = - Token.check_teacher token >>= function - | true -> lwt_ok () - | false -> lwt_fail (`Forbidden,"Access restricted") + Token.check_teacher token + >>= function + | true -> lwt_ok () | false -> lwt_fail (`Forbidden, "Access restricted") let string_of_date ts = let open Unix in let tm = gmtime ts in - Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ" - (tm.tm_year + 1900) (tm.tm_mon + 1) tm.tm_mday - tm.tm_hour tm.tm_min tm.tm_sec + Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ" (tm.tm_year + 1900) + (tm.tm_mon + 1) tm.tm_mday tm.tm_hour tm.tm_min tm.tm_sec let log conn api_req = match !log_channel with | None -> () | Some oc -> - let src_addr = match conn with + let src_addr = + match conn with | Conduit_lwt_unix.TCP tcp, _ -> Ipaddr.to_string tcp.Conduit_lwt_unix.ip | _ -> "" @@ -158,18 +145,17 @@ let log conn api_req = output_string oc src_addr; output_char oc '\t'; output_string oc - (match api_req.Api.meth with - | `GET -> "GET " - | `POST _ -> "POST"); + (match api_req.Api.meth with `GET -> "GET " | `POST _ -> "POST"); output_char oc '\t'; output_char oc '/'; output_string oc (String.concat "/" api_req.Api.path); - (match api_req.Api.args with | [] -> () | l -> + ( match api_req.Api.args with + | [] -> () + | l -> output_char oc '?'; output_string oc - (String.concat "&" (List.map (fun (a, b) -> a ^"="^ b) l))); - output_char oc '\n'; - flush oc + (String.concat "&" (List.map (fun (a, b) -> a ^ "=" ^ b) l)) ); + output_char oc '\n'; flush oc let check_report exo report grade = let max_grade = Learnocaml_exercise.(access File.max_score) exo in @@ -177,151 +163,156 @@ let check_report exo report grade = score * 100 / max_grade = grade module Memory_cache = struct - - let (tbl: (cache_request_hash, cached_response) Hashtbl.t) = + let (tbl : (cache_request_hash, cached_response) Hashtbl.t) = Hashtbl.create 533 - let get key = - try Some (Hashtbl.find tbl key) with Not_found -> None - - let add key entry = - Hashtbl.replace tbl key entry + let get key = try Some (Hashtbl.find tbl key) with Not_found -> None + let add key entry = Hashtbl.replace tbl key entry end module Request_handler = struct - type 'a ret = ('a response, error) result Lwt.t let map_ret f r = - r >?= function - | Response ({contents; _} as r) -> lwt_ok @@ Response {r with contents = f contents} - | (Cached _) as r -> lwt_ok r + r + >?= function + | Response ({contents; _} as r) -> + lwt_ok @@ Response {r with contents = f contents} + | Cached _ as r -> lwt_ok r + + let alphanum = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - let alphanum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" let alphanum_len = String.length alphanum let nonce_req : (string, string) Hashtbl.t = Hashtbl.create 533 let token_save_mutex = Lwt_utils.gen_mutex_table () - let rec string_of_endp = - function - | `TCP (i,_) -> Some (Ipaddr.to_string i) + let rec string_of_endp = function + | `TCP (i, _) -> Some (Ipaddr.to_string i) | `Unix_domain_socket s -> Some s - | `Vchan_direct (i,_) -> Some (string_of_int i) - | `Vchan_domain_socket (u,v) -> Some (u ^ v) - | `TLS (s,e) -> - begin - match string_of_endp e with - | Some s' -> Some (s ^ s') - | None -> None - end + | `Vchan_direct (i, _) -> Some (string_of_int i) + | `Vchan_domain_socket (u, v) -> Some (u ^ v) + | `TLS (s, e) -> ( + match string_of_endp e with Some s' -> Some (s ^ s') | None -> None ) | `Unknown _ -> None let valid_string_of_endp e = - lwt_option_fail - (string_of_endp e) + lwt_option_fail (string_of_endp e) (`Forbidden, "No address information avaible") lwt_ok - let callback_raw: type resp. Conduit.endp -> Learnocaml_data.Server.config -> - caching -> resp Api.request -> - (resp response, error) result Lwt.t - = let module ServerData = Learnocaml_data.Server in - fun conn config cache -> function + let callback_raw : type resp. + Conduit.endp + -> Learnocaml_data.Server.config + -> caching + -> resp Api.request + -> (resp response, error) result Lwt.t = + let module ServerData = Learnocaml_data.Server in + fun conn config cache -> function | Api.Version () -> - respond_json cache (Api.version, config.ServerData.server_id) - | Api.Static path -> - respond_static cache path + respond_json cache (Api.version, config.ServerData.server_id) + | Api.Static path -> respond_static cache path | Api.Nonce () -> - valid_string_of_endp conn - >?= fun conn -> - let nonce = - match Hashtbl.find_opt nonce_req conn with - | Some old -> old - | None -> - let nonce = String.init 20 (fun _ -> alphanum.[Random.int alphanum_len]) in - Hashtbl.add nonce_req conn nonce; - nonce - in respond_json cache nonce + valid_string_of_endp conn + >?= fun conn -> + let nonce = + match Hashtbl.find_opt nonce_req conn with + | Some old -> old + | None -> + let nonce = + String.init 20 (fun _ -> alphanum.[Random.int alphanum_len]) + in + Hashtbl.add nonce_req conn nonce; + nonce + in + respond_json cache nonce | Api.Create_token (secret_candidate, None, nick) -> - valid_string_of_endp conn - >?= fun conn -> - lwt_option_fail - (Hashtbl.find_opt nonce_req conn) - (`Forbidden, "No registered token for your address") - @@ fun nonce -> - Hashtbl.remove nonce_req conn; - let know_secret = - match config.ServerData.secret with - | None -> true - | Some x -> Sha.sha512 (nonce ^ x) = secret_candidate in - if not know_secret - then lwt_fail (`Forbidden, "Bad secret") - else - Token.create_student () - >>= fun tok -> - (match nick with | None -> Lwt.return_unit - | Some nickname -> - Save.set tok Save.{empty with nickname}) - >>= fun () -> respond_json cache tok + valid_string_of_endp conn + >?= fun conn -> + lwt_option_fail + (Hashtbl.find_opt nonce_req conn) + (`Forbidden, "No registered token for your address") + @@ fun nonce -> + Hashtbl.remove nonce_req conn; + let know_secret = + match config.ServerData.secret with + | None -> true + | Some x -> Sha.sha512 (nonce ^ x) = secret_candidate + in + if not know_secret then lwt_fail (`Forbidden, "Bad secret") + else + Token.create_student () + >>= fun tok -> + ( match nick with + | None -> Lwt.return_unit + | Some nickname -> Save.set tok Save.{empty with nickname} ) + >>= fun () -> respond_json cache tok | Api.Create_token (_secret_candidate, Some token, _nick) -> - lwt_catch_fail - (fun () -> Token.register token >>= fun () -> respond_json cache token) + lwt_catch_fail + (fun () -> + Token.register token >>= fun () -> respond_json cache token ) (function - | Failure body -> (`Bad_request, body) - | exn -> (`Internal_server_error, Printexc.to_string exn)) + | Failure body -> (`Bad_request, body) + | exn -> (`Internal_server_error, Printexc.to_string exn)) | Api.Create_teacher_token token -> - verify_teacher_token token - >?= fun () -> - Token.create_teacher () - >>= respond_json cache - + verify_teacher_token token + >?= fun () -> Token.create_teacher () >>= respond_json cache | Api.Fetch_save token -> - lwt_catch_fail - (fun () -> - Save.get token >>= fun tokopt -> - lwt_option_fail - tokopt - (`Not_found, "token not found") - (respond_json cache)) - (fun exn -> (`Internal_server_error, Printexc.to_string exn)) + lwt_catch_fail + (fun () -> + Save.get token + >>= fun tokopt -> + lwt_option_fail tokopt + (`Not_found, "token not found") + (respond_json cache) ) + (fun exn -> (`Internal_server_error, Printexc.to_string exn)) | Api.Archive_zip token -> let open Lwt_process in - let path = Filename.concat !sync_dir (Token.to_path token) in - let cmd = shell ("git archive master --format=zip -0 --remote="^path) + let path = Filename.concat !sync_dir (Token.to_path token) in + let cmd = + shell ("git archive master --format=zip -0 --remote=" ^ path) and stdout = `FD_copy Unix.stdout in - Lwt_process.pread ~stdin:stdout cmd >>= fun contents -> - lwt_ok @@ Response { contents = contents; - content_type = "application/zip"; - caching = Nocache } + Lwt_process.pread ~stdin:stdout cmd + >>= fun contents -> + lwt_ok + @@ Response + {contents; content_type = "application/zip"; caching = Nocache} | Api.Update_save (token, save) -> let save = Save.fix_mtimes save in let exercise_states = SMap.bindings save.Save.all_exercise_states in - (Token.check_teacher token >>= function - | true -> Lwt.return exercise_states - | false -> - Lwt_list.filter_s (fun (id, _) -> - Exercise.Status.is_open id token >|= function - | `Open -> true - | `Closed -> false - | `Deadline t -> t >= -300. (* Grace period! *)) - exercise_states) + Token.check_teacher token + >>= (function + | true -> Lwt.return exercise_states + | false -> + Lwt_list.filter_s + (fun (id, _) -> + Exercise.Status.is_open id token + >|= function + | `Open -> true + | `Closed -> false + | `Deadline t -> t >= -300. + (* Grace period! *) ) + exercise_states) >>= fun valid_exercise_states -> let save = { save with Save.all_exercise_states = - List.fold_left (fun m (id,save) -> SMap.add id save m) + List.fold_left + (fun m (id, save) -> SMap.add id save m) SMap.empty valid_exercise_states } in - token_save_mutex.Lwt_utils.with_lock (token :> Token.t) (fun () -> - Save.get token >>= fun x -> - lwt_option_fail x - (`Not_found, Token.to_string token) + token_save_mutex.Lwt_utils.with_lock + (token :> Token.t) + (fun () -> + Save.get token + >>= fun x -> + lwt_option_fail x (`Not_found, Token.to_string token) @@ fun prev_save -> - let save = Save.sync prev_save save in - Save.set token save >>= fun () -> respond_json cache save) + let save = Save.sync prev_save save in + Save.set token save >>= fun () -> respond_json cache save ) | Api.Git (token, path) -> let prefix = let ( / ) = Filename.concat in @@ -330,296 +321,319 @@ module Request_handler = struct let path = sanitise_path prefix path in lwt_catch_fail (fun () -> - Lwt_io.(with_file ~mode:Input path read) >>= fun contents -> - lwt_ok @@ - Response { contents; - content_type = "application/octet-stream"; - caching = Nocache }) + Lwt_io.(with_file ~mode:Input path read) + >>= fun contents -> + lwt_ok + @@ Response + { contents + ; content_type = "application/octet-stream" + ; caching = Nocache } ) (fun e -> (`Not_found, Printexc.to_string e)) - | Api.Students_list token -> - verify_teacher_token token >?= fun () -> - Student.Index.get () - >>= respond_json cache + verify_teacher_token token + >?= fun () -> Student.Index.get () >>= respond_json cache | Api.Set_students_list (token, students) -> - verify_teacher_token token >?= fun () -> + verify_teacher_token token + >?= fun () -> Lwt_list.map_s (fun (ancestor, ours) -> - let token = ancestor.Student.token in - Student.get token >|= fun theirs -> - let theirs = match theirs with - | None -> Student.default token - | Some std -> std - in - Student.three_way_merge ~ancestor ~theirs ~ours) - students >>= - Student.Index.set - >>= respond_json cache + let token = ancestor.Student.token in + Student.get token + >|= fun theirs -> + let theirs = + match theirs with + | None -> Student.default token + | Some std -> std + in + Student.three_way_merge ~ancestor ~theirs ~ours ) + students + >>= Student.Index.set >>= respond_json cache | Api.Students_csv (token, exercises, students) -> - verify_teacher_token token >?= fun () -> - (match students with - | [] -> Token.Index.get () >|= List.filter Token.is_student - | l -> Lwt.return l) + verify_teacher_token token + >?= fun () -> + ( match students with + | [] -> Token.Index.get () >|= List.filter Token.is_student + | l -> Lwt.return l ) >>= Lwt_list.map_p (fun token -> - Save.get token >|= fun save -> token, save) + Save.get token >|= fun save -> (token, save) ) >>= fun tok_saves -> let all_exercises = match exercises with | [] -> - List.fold_left (fun acc (_tok, save) -> + List.fold_left + (fun acc (_tok, save) -> match save with | None -> acc | Some save -> - SMap.fold (fun ex_id _ans acc -> SSet.add ex_id acc) - save.Save.all_exercise_states - acc) + SMap.fold + (fun ex_id _ans acc -> SSet.add ex_id acc) + save.Save.all_exercise_states acc ) SSet.empty tok_saves |> SSet.elements | exercises -> exercises in let columns = - "token" :: "nickname" :: - (List.fold_left (fun acc ex_id -> - (ex_id ^ " grade") :: - (ex_id ^ " date") :: - acc) - [] (List.rev all_exercises)) + "token" :: "nickname" + :: List.fold_left + (fun acc ex_id -> + (ex_id ^ " grade") :: (ex_id ^ " date") :: acc ) + [] (List.rev all_exercises) in let buf = Buffer.create 3497 in let sep () = Buffer.add_char buf ',' in let line () = Buffer.add_char buf '\n' in Buffer.add_string buf (String.concat "," columns); line (); - Lwt_list.iter_s (fun (tok, save) -> - match save with None -> Lwt.return_unit | Some save -> - Buffer.add_string buf (Token.to_string tok); - sep (); - Buffer.add_string buf save.Save.nickname; - Lwt_list.iter_s (fun ex_id -> - Lwt.catch (fun () -> - sep (); - Exercise.get ex_id >>= fun exo -> - Lwt.wrap2 SMap.find ex_id save.Save.all_exercise_states - >|= fun st -> - (match st.Answer.grade with - | None -> () - | Some grade -> - if match st.Answer.report with - | None -> false - | Some rep -> check_report exo rep grade - then Buffer.add_string buf (string_of_int grade) - else Printf.bprintf buf "CHEAT(%d)" grade); - sep (); - Buffer.add_string buf (string_of_date st.Answer.mtime)) - (function - | Not_found -> sep (); Lwt.return_unit - | e -> raise e)) - all_exercises - >|= line) + Lwt_list.iter_s + (fun (tok, save) -> + match save with + | None -> Lwt.return_unit + | Some save -> + Buffer.add_string buf (Token.to_string tok); + sep (); + Buffer.add_string buf save.Save.nickname; + Lwt_list.iter_s + (fun ex_id -> + Lwt.catch + (fun () -> + sep (); + Exercise.get ex_id + >>= fun exo -> + Lwt.wrap2 SMap.find ex_id + save.Save.all_exercise_states + >|= fun st -> + ( match st.Answer.grade with + | None -> () + | Some grade -> + if + match st.Answer.report with + | None -> false + | Some rep -> check_report exo rep grade + then Buffer.add_string buf (string_of_int grade) + else Printf.bprintf buf "CHEAT(%d)" grade ); + sep (); + Buffer.add_string buf + (string_of_date st.Answer.mtime) ) + (function + | Not_found -> sep (); Lwt.return_unit | e -> raise e) + ) + all_exercises + >|= line ) tok_saves >>= fun () -> - lwt_ok @@ - Response {contents = Buffer.contents buf; - content_type = "text/csv"; - caching = Nocache} - + lwt_ok + @@ Response + { contents = Buffer.contents buf + ; content_type = "text/csv" + ; caching = Nocache } | Api.Exercise_index (Some token) -> - Exercise.Index.get () >>= fun index -> - Token.check_teacher token >>= (function - | true -> Lwt.return (index, []) - | false -> - let deadlines = ref [] in - Exercise.Index.filterk - (fun id _ k -> - Exercise.Status.is_open id token >>= function - | `Open -> k true - | `Closed -> k false - | `Deadline t -> - deadlines := (id, max t 0.) :: !deadlines; - k true) - index (fun index -> Lwt.return (index, !deadlines))) + Exercise.Index.get () + >>= fun index -> + Token.check_teacher token + >>= (function + | true -> Lwt.return (index, []) + | false -> + let deadlines = ref [] in + Exercise.Index.filterk + (fun id _ k -> + Exercise.Status.is_open id token + >>= function + | `Open -> k true + | `Closed -> k false + | `Deadline t -> + deadlines := (id, max t 0.) :: !deadlines; + k true ) + index + (fun index -> Lwt.return (index, !deadlines))) >>= respond_json cache - | Api.Exercise_index None -> - lwt_fail (`Forbidden, "Forbidden") - - | Api.Exercise (Some token, id) -> - (Exercise.Status.is_open id token >>= function - | `Open | `Deadline _ as o -> - Exercise.Meta.get id >>= fun meta -> - Exercise.get id >>= fun ex -> + | Api.Exercise_index None -> lwt_fail (`Forbidden, "Forbidden") + | Api.Exercise (Some token, id) -> ( + Exercise.Status.is_open id token + >>= function + | (`Open | `Deadline _) as o -> + Exercise.Meta.get id + >>= fun meta -> + Exercise.get id + >>= fun ex -> respond_json cache - (meta, ex, - match o with `Deadline t -> Some (max t 0.) | `Open -> None) - | `Closed -> - lwt_fail (`Forbidden, "Exercise closed")) - | Api.Exercise (None, _) -> - lwt_fail (`Forbidden, "Forbidden") - - | Api.Lesson_index () -> - Lesson.Index.get () >>= respond_json cache - | Api.Lesson id -> - Lesson.get id >>= respond_json cache - - | Api.Tutorial_index () -> - Tutorial.Index.get () >>= respond_json cache - | Api.Tutorial id -> - Tutorial.get id >>= respond_json cache - + ( meta + , ex + , match o with `Deadline t -> Some (max t 0.) | `Open -> None + ) + | `Closed -> lwt_fail (`Forbidden, "Exercise closed") ) + | Api.Exercise (None, _) -> lwt_fail (`Forbidden, "Forbidden") + | Api.Lesson_index () -> Lesson.Index.get () >>= respond_json cache + | Api.Lesson id -> Lesson.get id >>= respond_json cache + | Api.Tutorial_index () -> Tutorial.Index.get () >>= respond_json cache + | Api.Tutorial id -> Tutorial.get id >>= respond_json cache | Api.Playground_index () -> Playground.Index.get () >>= respond_json cache - | Api.Playground id -> - Playground.get id >>= respond_json cache - + | Api.Playground id -> Playground.get id >>= respond_json cache | Api.Exercise_status_index token -> - verify_teacher_token token >?= fun () -> - Exercise.Status.all () >>= respond_json cache + verify_teacher_token token + >?= fun () -> Exercise.Status.all () >>= respond_json cache | Api.Exercise_status (token, id) -> - verify_teacher_token token >?= fun () -> - Exercise.Status.get id >>= respond_json cache + verify_teacher_token token + >?= fun () -> Exercise.Status.get id >>= respond_json cache | Api.Set_exercise_status (token, status) -> - verify_teacher_token token >?= fun () -> + verify_teacher_token token + >?= fun () -> Lwt_list.iter_s - Exercise.Status.(fun (ancestor, ours) -> - get ancestor.id >>= fun theirs -> - set (three_way_merge ~ancestor ~theirs ~ours)) + Exercise.Status.( + fun (ancestor, ours) -> + get ancestor.id + >>= fun theirs -> set (three_way_merge ~ancestor ~theirs ~ours)) status >>= respond_json cache - | Api.Partition (token, eid, fid, prof) -> - lwt_catch_fail (fun () -> - verify_teacher_token token - >?= fun () -> - Learnocaml_partition_create.partition eid fid prof - >>= respond_json cache - ) - (fun exn -> (`Not_found, Printexc.to_string exn)) - - | Api.Invalid_request body -> - lwt_fail (`Bad_request, body) - - let callback: type resp. Conduit.endp -> - Learnocaml_data.Server.config -> - resp Api.request -> resp ret - = fun conn config req -> + lwt_catch_fail + (fun () -> + verify_teacher_token token + >?= fun () -> + Learnocaml_partition_create.partition eid fid prof + >>= respond_json cache ) + (fun exn -> (`Not_found, Printexc.to_string exn)) + | Api.Invalid_request body -> lwt_fail (`Bad_request, body) + + let callback : type resp. + Conduit.endp + -> Learnocaml_data.Server.config + -> resp Api.request + -> resp ret = + fun conn config req -> let cache = caching req in let respond () = Lwt.catch (fun () -> callback_raw conn config cache req) (function - | Not_found -> - lwt_fail (`Not_found, "Component not found") + | Not_found -> lwt_fail (`Not_found, "Component not found") | e -> raise e) in match cache with | Nocache | Shortcache None -> respond () - | Longcache key | Shortcache (Some key) -> - match Memory_cache.get key with - | Some c -> lwt_ok (Cached c) - | None -> respond () - + | Longcache key | Shortcache (Some key) -> ( + match Memory_cache.get key with + | Some c -> lwt_ok (Cached c) + | None -> respond () ) end module Api_server = Api.Server (Json_codec) (Request_handler) let init_teacher_token () = - Token.Index.get () >>= function tokens -> + Token.Index.get () + >>= function + | tokens -> ( match List.filter Token.is_teacher tokens with | [] -> - Token.create_teacher () >|= fun token -> + Token.create_teacher () + >|= fun token -> Printf.printf "Initial teacher token created: %s\n%!" (Token.to_string token) | teachers -> Printf.printf "Found the following teacher tokens:\n - %s\n%!" (String.concat "\n - " (List.map Token.to_string teachers)); - Lwt.return_unit + Lwt.return_unit ) -let last_modified = (* server startup time *) +let last_modified = + (* server startup time *) let open Unix in let tm = gmtime (gettimeofday ()) in Printf.sprintf "%s, %02d %s %04d %02d:%02d:%02d GMT" - (match tm.tm_wday with - | 0 -> "Sun" | 1 -> "Mon" | 2 -> "Tue" | 3 -> "Wed" - | 4 -> "Thu" | 5 -> "Fri" | 6 -> "Sat" - | _ -> assert false) + ( match tm.tm_wday with + | 0 -> "Sun" + | 1 -> "Mon" + | 2 -> "Tue" + | 3 -> "Wed" + | 4 -> "Thu" + | 5 -> "Fri" + | 6 -> "Sat" + | _ -> assert false ) tm.tm_mday - (match tm.tm_mon with - | 0 -> "Jan" | 1 -> "Feb" | 2 -> "Mar" | 3 -> "Apr" | 4 -> "May" - | 5 -> "Jun" | 6 -> "Jul" | 7 -> "Aug" | 8 -> "Sep" | 9 -> "Oct" - | 10 -> "Nov" | 11 -> "Dec" | _ -> assert false) - (tm.tm_year + 1900) - tm.tm_hour tm.tm_min tm.tm_sec + ( match tm.tm_mon with + | 0 -> "Jan" + | 1 -> "Feb" + | 2 -> "Mar" + | 3 -> "Apr" + | 4 -> "May" + | 5 -> "Jun" + | 6 -> "Jul" + | 7 -> "Aug" + | 8 -> "Sep" + | 9 -> "Oct" + | 10 -> "Nov" + | 11 -> "Dec" + | _ -> assert false ) + (tm.tm_year + 1900) tm.tm_hour tm.tm_min tm.tm_sec (* Taken from the source of "decompress", from bin/easy.ml *) let compress ?(level = 4) data = let input_buffer = Bytes.create 0xFFFF in let output_buffer = Bytes.create 0xFFFF in - let pos = ref 0 in let res = Buffer.create (String.length data) in - Lwt_preemptive.detach - (Decompress.Zlib_deflate.bytes - input_buffer - output_buffer + (Decompress.Zlib_deflate.bytes input_buffer output_buffer (fun input_buffer -> function - | Some max -> - let n = min max (min 0xFFFF (String.length data - !pos)) in - Bytes.blit_string data !pos input_buffer 0 n; - pos := !pos + n; - n - | None -> - let n = min 0xFFFF (String.length data - !pos) in - Bytes.blit_string data !pos input_buffer 0 n; - pos := !pos + n; - n) + | Some max -> + let n = min max (min 0xFFFF (String.length data - !pos)) in + Bytes.blit_string data !pos input_buffer 0 n; + pos := !pos + n; + n + | None -> + let n = min 0xFFFF (String.length data - !pos) in + Bytes.blit_string data !pos input_buffer 0 n; + pos := !pos + n; + n ) (fun output_buffer len -> - Buffer.add_subbytes res output_buffer 0 len; - 0xFFFF)) + Buffer.add_subbytes res output_buffer 0 len; + 0xFFFF )) (Decompress.Zlib_deflate.default ~witness:Decompress.B.bytes level) >>= function | Ok _ -> Lwt.return (Buffer.contents res) | Error _ -> Lwt.fail_with "Could not compress" let launch () = - Random.self_init () ; - Learnocaml_store.Server.get () >>= fun config -> + Random.self_init (); + Learnocaml_store.Server.get () + >>= fun config -> let callback conn req body = let uri = Request.uri req in let path = Uri.path uri in let path = Stringext.split ~on:'/' path in let path = let rec clean = function - | [] | [_] as l -> l - | ""::l -> clean l - | s::l -> s::clean l + | ([] | [_]) as l -> l + | "" :: l -> clean l + | s :: l -> s :: clean l in clean path in let path = List.map Uri.pct_decode path in let query = Uri.query uri in - let args = List.map (fun (s, l) -> s, String.concat "," l) query in + let args = List.map (fun (s, l) -> (s, String.concat "," l)) query in let use_compression = - List.exists (function _, Cohttp.Accept.Deflate -> true | _ -> false) + List.exists + (function _, Cohttp.Accept.Deflate -> true | _ -> false) (Cohttp.Header.get_acceptable_encodings req.Request.headers) in let respond = function - | Response {contents=body; content_type; caching; _} - | Cached {body; content_type; caching; _} as resp -> + | ( Response {contents = body; content_type; caching; _} + | Cached {body; content_type; caching; _} ) as resp -> let headers = Cohttp.Header.init_with "Content-Type" content_type in - let headers = match caching with + let headers = + match caching with | Longcache _ -> - Cohttp.Header.add headers - "Cache-Control" "public, immutable, max-age=2592000" - (* 1 month *) + Cohttp.Header.add headers "Cache-Control" + "public, immutable, max-age=2592000" + (* 1 month *) | Shortcache _ -> - Cohttp.Header.add_list headers [ - "Last-Modified", last_modified; - "Cache-Control", "private, must-revalidate"; - ] - | Nocache -> - Cohttp.Header.add headers "Cache-Control" "no-cache" + Cohttp.Header.add_list headers + [ ("Last-Modified", last_modified) + ; ("Cache-Control", "private, must-revalidate") ] + | Nocache -> Cohttp.Header.add headers "Cache-Control" "no-cache" in - let resp = match resp, caching with + let resp = + match (resp, caching) with | Response _, (Longcache key | Shortcache (Some key)) -> let cached = {body; deflated_body = None; content_type; caching} @@ -628,72 +642,77 @@ let launch () = Cached cached | _ -> resp in - Lwt.try_bind (fun () -> - if use_compression && String.length body >= 1024 && - match String.split_on_char '/' content_type with - | "text"::_ - | "application" :: ("javascript" | "json") :: _ - | "image" :: ("gif" | "svg+xml") :: _ -> true - | _ -> false + Lwt.try_bind + (fun () -> + if + use_compression + && String.length body >= 1024 + && + match String.split_on_char '/' content_type with + | "text" :: _ + |"application" :: ("javascript" | "json") :: _ + |"image" :: ("gif" | "svg+xml") :: _ -> + true + | _ -> false then - (match resp with - | Cached {deflated_body = Some s; _} -> Lwt.return s - | Cached - ({deflated_body = None; - caching = Longcache key | Shortcache Some key; - _ } as c) -> - compress body >|= fun s -> - Memory_cache.add key {c with deflated_body = Some s}; - s - | _ -> compress body) >|= fun s -> - Cohttp.Header.add headers "Content-Encoding" "deflate", s - else - Lwt.return (headers, body)) + ( match resp with + | Cached {deflated_body = Some s; _} -> Lwt.return s + | Cached + ( { deflated_body = None + ; caching = Longcache key | Shortcache (Some key); _ } as + c ) -> + compress body + >|= fun s -> + Memory_cache.add key {c with deflated_body = Some s}; + s + | _ -> compress body ) + >|= fun s -> + (Cohttp.Header.add headers "Content-Encoding" "deflate", s) + else Lwt.return (headers, body) ) (fun (headers, str) -> - Server.respond_string ~headers ~status:`OK ~body:str ()) + Server.respond_string ~headers ~status:`OK ~body:str () ) (fun e -> Server.respond_error ~status:`Internal_server_error - ~body:(Printexc.to_string e) ()) + ~body:(Printexc.to_string e) () ) in - if Cohttp.Header.get req.Request.headers "If-Modified-Since" = - Some last_modified + if + Cohttp.Header.get req.Request.headers "If-Modified-Since" + = Some last_modified then Server.respond ~status:`Not_modified ~body:Cohttp_lwt.Body.empty () else - (match req.Request.meth with - | `GET -> lwt_ok {Api.meth = `GET; path; args} - | `POST -> - begin + ( match req.Request.meth with + | `GET -> lwt_ok {Api.meth = `GET; path; args} + | `POST -> ( string_of_stream (Cohttp_lwt.Body.to_stream body) >>= function | Some s -> lwt_ok {Api.meth = `POST s; path; args} - | None -> lwt_fail (`Bad_request, "Missing POST body") - end - | _ -> lwt_fail (`Bad_request, "Unsupported method")) - >?= (fun req -> - log conn req; - Api_server.handler (Conduit_lwt_unix.endp_of_flow (fst conn)) config req) - >>= function - | Error (code,body) -> - Server.respond_error ~status:code ~body () - | Ok response -> respond response + | None -> lwt_fail (`Bad_request, "Missing POST body") ) + | _ -> lwt_fail (`Bad_request, "Unsupported method") ) + >?= (fun req -> + log conn req; + Api_server.handler + (Conduit_lwt_unix.endp_of_flow (fst conn)) + config req ) + >>= function + | Error (code, body) -> Server.respond_error ~status:code ~body () + | Ok response -> respond response in let mode = match !cert_key_files with - | None -> (`TCP (`Port !port)) + | None -> `TCP (`Port !port) | Some (crt, key) -> `TLS (`Crt_file_path crt, `Key_file_path key, `No_password, `Port !port) in - init_teacher_token () >>= fun () -> + init_teacher_token () + >>= fun () -> Lwt.catch (fun () -> Server.create - ~on_exn: (function - | Unix.Unix_error(Unix.EPIPE, "write", "") -> () - | exn -> raise exn) - ~mode (Server.make ~callback ()) >>= fun () -> - Lwt.return true) + ~on_exn:(function + | Unix.Unix_error (Unix.EPIPE, "write", "") -> () | exn -> raise exn) + ~mode (Server.make ~callback ()) + >>= fun () -> Lwt.return true ) @@ function - | Sys.Break -> - Lwt.return true + | Sys.Break -> Lwt.return true | Unix.Unix_error (Unix.EADDRINUSE, _, _) -> Printf.eprintf "Could not bind port %d, another instance may still be running?\n%!" diff --git a/src/server/learnocaml_server.mli b/src/server/learnocaml_server.mli index a1b79c7c0..ac254c09a 100644 --- a/src/server/learnocaml_server.mli +++ b/src/server/learnocaml_server.mli @@ -8,13 +8,15 @@ (** Configuration options *) -val port: int ref -val cert_key_files: (string * string) option ref -val base_url: string ref +val port : int ref -val args: (Arg.key * Arg.spec * Arg.doc) list +val cert_key_files : (string * string) option ref + +val base_url : string ref + +val args : (Arg.key * Arg.spec * Arg.doc) list (** Main *) (* Returns [false] if interrupted prematurely due to an error *) -val launch: unit -> bool Lwt.t +val launch : unit -> bool Lwt.t diff --git a/src/state/learnocaml_api.ml b/src/state/learnocaml_api.ml index 63043e4c3..fd85cb4bd 100644 --- a/src/state/learnocaml_api.ml +++ b/src/state/learnocaml_api.ml @@ -11,184 +11,130 @@ open Learnocaml_data let version = Learnocaml_version.v type _ request = - | Static: - string list -> string request - | Version: - unit -> (string * int) request - | Nonce: - unit -> string request - | Create_token: - string * student token option * string option -> student token request - | Create_teacher_token: - teacher token -> teacher token request - | Fetch_save: - 'a token -> Save.t request - | Archive_zip: - 'a token -> string request - | Update_save: - 'a token * Save.t -> Save.t request - | Git: 'a token * string list -> string request - - | Students_list: - teacher token -> Student.t list request - | Set_students_list: - teacher token * (Student.t * Student.t) list -> unit request - | Students_csv: - teacher token * Exercise.id list * Token.t list -> string request - - | Exercise_index: - 'a token option -> (Exercise.Index.t * (Exercise.id * float) list) request - | Exercise: - 'a token option * string -> (Exercise.Meta.t * Exercise.t * float option) request - - | Lesson_index: - unit -> (string * string) list request - | Lesson: - string -> Lesson.t request - - | Tutorial_index: - unit -> Tutorial.Index.t request - | Tutorial: - string -> Tutorial.t request - - | Playground_index: - unit -> Playground.Index.t request - | Playground: - string -> Playground.t request - - | Exercise_status_index: - teacher token -> Exercise.Status.t list request - | Exercise_status: - teacher token * Exercise.id -> Exercise.Status.t request - | Set_exercise_status: - teacher token * (Exercise.Status.t * Exercise.Status.t) list -> unit request - - | Partition: - teacher token * Exercise.id * string * int -> Partition.t request - - | Invalid_request: - string -> string request - - -type http_request = { - meth: [ `GET | `POST of string]; - path: string list; - args: (string * string) list; -} + | Static : string list -> string request + | Version : unit -> (string * int) request + | Nonce : unit -> string request + | Create_token : + string * student token option * string option + -> student token request + | Create_teacher_token : teacher token -> teacher token request + | Fetch_save : 'a token -> Save.t request + | Archive_zip : 'a token -> string request + | Update_save : 'a token * Save.t -> Save.t request + | Git : 'a token * string list -> string request + | Students_list : teacher token -> Student.t list request + | Set_students_list : + teacher token * (Student.t * Student.t) list + -> unit request + | Students_csv : + teacher token * Exercise.id list * Token.t list + -> string request + | Exercise_index : + 'a token option + -> (Exercise.Index.t * (Exercise.id * float) list) request + | Exercise : + 'a token option * string + -> (Exercise.Meta.t * Exercise.t * float option) request + | Lesson_index : unit -> (string * string) list request + | Lesson : string -> Lesson.t request + | Tutorial_index : unit -> Tutorial.Index.t request + | Tutorial : string -> Tutorial.t request + | Playground_index : unit -> Playground.Index.t request + | Playground : string -> Playground.t request + | Exercise_status_index : teacher token -> Exercise.Status.t list request + | Exercise_status : teacher token * Exercise.id -> Exercise.Status.t request + | Set_exercise_status : + teacher token * (Exercise.Status.t * Exercise.Status.t) list + -> unit request + | Partition : + teacher token * Exercise.id * string * int + -> Partition.t request + | Invalid_request : string -> string request + +type http_request = + { meth : [`GET | `POST of string] + ; path : string list + ; args : (string * string) list } module J = Json_encoding module type JSON_CODEC = sig - val decode: 'a J.encoding -> string -> 'a - val encode: ?minify:bool -> 'a J.encoding -> 'a -> string -end - -module Conversions (Json: JSON_CODEC) = struct - - let response_codec - : type resp. - resp request -> (resp -> string) * (string -> resp) - = fun req -> - let str = (fun x -> x), (fun x -> x) in - let json enc = (Json.encode enc), (Json.decode enc) in - let ( +> ) (cod, decod) (cod', decod') = - (fun x -> cod (cod' x)), - (fun s -> decod' (decod s)) - in - match req with - | Static _ -> str - | Version _ -> json J.(obj2 (req "version" string) (req "server_id" int)) - | Nonce _ -> json J.(obj1 (req "nonce" string)) - | Create_token _ -> - json J.(obj1 (req "token" string)) +> - Token.(to_string, parse) - | Create_teacher_token _ -> - json J.(obj1 (req "token" string)) +> - Token.(to_string, parse) - | Fetch_save _ -> - json Save.enc - | Archive_zip _ -> - str - | Update_save _ -> - json Save.enc - | Git _ -> str - | Students_list _ -> - json (J.list Student.enc) - | Set_students_list _ -> - json J.unit - | Students_csv _ -> - str - | Exercise_index _ -> - json (J.tup2 Exercise.Index.enc (J.assoc J.float)) - | Exercise _ -> - json (J.tup3 Exercise.Meta.enc Exercise.enc (J.option J.float)) - | Lesson_index _ -> - json Lesson.Index.enc - | Lesson _ -> - json Lesson.enc - | Tutorial_index _ -> - json Tutorial.Index.enc - | Tutorial _ -> - json Tutorial.enc - | Playground_index _ -> - json Playground.Index.enc - | Playground _ -> - json Playground.enc + val decode : 'a J.encoding -> string -> 'a - | Exercise_status_index _ -> - json (J.list Exercise.Status.enc) - | Exercise_status _ -> - json Exercise.Status.enc - | Set_exercise_status _ -> - json J.unit - - | Partition _ -> - json Partition.enc + val encode : ?minify:bool -> 'a J.encoding -> 'a -> string +end - | Invalid_request _ -> - str +module Conversions (Json : JSON_CODEC) = struct + let response_codec : type resp. + resp request -> (resp -> string) * (string -> resp) = + fun req -> + let str = ((fun x -> x), fun x -> x) in + let json enc = (Json.encode enc, Json.decode enc) in + let ( +> ) (cod, decod) (cod', decod') = + ((fun x -> cod (cod' x)), fun s -> decod' (decod s)) + in + match req with + | Static _ -> str + | Version _ -> json J.(obj2 (req "version" string) (req "server_id" int)) + | Nonce _ -> json J.(obj1 (req "nonce" string)) + | Create_token _ -> + json J.(obj1 (req "token" string)) +> Token.(to_string, parse) + | Create_teacher_token _ -> + json J.(obj1 (req "token" string)) +> Token.(to_string, parse) + | Fetch_save _ -> json Save.enc + | Archive_zip _ -> str + | Update_save _ -> json Save.enc + | Git _ -> str + | Students_list _ -> json (J.list Student.enc) + | Set_students_list _ -> json J.unit + | Students_csv _ -> str + | Exercise_index _ -> json (J.tup2 Exercise.Index.enc (J.assoc J.float)) + | Exercise _ -> + json (J.tup3 Exercise.Meta.enc Exercise.enc (J.option J.float)) + | Lesson_index _ -> json Lesson.Index.enc + | Lesson _ -> json Lesson.enc + | Tutorial_index _ -> json Tutorial.Index.enc + | Tutorial _ -> json Tutorial.enc + | Playground_index _ -> json Playground.Index.enc + | Playground _ -> json Playground.enc + | Exercise_status_index _ -> json (J.list Exercise.Status.enc) + | Exercise_status _ -> json Exercise.Status.enc + | Set_exercise_status _ -> json J.unit + | Partition _ -> json Partition.enc + | Invalid_request _ -> str let response_encode r = fst (response_codec r) - let response_decode r = snd (response_codec r) + let response_decode r = snd (response_codec r) - let to_http_request - : type resp. resp request -> http_request - = - let get ?token path = { - meth = `GET; - path; - args = match token with None -> [] | Some t -> ["token", Token.to_string t]; - } in - let post ~token path body = { - meth = `POST body; - path; - args = ["token", Token.to_string token]; - } in + let to_http_request : type resp. resp request -> http_request = + let get ?token path = + { meth = `GET + ; path + ; args = + ( match token with + | None -> [] + | Some t -> [("token", Token.to_string t)] ) } + in + let post ~token path body = + {meth = `POST body; path; args = [("token", Token.to_string token)]} + in function - | Static path -> - get path - | Version () -> - get ["version"] - - | Nonce () -> - get ["nonce"] + | Static path -> get path + | Version () -> get ["version"] + | Nonce () -> get ["nonce"] | Create_token (secret_candiate, token, nick) -> - get ?token (["sync"; "new"; secret_candiate] @ - (match nick with None -> [] | Some n -> [n])) + get ?token + ( ["sync"; "new"; secret_candiate] + @ match nick with None -> [] | Some n -> [n] ) | Create_teacher_token token -> assert (Token.is_teacher token); get ~token ["teacher"; "new"] - - | Fetch_save token -> - get ~token ["save.json"] - | Archive_zip token -> - get ~token ["archive.zip"] + | Fetch_save token -> get ~token ["save.json"] + | Archive_zip token -> get ~token ["archive.zip"] | Update_save (token, save) -> post ~token ["sync"] (Json.encode Save.enc save) - | Git _ -> - assert false (* Reserved for the [git] client *) - + | Git _ -> assert false (* Reserved for the [git] client *) | Students_list token -> assert (Token.is_teacher token); get ~token ["teacher"; "students.json"] @@ -199,38 +145,25 @@ module Conversions (Json: JSON_CODEC) = struct (Json.encode (J.list (J.tup2 Student.enc Student.enc)) students) | Students_csv (token, exercises, students) -> assert (Token.is_teacher token); - post ~token ["teacher"; "students.csv"] + post ~token + ["teacher"; "students.csv"] (Json.encode (J.obj2 (J.dft "exercises" (J.list J.string) []) (J.dft "students" (J.list Token.enc) [])) (exercises, students)) - - | Exercise_index (Some token) -> - get ~token ["exercise-index.json"] - | Exercise_index None -> - get ["exercise-index.json"] - + | Exercise_index (Some token) -> get ~token ["exercise-index.json"] + | Exercise_index None -> get ["exercise-index.json"] | Exercise (Some token, id) -> - get ~token ("exercises" :: String.split_on_char '/' (id^".json")) + get ~token ("exercises" :: String.split_on_char '/' (id ^ ".json")) | Exercise (None, id) -> - get ("exercises" :: String.split_on_char '/' (id^".json")) - - | Lesson_index () -> - get ["lessons.json"] - | Lesson id -> - get ["lessons"; id^".json"] - - | Playground_index () -> - get ["playgrounds.json"] - | Playground id -> - get ["playgrounds"; id^".json"] - - | Tutorial_index () -> - get ["tutorials.json"] - | Tutorial id -> - get ["tutorials"; id^".json"] - + get ("exercises" :: String.split_on_char '/' (id ^ ".json")) + | Lesson_index () -> get ["lessons.json"] + | Lesson id -> get ["lessons"; id ^ ".json"] + | Playground_index () -> get ["playgrounds.json"] + | Playground id -> get ["playgrounds"; id ^ ".json"] + | Tutorial_index () -> get ["tutorials.json"] + | Tutorial id -> get ["tutorials"; id ^ ".json"] | Exercise_status_index token -> assert (Token.is_teacher token); get ~token ["teacher"; "exercise-status.json"] @@ -243,199 +176,164 @@ module Conversions (Json: JSON_CODEC) = struct (Json.encode (J.list (J.tup2 Exercise.Status.enc Exercise.Status.enc)) status) - | Partition (token, eid, fid, prof) -> - get ~token - ["partition"; eid; fid; string_of_int prof] - - | Invalid_request s -> - failwith ("Error request "^s) - + get ~token ["partition"; eid; fid; string_of_int prof] + | Invalid_request s -> failwith ("Error request " ^ s) end module type REQUEST_HANDLER = sig type 'resp ret - val map_ret: ('a -> 'b) -> 'a ret -> 'b ret - val callback: Conduit.endp -> - Learnocaml_data.Server.config -> 'resp request -> 'resp ret -end + val map_ret : ('a -> 'b) -> 'a ret -> 'b ret -module Server (Json: JSON_CODEC) (Rh: REQUEST_HANDLER) = struct + val callback : + Conduit.endp -> Learnocaml_data.Server.config -> 'resp request -> 'resp ret +end - module C = Conversions(Json) +module Server (Json : JSON_CODEC) (Rh : REQUEST_HANDLER) = struct + module C = Conversions (Json) - let rec last = - function [f] -> Some f | [] -> None | _::r -> last r + let rec last = function [f] -> Some f | [] -> None | _ :: r -> last r let handler conn config request = - let k req = - Rh.callback conn config req |> Rh.map_ret (C.response_encode req) - in - let token = - match List.assoc_opt "token" request.args with - | None -> None - | Some stoken -> - try Some (Token.parse stoken) - with Failure _ -> None - in - match request.meth, request.path, token with - | `GET, ([] | [""]), _ -> - Static ["index.html"] |> k - | `GET, ["version"], _ -> - Version () |> k - - | `GET, ["nonce"], _ -> - Nonce () |> k - | `GET, ["sync"; "new"; secret_candidate], token -> - Create_token (secret_candidate, token, None) |> k - | `GET, ["sync"; "new"; secret_candidate; nick], token -> - Create_token (secret_candidate, token, Some nick) |> k - | `GET, ["teacher"; "new"], Some token when Token.is_teacher token -> - Create_teacher_token token |> k - - | `GET, ["save.json"], Some token -> - Fetch_save token |> k - | `GET, ["archive.zip"], Some token -> - Archive_zip token |> k - | `POST body, ["sync"], Some token -> - (match Json.decode Save.enc body with - | save -> Update_save (token, save) |> k - | exception e -> Invalid_request (Printexc.to_string e) |> k) - | `GET, (stoken::"learnocaml-workspace.git"::p), None -> - (match Token.parse stoken with - | token -> Git (token, p) |> k - | exception Failure e -> Invalid_request e |> k) - - | `GET, ["teacher"; "students.json"], Some token - when Token.is_teacher token -> - Students_list token |> k - | `POST body, ["teacher"; "students.json"], Some token - when Token.is_teacher token -> - (match Json.decode (J.list (J.tup2 Student.enc Student.enc)) body with - | students -> Set_students_list (token, students) |> k - | exception e -> Invalid_request (Printexc.to_string e) |> k) - | `GET, ["teacher"; "students.csv"], Some token - when Token.is_teacher token -> - Students_csv (token, [], []) |> k - | `POST body, ["teacher"; "students.csv"], Some token - when Token.is_teacher token -> - (match Json.decode - (J.obj2 - (J.dft "exercises" (J.list J.string) []) - (J.dft "students" (J.list Token.enc) [])) - body - with - | exercises, students -> - Students_csv (token, exercises, students) |> k - | exception e -> Invalid_request (Printexc.to_string e) |> k) - - | `GET, ["exercise-index.json"], token -> - Exercise_index token |> k - | `GET, ("exercises"::path), token -> - (match last path with - | Some s when String.lowercase_ascii (Filename.extension s) = ".json" -> - (match token with - | Some token -> - let id = Filename.chop_suffix (String.concat "/" path) ".json" in - Exercise (Some token, id) |> k - | None -> Invalid_request "Missing token" |> k) - | Some "" -> - Static ["exercise.html"] |> k - | _ -> - Static ("static"::path) |> k) - | `GET, ("description"::_), _token -> - (* match token with + let k req = + Rh.callback conn config req |> Rh.map_ret (C.response_encode req) + in + let token = + match List.assoc_opt "token" request.args with + | None -> None + | Some stoken -> ( + try Some (Token.parse stoken) with Failure _ -> None ) + in + match (request.meth, request.path, token) with + | `GET, ([] | [""]), _ -> Static ["index.html"] |> k + | `GET, ["version"], _ -> Version () |> k + | `GET, ["nonce"], _ -> Nonce () |> k + | `GET, ["sync"; "new"; secret_candidate], token -> + Create_token (secret_candidate, token, None) |> k + | `GET, ["sync"; "new"; secret_candidate; nick], token -> + Create_token (secret_candidate, token, Some nick) |> k + | `GET, ["teacher"; "new"], Some token when Token.is_teacher token -> + Create_teacher_token token |> k + | `GET, ["save.json"], Some token -> Fetch_save token |> k + | `GET, ["archive.zip"], Some token -> Archive_zip token |> k + | `POST body, ["sync"], Some token -> ( + match Json.decode Save.enc body with + | save -> Update_save (token, save) |> k + | exception e -> Invalid_request (Printexc.to_string e) |> k ) + | `GET, stoken :: "learnocaml-workspace.git" :: p, None -> ( + match Token.parse stoken with + | token -> Git (token, p) |> k + | exception Failure e -> Invalid_request e |> k ) + | `GET, ["teacher"; "students.json"], Some token + when Token.is_teacher token -> + Students_list token |> k + | `POST body, ["teacher"; "students.json"], Some token + when Token.is_teacher token -> ( + match Json.decode (J.list (J.tup2 Student.enc Student.enc)) body with + | students -> Set_students_list (token, students) |> k + | exception e -> Invalid_request (Printexc.to_string e) |> k ) + | `GET, ["teacher"; "students.csv"], Some token when Token.is_teacher token + -> + Students_csv (token, [], []) |> k + | `POST body, ["teacher"; "students.csv"], Some token + when Token.is_teacher token -> ( + match + Json.decode + (J.obj2 + (J.dft "exercises" (J.list J.string) []) + (J.dft "students" (J.list Token.enc) [])) + body + with + | exercises, students -> Students_csv (token, exercises, students) |> k + | exception e -> Invalid_request (Printexc.to_string e) |> k ) + | `GET, ["exercise-index.json"], token -> Exercise_index token |> k + | `GET, "exercises" :: path, token -> ( + match last path with + | Some s when String.lowercase_ascii (Filename.extension s) = ".json" -> ( + match token with + | Some token -> + let id = Filename.chop_suffix (String.concat "/" path) ".json" in + Exercise (Some token, id) |> k + | None -> Invalid_request "Missing token" |> k ) + | Some "" -> Static ["exercise.html"] |> k + | _ -> Static ("static" :: path) |> k ) + | `GET, "description" :: _, _token -> + (* match token with | None -> Invalid_request "Missing token" |> k *) - Static ["description.html"] |> k - | `GET, ("playground"::path), _token -> - begin - match last path with - | Some s when String.lowercase_ascii (Filename.extension s) = ".json" -> - let id = Filename.chop_suffix (String.concat "/" path) ".json" in - Playground id |> k - | Some "" -> - Static ["playground.html"] |> k - | _ -> - Static ("static"::path) |> k - end - | `GET, ["lessons.json"], _ -> - Lesson_index () |> k - | `GET, ["lessons"; f], _ when Filename.check_suffix f ".json" -> - Lesson (Filename.chop_suffix f ".json") |> k - - | `GET, ["tutorials.json"], _ -> - Tutorial_index () |> k - | `GET, ["tutorials"; f], _ when Filename.check_suffix f ".json" -> - Tutorial (Filename.chop_suffix f ".json") |> k - - | `GET, ["playgrounds.json"], _ -> - Playground_index () |> k - | `GET, ["playgrounds"; f], _ when Filename.check_suffix f ".json" -> - Playground (Filename.chop_suffix f ".json") |> k - - | `GET, ["partition"; eid; fid; prof], Some token - when Token.is_teacher token -> - Partition (token, eid, fid, int_of_string prof) |> k - - | `GET, ["teacher"; "exercise-status.json"], Some token - when Token.is_teacher token -> - Exercise_status_index token |> k - | `GET, ("teacher" :: "exercise-status" :: id), Some token - when Token.is_teacher token -> - Exercise_status (token, String.concat "/" id) |> k - | `POST body, ["teacher"; "exercise-status"], Some token - when Token.is_teacher token -> - (match Json.decode - (J.list (J.tup2 Exercise.Status.enc Exercise.Status.enc)) - body - with - | status -> - Set_exercise_status (token, status) |> k - | exception e -> Invalid_request (Printexc.to_string e) |> k) - - | `GET, - ( ["index.html"] + Static ["description.html"] |> k + | `GET, "playground" :: path, _token -> ( + match last path with + | Some s when String.lowercase_ascii (Filename.extension s) = ".json" -> + let id = Filename.chop_suffix (String.concat "/" path) ".json" in + Playground id |> k + | Some "" -> Static ["playground.html"] |> k + | _ -> Static ("static" :: path) |> k ) + | `GET, ["lessons.json"], _ -> Lesson_index () |> k + | `GET, ["lessons"; f], _ when Filename.check_suffix f ".json" -> + Lesson (Filename.chop_suffix f ".json") |> k + | `GET, ["tutorials.json"], _ -> Tutorial_index () |> k + | `GET, ["tutorials"; f], _ when Filename.check_suffix f ".json" -> + Tutorial (Filename.chop_suffix f ".json") |> k + | `GET, ["playgrounds.json"], _ -> Playground_index () |> k + | `GET, ["playgrounds"; f], _ when Filename.check_suffix f ".json" -> + Playground (Filename.chop_suffix f ".json") |> k + | `GET, ["partition"; eid; fid; prof], Some token + when Token.is_teacher token -> + Partition (token, eid, fid, int_of_string prof) |> k + | `GET, ["teacher"; "exercise-status.json"], Some token + when Token.is_teacher token -> + Exercise_status_index token |> k + | `GET, "teacher" :: "exercise-status" :: id, Some token + when Token.is_teacher token -> + Exercise_status (token, String.concat "/" id) |> k + | `POST body, ["teacher"; "exercise-status"], Some token + when Token.is_teacher token -> ( + match + Json.decode + (J.list (J.tup2 Exercise.Status.enc Exercise.Status.enc)) + body + with + | status -> Set_exercise_status (token, status) |> k + | exception e -> Invalid_request (Printexc.to_string e) |> k ) + | ( `GET + , ( ( ["index.html"] | ["exercise.html"] - | ["playground.html"] - | ["student-view.html"] - | ["description.html"] - | ["partition-view.html"] - | ("js"|"fonts"|"icons"|"css"|"static") :: _ as path), - _ -> - Static path |> k - - | `GET, ["favicon.ico"], _ -> - Static ["icons"; "favicon.ico"] |> k - - | meth, path, _ -> - Invalid_request - (Printf.sprintf "%s /%s%s" - (match meth with `GET -> "GET" | `POST _ -> "POST") - (String.concat "/" path) - (match request.args with [] -> "" | l -> - "?" ^ String.concat "&" - (List.map (fun (k, v) -> k ^"="^ v) l))) - |> k - + | ["playground.html"] + | ["student-view.html"] + | ["description.html"] + | ["partition-view.html"] + | ("js" | "fonts" | "icons" | "css" | "static") :: _ ) as path ) + , _ ) -> + Static path |> k + | `GET, ["favicon.ico"], _ -> Static ["icons"; "favicon.ico"] |> k + | meth, path, _ -> + Invalid_request + (Printf.sprintf "%s /%s%s" + (match meth with `GET -> "GET" | `POST _ -> "POST") + (String.concat "/" path) + ( match request.args with + | [] -> "" + | l -> + "?" + ^ String.concat "&" (List.map (fun (k, v) -> k ^ "=" ^ v) l) + )) + |> k end -module Client (Json: JSON_CODEC) = struct - +module Client (Json : JSON_CODEC) = struct open Lwt.Infix - - module C = Conversions(Json) - - let make_request - : type resp. - (http_request -> (string, 'b) result Lwt.t) -> - resp request -> (resp, 'b) result Lwt.t - = fun send req -> - let http_request = C.to_http_request req in - send http_request >|= function - | Ok str -> Ok (C.response_decode req str) - | Error e -> Error e - + module C = Conversions (Json) + + let make_request : type resp. + (http_request -> (string, 'b) result Lwt.t) + -> resp request + -> (resp, 'b) result Lwt.t = + fun send req -> + let http_request = C.to_http_request req in + send http_request + >|= function + | Ok str -> Ok (C.response_decode req str) | Error e -> Error e end (* diff --git a/src/state/learnocaml_api.mli b/src/state/learnocaml_api.mli index b51db4eb2..5f3139553 100644 --- a/src/state/learnocaml_api.mli +++ b/src/state/learnocaml_api.mli @@ -21,110 +21,90 @@ open Learnocaml_data -val version: string +val version : string type _ request = - | Static: - string list -> string request - | Version: - unit -> (string * int) request - | Nonce: - unit -> string request - | Create_token: - string * student token option * string option -> student token request - | Create_teacher_token: - teacher token -> teacher token request - | Fetch_save: - 'a token -> Save.t request - | Archive_zip: - 'a token -> string request - | Update_save: - 'a token * Save.t -> Save.t request - | Git: - 'a token * string list -> string request - - | Students_list: - teacher token -> Student.t list request - | Set_students_list: - teacher token * (Student.t * Student.t) list -> unit request - (** Does not affect the students absent from the list. the pairs are the + | Static : string list -> string request + | Version : unit -> (string * int) request + | Nonce : unit -> string request + | Create_token : + string * student token option * string option + -> student token request + | Create_teacher_token : teacher token -> teacher token request + | Fetch_save : 'a token -> Save.t request + | Archive_zip : 'a token -> string request + | Update_save : 'a token * Save.t -> Save.t request + | Git : 'a token * string list -> string request + | Students_list : teacher token -> Student.t list request + | Set_students_list : + teacher token * (Student.t * Student.t) list + -> unit request + (** Does not affect the students absent from the list. the pairs are the before/after states, used for merging *) - | Students_csv: - teacher token * Exercise.id list * Token.t list -> string request - - | Exercise_index: - 'a token option -> (Exercise.Index.t * (Exercise.id * float) list) request - | Exercise: - 'a token option * string -> (Exercise.Meta.t * Exercise.t * float option) request - - | Lesson_index: - unit -> (string * string) list request - | Lesson: - string -> Lesson.t request - - | Tutorial_index: - unit -> Tutorial.Index.t request - | Tutorial: - string -> Tutorial.t request - - | Playground_index: - unit -> Playground.Index.t request - | Playground: - string -> Playground.t request - - | Exercise_status_index: - teacher token -> Exercise.Status.t list request - | Exercise_status: - teacher token * Exercise.id -> Exercise.Status.t request - | Set_exercise_status: - teacher token * (Exercise.Status.t * Exercise.Status.t) list -> - unit request - (** The two Status.t correspond to the states before and after changes, used + | Students_csv : + teacher token * Exercise.id list * Token.t list + -> string request + | Exercise_index : + 'a token option + -> (Exercise.Index.t * (Exercise.id * float) list) request + | Exercise : + 'a token option * string + -> (Exercise.Meta.t * Exercise.t * float option) request + | Lesson_index : unit -> (string * string) list request + | Lesson : string -> Lesson.t request + | Tutorial_index : unit -> Tutorial.Index.t request + | Tutorial : string -> Tutorial.t request + | Playground_index : unit -> Playground.Index.t request + | Playground : string -> Playground.t request + | Exercise_status_index : teacher token -> Exercise.Status.t list request + | Exercise_status : teacher token * Exercise.id -> Exercise.Status.t request + | Set_exercise_status : + teacher token * (Exercise.Status.t * Exercise.Status.t) list + -> unit request + (** The two Status.t correspond to the states before and after changes, used for three-way merge *) - - | Partition: - teacher token * Exercise.id * string * int -> Partition.t request - - | Invalid_request: - string -> string request - (** Only for server-side handling: bound to requests not matching any case + | Partition : + teacher token * Exercise.id * string * int + -> Partition.t request + | Invalid_request : string -> string request + (** Only for server-side handling: bound to requests not matching any case above *) -type http_request = { - meth: [ `GET | `POST of string]; - path: string list; - args: (string * string) list; -} +type http_request = + { meth : [`GET | `POST of string] + ; path : string list + ; args : (string * string) list } module type JSON_CODEC = sig - val decode: 'a Json_encoding.encoding -> string -> 'a - val encode: ?minify:bool -> 'a Json_encoding.encoding -> 'a -> string + val decode : 'a Json_encoding.encoding -> string -> 'a + + val encode : ?minify:bool -> 'a Json_encoding.encoding -> 'a -> string end module type REQUEST_HANDLER = sig type 'resp ret - val map_ret: ('a -> 'b) -> 'a ret -> 'b ret + val map_ret : ('a -> 'b) -> 'a ret -> 'b ret - val callback: Conduit.endp -> - Learnocaml_data.Server.config -> 'resp request -> 'resp ret + val callback : + Conduit.endp -> Learnocaml_data.Server.config -> 'resp request -> 'resp ret end -module Server: functor (Json: JSON_CODEC) (Rh: REQUEST_HANDLER) -> sig - +module Server (Json : JSON_CODEC) (Rh : REQUEST_HANDLER) : sig + val handler : + Conduit.endp + -> Learnocaml_data.Server.config + -> http_request + -> string Rh.ret (** Helper to define a server: handles recognition of the incoming request, and encoding of the response. *) - val handler: Conduit.endp -> - Learnocaml_data.Server.config -> http_request -> string Rh.ret - end -module Client: functor (Json: JSON_CODEC) -> sig - +module Client (Json : JSON_CODEC) : sig + val make_request : + (http_request -> (string, 'a) result Lwt.t) + -> 'resp request + -> ('resp, 'a) result Lwt.t (** Helper to make a client request: handles encoding of the request and decoding of the response. *) - val make_request: - (http_request -> (string, 'a) result Lwt.t) -> - 'resp request -> ('resp, 'a) result Lwt.t - end diff --git a/src/state/learnocaml_data.ml b/src/state/learnocaml_data.ml index 39ce6f6d0..d93f14f2b 100644 --- a/src/state/learnocaml_data.ml +++ b/src/state/learnocaml_data.ml @@ -9,342 +9,309 @@ module J = Json_encoding module SMap = struct - include Map.Make (String) let enc val_enc = - J.conv - bindings - (List.fold_left - (fun acc (n, v) -> add n v acc) - empty) + J.conv bindings + (List.fold_left (fun acc (n, v) -> add n v acc) empty) (J.assoc val_enc) - end module SSet = struct - include Set.Make (String) let enc = J.conv elements of_list (J.list J.string) let merge3 ~ancestor ~theirs ~ours = - let (++), (--), (%%) = union, diff, inter in + let ( ++ ), ( -- ), ( %% ) = (union, diff, inter) in (ancestor %% theirs %% ours) ++ (theirs -- ancestor) ++ (ours -- ancestor) - end module Answer = struct - type t = - { solution : string ; - grade : int (* \in [0, 100] *) option ; - report : Learnocaml_report.t option ; - mtime : float } + { solution : string + ; grade : int (* \in [0, 100] *) option + ; report : Learnocaml_report.t option + ; mtime : float } let enc = let grade_enc = J.conv + (function Some n when n < 0 || n > 100 -> None | g -> g) (function - | Some n when n < 0 || n > 100 -> None - | g -> g) - (function - | Some s when s < 0 || s > 100 -> failwith "grade overflow" - | g -> g) - J.(option int) in + | Some s when s < 0 || s > 100 -> failwith "grade overflow" | g -> g) + J.(option int) + in J.conv - (fun { grade ; solution ; report ; mtime } -> - (grade, solution, report, mtime)) - (fun (grade, solution, report, mtime) -> - { grade ; solution ; report ; mtime }) + (fun {grade; solution; report; mtime} -> (grade, solution, report, mtime)) + (fun (grade, solution, report, mtime) -> {grade; solution; report; mtime}) (J.obj4 (J.dft "grade" grade_enc None) (J.req "solution" J.string) (J.opt "report" Learnocaml_report.enc) (J.dft "mtime" J.float 0.)) - end module Report = Learnocaml_report module Save = struct - type t = - { nickname : string ; - all_exercise_editors : (float * string) SMap.t ; - all_exercise_states : Answer.t SMap.t ; - all_toplevel_histories : - Learnocaml_toplevel_history.snapshot SMap.t ; - all_exercise_toplevel_histories : + { nickname : string + ; all_exercise_editors : (float * string) SMap.t + ; all_exercise_states : Answer.t SMap.t + ; all_toplevel_histories : Learnocaml_toplevel_history.snapshot SMap.t + ; all_exercise_toplevel_histories : Learnocaml_toplevel_history.snapshot SMap.t } let enc = J.conv (fun t -> - t.nickname, - t.all_exercise_editors, - t.all_exercise_states, - t.all_toplevel_histories, - t.all_exercise_toplevel_histories) - (fun (nickname, - all_exercise_editors, - all_exercise_states, - all_toplevel_histories, - all_exercise_toplevel_histories) -> - { nickname ; - all_exercise_editors ; - all_exercise_states ; - all_toplevel_histories ; - all_exercise_toplevel_histories }) @@ - J.obj5 - (J.dft "nickname" J.string "") - (J.dft "exercises-editors" - (SMap.enc (J.tup2 J.float J.string)) SMap.empty) - (J.dft "exercises" - (SMap.enc Answer.enc) SMap.empty) - (J.dft "toplevel-histories" - (SMap.enc Learnocaml_toplevel_history.snapshot_enc) SMap.empty) - (J.dft "exercise-toplevel-histories" - (SMap.enc Learnocaml_toplevel_history.snapshot_enc) SMap.empty) + ( t.nickname + , t.all_exercise_editors + , t.all_exercise_states + , t.all_toplevel_histories + , t.all_exercise_toplevel_histories ) ) + (fun ( nickname + , all_exercise_editors + , all_exercise_states + , all_toplevel_histories + , all_exercise_toplevel_histories ) -> + { nickname + ; all_exercise_editors + ; all_exercise_states + ; all_toplevel_histories + ; all_exercise_toplevel_histories } ) + @@ J.obj5 + (J.dft "nickname" J.string "") + (J.dft "exercises-editors" + (SMap.enc (J.tup2 J.float J.string)) + SMap.empty) + (J.dft "exercises" (SMap.enc Answer.enc) SMap.empty) + (J.dft "toplevel-histories" + (SMap.enc Learnocaml_toplevel_history.snapshot_enc) + SMap.empty) + (J.dft "exercise-toplevel-histories" + (SMap.enc Learnocaml_toplevel_history.snapshot_enc) + SMap.empty) let sync a b = let sync_snapshot snapshot_a snapshot_b = let open Learnocaml_toplevel_history in - if snapshot_a.mtime > snapshot_b.mtime then - snapshot_a - else - snapshot_b in - let sync_exercise_edits (ts_a, _ as a) (ts_b, _ as b) = + if snapshot_a.mtime > snapshot_b.mtime then snapshot_a else snapshot_b + in + let sync_exercise_edits ((ts_a, _) as a) ((ts_b, _) as b) = if ts_a > ts_b then a else b in let sync_exercise_state state_a state_b = let open Answer in - if state_a.mtime > state_b.mtime then - state_a - else - state_b in + if state_a.mtime > state_b.mtime then state_a else state_b + in let sync_map sync_item index_a index_b = SMap.merge - (fun _id a b -> match a, b with - | None, None -> assert false - | None, Some i | Some i, None -> Some i - | Some a, Some b -> Some (sync_item a b)) - index_a index_b in + (fun _id a b -> + match (a, b) with + | None, None -> assert false + | None, Some i | Some i, None -> Some i + | Some a, Some b -> Some (sync_item a b) ) + index_a index_b + in let all_exercise_states = - sync_map sync_exercise_state - a.all_exercise_states - b.all_exercise_states + sync_map sync_exercise_state a.all_exercise_states b.all_exercise_states in let all_exercise_editors = - sync_map sync_exercise_edits - a.all_exercise_editors + sync_map sync_exercise_edits a.all_exercise_editors b.all_exercise_editors |> SMap.filter (fun id (ts, _) -> - match SMap.find_opt id all_exercise_states with - | Some {Answer.mtime; _} when mtime > ts -> false - | _ -> true) + match SMap.find_opt id all_exercise_states with + | Some {Answer.mtime; _} when mtime > ts -> false + | _ -> true ) in - { nickname = if b.nickname = "" then a.nickname else b.nickname; - all_exercise_editors; - all_exercise_states; - all_toplevel_histories = - sync_map sync_snapshot - a.all_toplevel_histories - b.all_toplevel_histories ; - all_exercise_toplevel_histories = - sync_map sync_snapshot - a.all_exercise_toplevel_histories + { nickname = (if b.nickname = "" then a.nickname else b.nickname) + ; all_exercise_editors + ; all_exercise_states + ; all_toplevel_histories = + sync_map sync_snapshot a.all_toplevel_histories + b.all_toplevel_histories + ; all_exercise_toplevel_histories = + sync_map sync_snapshot a.all_exercise_toplevel_histories b.all_exercise_toplevel_histories } let fix_mtimes save = let now = Unix.gettimeofday () in let fix t = min t now in let fix_snapshot s = - Learnocaml_toplevel_history.{ s with mtime = fix s.mtime } - in - let fix_exercise_edits (ts, e) = fix ts, e in - let fix_exercise_state s = - Answer.{ s with mtime = fix s.mtime } + Learnocaml_toplevel_history.{s with mtime = fix s.mtime} in - { - save with + let fix_exercise_edits (ts, e) = (fix ts, e) in + let fix_exercise_state s = Answer.{s with mtime = fix s.mtime} in + { save with all_exercise_editors = - SMap.map fix_exercise_edits save.all_exercise_editors; - all_exercise_states = - SMap.map fix_exercise_state save.all_exercise_states; - all_toplevel_histories = - SMap.map fix_snapshot save.all_toplevel_histories; - all_exercise_toplevel_histories = - SMap.map fix_snapshot save.all_exercise_toplevel_histories; - } - - let empty = { - all_exercise_editors = SMap.empty; - all_exercise_states = SMap.empty; - all_toplevel_histories = SMap.empty; - all_exercise_toplevel_histories = SMap.empty; - nickname = ""; - } - + SMap.map fix_exercise_edits save.all_exercise_editors + ; all_exercise_states = + SMap.map fix_exercise_state save.all_exercise_states + ; all_toplevel_histories = + SMap.map fix_snapshot save.all_toplevel_histories + ; all_exercise_toplevel_histories = + SMap.map fix_snapshot save.all_exercise_toplevel_histories } + + let empty = + { all_exercise_editors = SMap.empty + ; all_exercise_states = SMap.empty + ; all_toplevel_histories = SMap.empty + ; all_exercise_toplevel_histories = SMap.empty + ; nickname = "" } end module Token = struct - type t = string list let teacher_token_prefix = "X" let to_string = String.concat "-" - let to_path = String.concat (Filename.dir_sep) + + let to_path = String.concat Filename.dir_sep + let teacher_tokens_path = teacher_token_prefix - let alphabet = - "ABCDEFGH1JKLMNOPORSTUVWXYZO1Z34SG1B9" - let visually_equivalent_alphabet = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + let alphabet = "ABCDEFGH1JKLMNOPORSTUVWXYZO1Z34SG1B9" + + let visually_equivalent_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" let parse = let table = Array.make 256 None in String.iteri - (fun i c -> Array.set table (Char.code c) (Some alphabet.[i])) - visually_equivalent_alphabet ; + (fun i c -> table.(Char.code c) <- Some alphabet.[i]) + visually_equivalent_alphabet; let translate part = - String.map (fun c -> - match Array.get table (Char.code c) with + String.map + (fun c -> + match table.(Char.code c) with | None -> failwith "bad token character" - | Some c -> c) - part in + | Some c -> c ) + part + in fun token -> let token = String.trim token in let translate_base_token token = if String.length token = 15 then - if String.get token 3 <> '-' - || String.get token 7 <> '-' - || String.get token 11 <> '-' then + if token.[3] <> '-' || token.[7] <> '-' || token.[11] <> '-' then failwith "bad token format" else List.map translate - [ String.sub token 0 3 ; - String.sub token 4 3 ; - String.sub token 8 3 ; - String.sub token 12 3 ] - else - failwith "bad token length" + [ String.sub token 0 3 + ; String.sub token 4 3 + ; String.sub token 8 3 + ; String.sub token 12 3 ] + else failwith "bad token length" in - if String.length token >= 2 && - String.sub token 0 2 = teacher_token_prefix ^ "-" + if + String.length token >= 2 + && String.sub token 0 2 = teacher_token_prefix ^ "-" then - teacher_token_prefix :: - translate_base_token (String.sub token 2 (String.length token - 2)) - else - translate_base_token token + teacher_token_prefix + :: translate_base_token (String.sub token 2 (String.length token - 2)) + else translate_base_token token let enc = J.conv to_string parse J.string let check token = - try ignore (parse token) ; true + try + ignore (parse token); + true with _ -> false let random () = - let rand () = String.get alphabet (Random.int (String.length alphabet)) in + let rand () = alphabet.[Random.int (String.length alphabet)] in let part () = String.init 3 (fun _ -> rand ()) in - [ part () ; part () ; part () ; part () ] + [part (); part (); part (); part ()] let random_teacher () = teacher_token_prefix :: random () let is_teacher = function - | x::_ when x = teacher_token_prefix -> true + | x :: _ when x = teacher_token_prefix -> true | _ -> false let is_student t = not (is_teacher t) module T = struct type nonrec t = t + let compare = Pervasives.compare end - module Set = Set.Make(T) - module Map = Map.Make(T) + module Set = Set.Make (T) + module Map = Map.Make (T) end type 'a token = Token.t type student + type teacher module Student = struct - - type t = { - token: student token; - nickname: string option; - results: (float * int option) SMap.t; - creation_date: float; - tags: SSet.t; - } + type t = + { token : student token + ; nickname : string option + ; results : (float * int option) SMap.t + ; creation_date : float + ; tags : SSet.t } let enc = let open Json_encoding in - obj5 - (req "token" string) - (opt "nickname" string) + obj5 (req "token" string) (opt "nickname" string) (dft "results" (assoc (tup2 float (option int))) []) (dft "creation_date" float 0.) (dft "tags" (list string) []) |> conv - (fun t -> - Token.to_string t.token, - t.nickname, SMap.bindings t.results, t.creation_date, - SSet.elements t.tags) - (fun (token, nickname, results, creation_date, tags) -> { - token = Token.parse token; - nickname; - results = - List.fold_left (fun m (s, r) -> SMap.add s r m) - SMap.empty - results; - creation_date; - tags = SSet.of_list tags; - }) - - let default token = { - token; - nickname = None; - results = SMap.empty; - creation_date = Unix.gettimeofday (); - tags = SSet.empty; - } + (fun t -> + ( Token.to_string t.token + , t.nickname + , SMap.bindings t.results + , t.creation_date + , SSet.elements t.tags ) ) + (fun (token, nickname, results, creation_date, tags) -> + { token = Token.parse token + ; nickname + ; results = + List.fold_left + (fun m (s, r) -> SMap.add s r m) + SMap.empty results + ; creation_date + ; tags = SSet.of_list tags } ) + + let default token = + { token + ; nickname = None + ; results = SMap.empty + ; creation_date = Unix.gettimeofday () + ; tags = SSet.empty } let three_way_merge ~ancestor ~theirs ~ours = let token = ancestor.token in if token <> theirs.token || token <> ours.token then invalid_arg "three_way_merge"; - let tags = SSet.merge3 - ~ancestor:ancestor.tags - ~theirs:theirs.tags - ~ours:ours.tags + let tags = + SSet.merge3 ~ancestor:ancestor.tags ~theirs:theirs.tags ~ours:ours.tags in let nickname = - if ours.nickname <> ancestor.nickname - then ours.nickname + if ours.nickname <> ancestor.nickname then ours.nickname else theirs.nickname in let results = - SMap.merge (fun id a o -> - if a <> o then o else - SMap.find_opt id theirs.results) + SMap.merge + (fun id a o -> if a <> o then o else SMap.find_opt id theirs.results) ancestor.results ours.results in let creation_date = min ancestor.creation_date (min theirs.creation_date ours.creation_date) in - { token; tags; nickname; creation_date; results } + {token; tags; nickname; creation_date; results} module Index = struct - type nonrec t = t list let enc = J.(list enc) - end end @@ -352,63 +319,54 @@ let enc_check_version_1 enc = J.conv (fun exercise -> ("1", exercise)) (fun (version, exercise) -> - if version <> "1" then begin - let msg = Format.asprintf "unknown version %s" version in - raise (J.Cannot_destruct ([], Failure msg)) - end ; - exercise) + ( if version <> "1" then + let msg = Format.asprintf "unknown version %s" version in + raise (J.Cannot_destruct ([], Failure msg)) ); + exercise ) (J.merge_objs (J.obj1 (J.req "learnocaml_version" J.string)) enc) let enc_check_version_2 enc = J.conv (fun exercise -> ("2", exercise)) (fun (version, exercise) -> - begin - match version with - | "1" | "2" -> () - | _ -> - let msg = Format.asprintf "unknown version %s" version in - raise (J.Cannot_destruct ([], Failure msg)) - end ; - exercise) + ( match version with + | "1" | "2" -> () + | _ -> + let msg = Format.asprintf "unknown version %s" version in + raise (J.Cannot_destruct ([], Failure msg)) ); + exercise ) (J.merge_objs (J.obj1 (J.req "learnocaml_version" J.string)) enc) module Server = struct - type preconfig = { - secret : string option; - } - let empty_preconfig = { - secret = None; - } + type preconfig = {secret : string option} + + let empty_preconfig = {secret = None} let preconfig_enc = - J.conv (fun (c : preconfig) -> c.secret) - (fun secret : preconfig -> {secret}) @@ - J.obj1 (J.opt "secret" J.string) + J.conv + (fun (c : preconfig) -> c.secret) + (fun secret -> ({secret} : preconfig)) + @@ J.obj1 (J.opt "secret" J.string) - type config = { - secret : string option; - server_id : int; - } + type config = {secret : string option; server_id : int} let build_config (preconf : preconfig) : config = - let secret = match preconf.secret with + let secret = + match preconf.secret with | None -> None - | Some secret_in_clear -> Some (Sha.sha512 secret_in_clear) in + | Some secret_in_clear -> Some (Sha.sha512 secret_in_clear) + in let server_id = Random.bits () in - { - secret; - server_id; - } + {secret; server_id} let config_enc = - J.conv (fun (c : config) -> (c.secret,c.server_id)) - (fun (secret,server_id) : config -> {secret; server_id}) @@ - J.obj2 (J.opt "secret" J.string) (J.req "server_id" J.int) + J.conv + (fun (c : config) -> (c.secret, c.server_id)) + (fun (secret, server_id) -> ({secret; server_id} : config)) + @@ J.obj2 (J.opt "secret" J.string) (J.req "server_id" J.int) end module Exercise = struct - type id = string type t = Learnocaml_exercise.t @@ -416,93 +374,91 @@ module Exercise = struct let enc = Learnocaml_exercise.encoding module Meta = struct + type kind = Project | Problem | Exercise - type kind = - | Project - | Problem - | Exercise - - type t = { - kind: kind; - title: string; - short_description: string option; - stars: float (* \in [0.,4.] *); - id: id option; - author: (string * string) list; - focus: string list; - requirements: string list; - forward: id list; - backward: id list; - } + type t = + { kind : kind + ; title : string + ; short_description : string option + ; stars : float (* \in [0.,4.] *) + ; id : id option + ; author : (string * string) list + ; focus : string list + ; requirements : string list + ; forward : id list + ; backward : id list } let enc = let kind_enc = J.string_enum - [ "problem", Problem ; - "project", Project ; - "exercise", Exercise ] + [("problem", Problem); ("project", Project); ("exercise", Exercise)] in let exercise_enc_v1 = - J.(obj10 - (req "kind" kind_enc) - (dft "title" string "") - (opt "short_description" string) - (req "stars" float) - (opt "identifier" string) - (dft "authors" (list (tup2 string string)) []) - (dft "focus" (list string) []) - (dft "requirements" (list string) []) - (dft "forward_exercises" (list string) []) - (dft "backward_exercises" (list string) [])) + J.( + obj10 (req "kind" kind_enc) (dft "title" string "") + (opt "short_description" string) + (req "stars" float) (opt "identifier" string) + (dft "authors" (list (tup2 string string)) []) + (dft "focus" (list string) []) + (dft "requirements" (list string) []) + (dft "forward_exercises" (list string) []) + (dft "backward_exercises" (list string) [])) in let exercise_enc_v2 = - J.(obj1 - (opt "max_score" int)) + J.(obj1 (opt "max_score" int)) (* deprecated & ignored *) in J.conv (fun t -> - ((t.kind, t.title, t.short_description, t.stars, t.id, - t.author, t.focus, t.requirements, t.forward, t.backward), - None)) - (fun ((kind, title, short_description, stars, id, - author, focus, requirements, forward, backward), - _max_score) -> - { kind; title; short_description; stars; id; - author; focus; requirements; forward; backward; - }) - (enc_check_version_2 - (J.merge_objs - exercise_enc_v1 - exercise_enc_v2)) - + ( ( t.kind + , t.title + , t.short_description + , t.stars + , t.id + , t.author + , t.focus + , t.requirements + , t.forward + , t.backward ) + , None ) ) + (fun ( ( kind + , title + , short_description + , stars + , id + , author + , focus + , requirements + , forward + , backward ) + , _max_score ) -> + { kind + ; title + ; short_description + ; stars + ; id + ; author + ; focus + ; requirements + ; forward + ; backward } ) + (enc_check_version_2 (J.merge_objs exercise_enc_v1 exercise_enc_v2)) end module Status = struct - type skill = [`Plus | `Minus] * string - type status = - | Open - | Closed - | Assigned of {start: float; stop: float} + type status = Open | Closed | Assigned of {start : float; stop : float} - type assignments = { - token_map: status Token.Map.t; - default: status; - } + type assignments = {token_map : status Token.Map.t; default : status} - type t = { - id: id; - skills_prereq: skill list; - skills_focus: skill list; - assignments: assignments; - } + type t = + { id : id + ; skills_prereq : skill list + ; skills_focus : skill list + ; assignments : assignments } - let empty_assignments = { - token_map = Token.Map.empty; - default = Open; - } + let empty_assignments = {token_map = Token.Map.empty; default = Open} let default_assignment a = a.default @@ -516,34 +472,32 @@ module Exercise = struct let is_open_assignment token a = match get_status token a with | Assigned a -> - let t = Unix.gettimeofday () in - if t < a.start then `Closed - else `Deadline (a.stop -. t) + let t = Unix.gettimeofday () in + if t < a.start then `Closed else `Deadline (a.stop -. t) | Open -> `Open | Closed -> `Closed let get_skills ~base skills = - SSet.elements @@ - List.fold_left (fun acc (op, sk) -> - match op with - | `Plus -> SSet.add sk acc - | `Minus -> SSet.remove sk acc) - (SSet.of_list base) skills + SSet.elements + @@ List.fold_left + (fun acc (op, sk) -> + match op with + | `Plus -> SSet.add sk acc + | `Minus -> SSet.remove sk acc ) + (SSet.of_list base) skills let skills_base ~current skills = - get_skills ~base:current (List.map (function - | `Plus, x -> `Minus, x - | `Minus, x -> `Plus, x) - skills) + get_skills ~base:current + (List.map + (function `Plus, x -> (`Minus, x) | `Minus, x -> (`Plus, x)) + skills) let make_skills ~base current = - let base = SSet.of_list base in - let current = SSet.of_list current in - SSet.fold (fun sk acc -> (`Plus, sk) :: acc) - (SSet.diff current base) @@ - SSet.fold (fun sk acc -> (`Minus, sk) :: acc) - (SSet.diff base current) @@ - [] + let base = SSet.of_list base in + let current = SSet.of_list current in + SSet.fold (fun sk acc -> (`Plus, sk) :: acc) (SSet.diff current base) + @@ SSet.fold (fun sk acc -> (`Minus, sk) :: acc) (SSet.diff base current) + @@ [] let skills_prereq meta status = get_skills ~base:meta.Meta.requirements status.skills_prereq @@ -551,22 +505,25 @@ module Exercise = struct let skills_focus meta status = get_skills ~base:meta.Meta.focus status.skills_focus - module STM = Map.Make(struct - type t = status - let compare = compare - end) + module STM = Map.Make (struct + type t = status + + let compare = compare + end) let by_status_explicit base a = - Token.Map.fold (fun tok st sm -> + Token.Map.fold + (fun tok st sm -> match STM.find_opt st sm with | None -> STM.add st (Token.Set.singleton tok) sm - | Some toks -> STM.add st (Token.Set.add tok toks) sm) - a.token_map - base + | Some toks -> STM.add st (Token.Set.add tok toks) sm ) + a.token_map base let by_status tokens a = let rem_tokens = - Token.Set.filter (fun tok -> not (Token.Map.mem tok a.token_map)) tokens + Token.Set.filter + (fun tok -> not (Token.Map.mem tok a.token_map)) + tokens in let base = if Token.Set.is_empty rem_tokens then STM.empty @@ -576,41 +533,39 @@ module Exercise = struct let three_way_merge ~ancestor ~theirs ~ours = let id = ancestor.id in - if id <> theirs.id || id <> ours.id then - invalid_arg "three_way_merge"; + if id <> theirs.id || id <> ours.id then invalid_arg "three_way_merge"; let skills_merge3 field = let aux filter = - SSet.merge3 - ~ancestor:(filter ancestor) - ~theirs:(filter theirs) + SSet.merge3 ~ancestor:(filter ancestor) ~theirs:(filter theirs) ~ours:(filter ours) |> SSet.elements in - List.map (fun sk -> `Plus, sk) - (aux (fun a -> - List.fold_left (fun acc -> function - |`Plus, sk -> SSet.add sk acc - | _ -> acc) SSet.empty (field a))) @ - List.map (fun sk -> `Minus, sk) + List.map + (fun sk -> (`Plus, sk)) (aux (fun a -> - List.fold_left (fun acc -> function - |`Minus, sk -> SSet.add sk acc - | _ -> acc) SSet.empty (field a))) - in - let skills_prereq = - skills_merge3 (fun a -> a.skills_prereq) - in - let skills_focus = - skills_merge3 (fun a -> a.skills_focus) + List.fold_left + (fun acc -> function `Plus, sk -> SSet.add sk acc | _ -> acc) + SSet.empty (field a) )) + @ List.map + (fun sk -> (`Minus, sk)) + (aux (fun a -> + List.fold_left + (fun acc -> function `Minus, sk -> SSet.add sk acc + | _ -> acc ) + SSet.empty (field a) )) in + let skills_prereq = skills_merge3 (fun a -> a.skills_prereq) in + let skills_focus = skills_merge3 (fun a -> a.skills_focus) in let default = - if ours.assignments.default <> ancestor.assignments.default - then ours.assignments.default + if ours.assignments.default <> ancestor.assignments.default then + ours.assignments.default else theirs.assignments.default in let token_map = - Token.Map.merge (fun tok a o -> - let a = match a with + Token.Map.merge + (fun tok a o -> + let a = + match a with | None -> ancestor.assignments.default | Some st -> st in @@ -619,161 +574,141 @@ module Exercise = struct | None -> theirs.assignments.default | Some st -> st in - let o = match o with - | None -> ours.assignments.default - | Some st -> st + let o = + match o with None -> ours.assignments.default | Some st -> st in - if o <> a then Some o else Some t) - ancestor.assignments.token_map - ours.assignments.token_map + if o <> a then Some o else Some t ) + ancestor.assignments.token_map ours.assignments.token_map in let token_map = - Token.Map.merge (fun _ t o -> match t, o with + Token.Map.merge + (fun _ t o -> + match (t, o) with | _, (Some st as s) | (Some st as s), _ -> if st = default then None else s - | None, None -> assert false) - theirs.assignments.token_map - token_map + | None, None -> assert false ) + theirs.assignments.token_map token_map in - { id; - skills_prereq; - skills_focus; - assignments = { default; token_map } } + {id; skills_prereq; skills_focus; assignments = {default; token_map}} - let make_assignments token_map default = - { token_map; default } + let make_assignments token_map default = {token_map; default} let enc = let status_enc = - J.union [ - J.case (J.constant "Open") - (function Open -> Some () | _ -> None) (fun () -> Open); - J.case (J.constant "Closed") - (function Closed -> Some () | _ -> None) (fun () -> Closed); - J.case - (J.obj2 (J.req "start" J.float) (J.req "stop" J.float)) - (function Assigned a -> Some (a.start, a.stop) | _ -> None) - (fun (start, stop) -> Assigned {start; stop}) - ] + J.union + [ J.case (J.constant "Open") + (function Open -> Some () | _ -> None) + (fun () -> Open) + ; J.case (J.constant "Closed") + (function Closed -> Some () | _ -> None) + (fun () -> Closed) + ; J.case + (J.obj2 (J.req "start" J.float) (J.req "stop" J.float)) + (function Assigned a -> Some (a.start, a.stop) | _ -> None) + (fun (start, stop) -> Assigned {start; stop}) ] in let assignments_enc = J.conv (fun t -> - t.default, - List.map (fun (tk, st) -> Token.to_string tk, st) - (Token.Map.bindings t.token_map)) - (fun (default, token_assoc) -> { - default; - token_map = - (List.fold_left (fun acc (tok, st) -> - Token.Map.add (Token.parse tok) st acc) - Token.Map.empty token_assoc) - }) - @@ - J.obj2 - (J.dft "default" status_enc empty_assignments.default) - (J.dft "token_map" (J.assoc status_enc) []) + ( t.default + , List.map + (fun (tk, st) -> (Token.to_string tk, st)) + (Token.Map.bindings t.token_map) ) ) + (fun (default, token_assoc) -> + { default + ; token_map = + List.fold_left + (fun acc (tok, st) -> Token.Map.add (Token.parse tok) st acc) + Token.Map.empty token_assoc } ) + @@ J.obj2 + (J.dft "default" status_enc empty_assignments.default) + (J.dft "token_map" (J.assoc status_enc) []) in let skill_enc = - J.union [ - J.case (J.obj1 (J.req "plus" J.string)) - (function `Plus, sk -> Some sk | _ -> None) - (fun sk -> `Plus, sk); - J.case (J.obj1 (J.req "minus" J.string)) - (function `Minus, sk -> Some sk | _ -> None) - (fun sk -> `Minus, sk); - ] + J.union + [ J.case + (J.obj1 (J.req "plus" J.string)) + (function `Plus, sk -> Some sk | _ -> None) + (fun sk -> (`Plus, sk)) + ; J.case + (J.obj1 (J.req "minus" J.string)) + (function `Minus, sk -> Some sk | _ -> None) + (fun sk -> (`Minus, sk)) ] in - enc_check_version_2 @@ - J.conv - (fun t -> t.id, t.skills_prereq, t.skills_focus, t.assignments) - (fun (id, skills_prereq, skills_focus, assignments) -> - {id; skills_prereq; skills_focus; assignments}) - @@ - J.obj4 - (J.req "id" J.string) - (J.dft "skills_prereq" (J.list skill_enc) []) - (J.dft "skills_focus" (J.list skill_enc) []) - (J.dft "assignments" assignments_enc empty_assignments) - - let default id = { - id; - skills_prereq = []; - skills_focus = []; - assignments = { - token_map = Token.Map.empty; - default = Open; - } - } - + enc_check_version_2 + @@ J.conv + (fun t -> (t.id, t.skills_prereq, t.skills_focus, t.assignments)) + (fun (id, skills_prereq, skills_focus, assignments) -> + {id; skills_prereq; skills_focus; assignments} ) + @@ J.obj4 (J.req "id" J.string) + (J.dft "skills_prereq" (J.list skill_enc) []) + (J.dft "skills_focus" (J.list skill_enc) []) + (J.dft "assignments" assignments_enc empty_assignments) + + let default id = + { id + ; skills_prereq = [] + ; skills_focus = [] + ; assignments = {token_map = Token.Map.empty; default = Open} } end module Index = struct - type t = | Exercises of (id * Meta.t option) list | Groups of (string * group) list - and group = - { title : string; - contents : t } + + and group = {title : string; contents : t} let enc = let exercise_enc = - J.union [ - J.case J.string - (function id, None -> Some id | _ -> None) - (fun id -> id, None); - J.case J.(tup2 string Meta.enc) - (function id, Some meta -> Some (id, meta) | _ -> None) - (fun (id, meta) -> id, Some meta); - ] + J.union + [ J.case J.string + (function id, None -> Some id | _ -> None) + (fun id -> (id, None)) + ; J.case + J.(tup2 string Meta.enc) + (function id, Some meta -> Some (id, meta) | _ -> None) + (fun (id, meta) -> (id, Some meta)) ] in let group_enc = - J.mu "group" @@ fun group_enc -> + J.mu "group" + @@ fun group_enc -> J.conv - (fun (g : group) -> g.title, g.contents) - (fun (title, contents) -> { title; contents }) @@ - J.union - [ J.case - J.(obj2 - (req "title" string) - (req "exercises" (list exercise_enc))) - (function - | (title, Exercises map) -> Some (title, map) - | _ -> None) - (fun (title, map) -> (title, Exercises map)) ; - J.case - J.(obj2 - (req "title" string) - (req "groups" (assoc group_enc))) - (function - | (title, Groups map) -> Some (title, map) - | _ -> None) - (fun (title, map) -> (title, Groups map)) ] + (fun (g : group) -> (g.title, g.contents)) + (fun (title, contents) -> {title; contents}) + @@ J.union + [ J.case + J.( + obj2 (req "title" string) + (req "exercises" (list exercise_enc))) + (function + | title, Exercises map -> Some (title, map) | _ -> None) + (fun (title, map) -> (title, Exercises map)) + ; J.case + J.(obj2 (req "title" string) (req "groups" (assoc group_enc))) + (function + | title, Groups map -> Some (title, map) | _ -> None) + (fun (title, map) -> (title, Groups map)) ] in - enc_check_version_2 @@ - J.union - [ J.case - J.(obj1 (req "exercises" (list exercise_enc))) - (function - | Exercises map -> Some map - | _ -> None) - (fun map -> Exercises map) ; - J.case - J.(obj1 (req "groups" (assoc group_enc))) - (function - | Groups map -> Some map - | _ -> None) - (fun map -> Groups map) ] + enc_check_version_2 + @@ J.union + [ J.case + J.(obj1 (req "exercises" (list exercise_enc))) + (function Exercises map -> Some map | _ -> None) + (fun map -> Exercises map) + ; J.case + J.(obj1 (req "groups" (assoc group_enc))) + (function Groups map -> Some map | _ -> None) + (fun map -> Groups map) ] let find t id = - let rec aux t = match t with - | Groups ((_, g)::r) -> - (try aux g.contents with Not_found -> aux (Groups r)) + let rec aux t = + match t with + | Groups ((_, g) :: r) -> ( + try aux g.contents with Not_found -> aux (Groups r) ) | Groups [] -> raise Not_found - | Exercises l -> (match List.assoc id l with - | None -> raise Not_found - | Some e -> e) + | Exercises l -> ( + match List.assoc id l with None -> raise Not_found | Some e -> e ) in aux t @@ -782,66 +717,70 @@ module Exercise = struct let rec map_exercises f = function | Groups gs -> Groups - (List.map (fun (id, (g: group)) -> - (id, {g with contents = map_exercises f g.contents})) - gs) + (List.map + (fun (id, (g : group)) -> + (id, {g with contents = map_exercises f g.contents}) ) + gs) | Exercises l -> Exercises - (List.map (function - | (id, Some ex) -> (id, Some (f id ex)) - | x -> x) - l) + (List.map + (function id, Some ex -> (id, Some (f id ex)) | x -> x) + l) let rec mapk_exercises f t k = - let rec mapk_list acc f l k = match l with - | x::r -> f x (fun y -> mapk_list (y::acc) f r @@ k) + let rec mapk_list acc f l k = + match l with + | x :: r -> f x (fun y -> mapk_list (y :: acc) f r @@ k) | [] -> List.rev acc |> k in match t with | Groups gs -> - mapk_list [] (fun (id, (g: group)) k -> + mapk_list [] + (fun (id, (g : group)) k -> mapk_exercises f g.contents - @@ fun contents -> (id, {g with contents}) |> k) + @@ fun contents -> (id, {g with contents}) |> k ) gs @@ fun gs -> Groups gs |> k | Exercises l -> - mapk_list [] (fun e k -> match e with - | (id, Some ex) -> - f id ex @@ fun ex -> (id, Some ex) |> k - | x -> x |> k) + mapk_list [] + (fun e k -> + match e with + | id, Some ex -> f id ex @@ fun ex -> (id, Some ex) |> k + | x -> x |> k ) l @@ fun l -> Exercises l |> k let rec fold_exercises f acc = function | Groups gs -> List.fold_left - (fun acc (_, (g: group)) -> fold_exercises f acc g.contents) + (fun acc (_, (g : group)) -> fold_exercises f acc g.contents) acc gs | Exercises l -> - List.fold_left (fun acc -> function - | (id, Some ex) -> f acc id ex - | _ -> acc) + List.fold_left + (fun acc -> function id, Some ex -> f acc id ex | _ -> acc) acc l let rec filterk f g k = match g with | Groups gs -> let rec aux acc = function - | (id, (g: group)) :: r -> - (filterk f g.contents @@ function - | Exercises [] -> aux acc r - | contents -> aux ((id, { g with contents }) :: acc) r) - | [] -> match acc with + | (id, (g : group)) :: r -> ( + filterk f g.contents + @@ function + | Exercises [] -> aux acc r + | contents -> aux ((id, {g with contents}) :: acc) r ) + | [] -> ( + match acc with | [] -> k (Exercises []) - | l -> k (Groups (List.rev l)) + | l -> k (Groups (List.rev l)) ) in aux [] gs | Exercises l -> let rec aux acc = function - | (id, Some ex) :: r -> - (f id ex @@ function - | true -> aux ((id, Some ex) :: acc) r - | false -> aux acc r) + | (id, Some ex) :: r -> ( + f id ex + @@ function + | true -> aux ((id, Some ex) :: acc) r | false -> aux acc r ) | (_, None) :: r -> aux acc r | [] -> k (Exercises (List.rev acc)) in @@ -864,50 +803,46 @@ module Exercise = struct * | _ -> acc) * [] (List.rev l) * |> (function l -> Exercises l) *) - end module Graph = struct - type relation = Skill of string | Exercise of id - type node = - { name : id; - mutable children : (node * relation list) list } + type node = {name : id; mutable children : (node * relation list) list} + + let node_exercise {name; _} = name - let node_exercise { name; _ } = name - let node_children { children; _ } = children + let node_children {children; _} = children let ex_node exs id = - try Hashtbl.find exs id - with Not_found -> - let node = { name = id; children = [] } in - Hashtbl.add exs id node; - node + try Hashtbl.find exs id with Not_found -> + let node = {name = id; children = []} in + Hashtbl.add exs id node; node let merge_children ch = let rec merge acc = function | [] -> acc - | (n, ks) :: [] -> (n, ks) :: acc - | (n, ks) :: (((n', ks') :: rem) as ch') -> + | [(n, ks)] -> (n, ks) :: acc + | (n, ks) :: ((n', ks') :: rem as ch') -> if n.name = n'.name then merge acc ((n, ks @ ks') :: rem) else merge ((n, ks) :: acc) ch' in List.fast_sort (fun (n1, _) (n2, _) -> compare n1.name n2.name) ch |> merge [] - let compute_node ex_id ex_meta focus exercises = let exs = - List.map (fun id -> ex_node exercises id, [Exercise ex_id]) + List.map + (fun id -> (ex_node exercises id, [Exercise ex_id])) ex_meta.Meta.backward in let exs = - List.fold_left (fun exs skill -> - List.fold_left (fun exs id -> - (ex_node exercises id, [Skill skill]) :: exs) - exs (SMap.find skill focus) - ) exs ex_meta.Meta.requirements + List.fold_left + (fun exs skill -> + List.fold_left + (fun exs id -> (ex_node exercises id, [Skill skill]) :: exs) + exs (SMap.find skill focus) ) + exs ex_meta.Meta.requirements in let exs = merge_children exs in let node = ex_node exercises ex_id in @@ -916,52 +851,55 @@ module Exercise = struct let focus_map exercises = let add_ex focus (id, skill) = - let exs = - try SMap.find skill focus - with Not_found -> [] in + let exs = try SMap.find skill focus with Not_found -> [] in SMap.add skill (id :: exs) focus in Index.fold_exercises (fun focus id meta -> - List.fold_left add_ex focus - @@ List.map (fun s -> id, s) meta.Meta.focus) + List.fold_left add_ex focus + @@ List.map (fun s -> (id, s)) meta.Meta.focus ) SMap.empty exercises let apply_filters filters exercises = - Index.filter (fun id _ -> - not (List.mem (Exercise id) filters)) exercises |> - Index.map_exercises (fun _ meta -> - let requirements = - List.filter (fun s -> not (List.mem (Skill s) filters)) - meta.Meta.requirements in - let focus = - List.filter (fun s -> not (List.mem (Skill s) filters)) - meta.Meta.focus in - let backward = - List.filter (fun s -> not (List.mem (Exercise s) filters)) - meta.Meta.backward in - { meta with Meta.requirements; Meta.focus; Meta.backward }) - - let compute_graph ?(filters=[]) exercises = + Index.filter (fun id _ -> not (List.mem (Exercise id) filters)) exercises + |> Index.map_exercises (fun _ meta -> + let requirements = + List.filter + (fun s -> not (List.mem (Skill s) filters)) + meta.Meta.requirements + in + let focus = + List.filter + (fun s -> not (List.mem (Skill s) filters)) + meta.Meta.focus + in + let backward = + List.filter + (fun s -> not (List.mem (Exercise s) filters)) + meta.Meta.backward + in + {meta with Meta.requirements; Meta.focus; Meta.backward} ) + + let compute_graph ?(filters = []) exercises = let exercises_nodes = Hashtbl.create 17 in let ex_filtered = apply_filters filters exercises in let focus = focus_map ex_filtered in let compute acc ex_id ex_meta = compute_node ex_id ex_meta focus exercises_nodes :: acc in - Index.fold_exercises (fun acc id meta -> compute acc id meta) [] ex_filtered + Index.fold_exercises + (fun acc id meta -> compute acc id meta) + [] ex_filtered let compute_exercise_set graph = let seen = ref SSet.empty in let rec compute acc node = if SSet.mem node.name !seen then acc - else begin + else ( seen := SSet.add node.name !seen; - List.fold_right (fun (node, _kinds) acc -> - compute acc node) - node.children - (node.name :: acc) - end + List.fold_right + (fun (node, _kinds) acc -> compute acc node) + node.children (node.name :: acc) ) in compute [] graph @@ -971,326 +909,265 @@ module Exercise = struct | Exercise s -> Format.fprintf fmt "(E %s)" s in let print_child fmt ex child kinds = - Format.fprintf fmt "%s -> %s [label=\"%a\"];\n" - ex - child.name - (fun fmt -> List.iter (print_kind fmt)) kinds + Format.fprintf fmt "%s -> %s [label=\"%a\"];\n" ex child.name + (fun fmt -> List.iter (print_kind fmt)) + kinds in let print_node fmt n = - List.iter (fun (child, kinds) -> - print_child fmt n.name child kinds) + List.iter + (fun (child, kinds) -> print_child fmt n.name child kinds) n.children in - Format.fprintf fmt - "digraph exercises {\n\ - %a\n\ - }" - (fun fmt -> List.iter (print_node fmt)) nodes - + Format.fprintf fmt "digraph exercises {\n%a\n}" + (fun fmt -> List.iter (print_node fmt)) + nodes end - end module Lesson = struct - type id = string - type phrase = - | Text of string - | Code of string + type phrase = Text of string | Code of string - type step = { - step_title: string; - step_phrases: phrase list; - } + type step = {step_title : string; step_phrases : phrase list} - type t = { - title: string; - steps: step list; - } + type t = {title : string; steps : step list} let enc = - enc_check_version_2 @@ - J.conv - (fun t -> (t.title, t.steps)) - (fun (title, steps) -> { title; steps }) @@ - J.obj2 - J.(req "title" string) - J.(req "steps" - (list @@ - conv - (fun s -> (s.step_title, s.step_phrases)) - (fun (step_title, step_phrases) -> {step_title; step_phrases}) @@ - (obj2 - (req "title" string) - (req "contents" - (list @@ union - [ case - (obj1 (req "html" string)) - (function Text text -> Some text | Code _ -> None) - (fun text -> Text text) ; - case - (obj1 (req "code" string)) - (function Code code -> Some code | Text _ -> None) - (fun code -> Code code) ]))))) + enc_check_version_2 + @@ J.conv + (fun t -> (t.title, t.steps)) + (fun (title, steps) -> {title; steps}) + @@ J.obj2 + J.(req "title" string) + J.( + req "steps" + ( list + @@ conv + (fun s -> (s.step_title, s.step_phrases)) + (fun (step_title, step_phrases) -> {step_title; step_phrases}) + @@ obj2 (req "title" string) + (req "contents" + ( list + @@ union + [ case + (obj1 (req "html" string)) + (function + | Text text -> Some text | Code _ -> None) + (fun text -> Text text) + ; case + (obj1 (req "code" string)) + (function + | Code code -> Some code | Text _ -> None) + (fun code -> Code code) ] )) )) module Index = struct - type t = (id * string) list let enc = - enc_check_version_2 @@ - J.(obj1 (req "lessons" (list @@ tup2 string string))) - + enc_check_version_2 + @@ J.(obj1 (req "lessons" (list @@ tup2 string string))) end - end module Tutorial = struct - type id = string - type code = { - code: string; - runnable: bool; - } + type code = {code : string; runnable : bool} type word = | Text of string | Code of code | Emph of text - | Image of { alt : string ; mime : string ; contents : bytes } + | Image of {alt : string; mime : string; contents : bytes} | Math of string - and text = - word list + and text = word list type phrase = | Paragraph of text | Enum of phrase list list | Code_block of code - type step = { - step_title: text; - step_contents: phrase list; - } + type step = {step_title : text; step_contents : phrase list} - type t = { - title: text; - steps: step list; - } + type t = {title : text; steps : step list} let text_enc = - J.mu "text" @@ fun content_enc -> + J.mu "text" + @@ fun content_enc -> let word_enc = J.union [ J.case J.string (function Text text -> Some text | _ -> None) - (fun text -> Text text) ; - J.case + (fun text -> Text text) + ; J.case J.(obj1 (req "text" string)) (function Text text -> Some text | _ -> None) - (fun text -> Text text) ; - J.case + (fun text -> Text text) + ; J.case J.(obj1 (req "emph" content_enc)) (function Emph content -> Some content | _ -> None) - (fun content -> Emph content) ; - J.case + (fun content -> Emph content) + ; J.case J.(obj2 (req "code" string) (dft "runnable" bool false)) - (function Code { code ; runnable } -> Some (code, runnable) - | _ -> None) - (fun (code, runnable) -> Code { code ; runnable }) ; - J.case + (function + | Code {code; runnable} -> Some (code, runnable) | _ -> None) + (fun (code, runnable) -> Code {code; runnable}) + ; J.case J.(obj1 (req "math" string)) - (function Math math-> Some math | _ -> None) - (fun math -> Math math) ; - J.case + (function Math math -> Some math | _ -> None) + (fun math -> Math math) + ; J.case J.(obj3 (req "image" bytes) (req "alt" string) (req "mime" string)) (function - | Image { alt ; mime ; contents = image } -> - Some (image, alt, mime) + | Image {alt; mime; contents = image} -> Some (image, alt, mime) | _ -> None) - (fun (image, alt, mime) -> - Image { alt ; mime ; contents = image }) ] in + (fun (image, alt, mime) -> Image {alt; mime; contents = image}) ] + in J.union - [ J.case - word_enc - (function [ ctns ] -> Some ctns | _ -> None) (fun ctns -> [ ctns ]) ; - J.case - (J.list @@ word_enc) - (fun ctns -> Some ctns) (fun ctns -> ctns) ] + [ J.case word_enc + (function [ctns] -> Some ctns | _ -> None) + (fun ctns -> [ctns]) + ; J.case (J.list @@ word_enc) (fun ctns -> Some ctns) (fun ctns -> ctns) + ] let phrase_enc = - J.mu "phrase" @@ fun phrase_enc -> + J.mu "phrase" + @@ fun phrase_enc -> J.union [ J.case J.(obj1 (req "paragraph" text_enc)) (function Paragraph phrase -> Some phrase | _ -> None) - (fun phrase -> Paragraph phrase) ; - J.case + (fun phrase -> Paragraph phrase) + ; J.case J.(obj1 (req "enum" (list (list phrase_enc)))) (function Enum items -> Some items | _ -> None) - (fun items -> Enum items) ; - J.case + (fun items -> Enum items) + ; J.case J.(obj2 (req "code" string) (dft "runnable" bool false)) - (function Code_block { code ; runnable } -> - Some (code, runnable) | _ -> None) - (fun (code, runnable) -> - Code_block { code ; runnable }) ; - J.case - text_enc + (function + | Code_block {code; runnable} -> Some (code, runnable) | _ -> None) + (fun (code, runnable) -> Code_block {code; runnable}) + ; J.case text_enc (function Paragraph phrase -> Some phrase | _ -> None) (fun phrase -> Paragraph phrase) ] let enc = - enc_check_version_2 @@ - J.conv - (fun t -> t.title, t.steps) - (fun (title, steps) -> {title; steps}) @@ - J.obj2 - (J.req "title" text_enc) - (J.req "steps" - (J.list @@ - J.conv - (fun t -> t.step_title, t.step_contents) - (fun (step_title, step_contents) -> {step_title; step_contents}) @@ - J.(obj2 - (req "title" text_enc) - (req "contents" (list phrase_enc))))) + enc_check_version_2 + @@ J.conv + (fun t -> (t.title, t.steps)) + (fun (title, steps) -> {title; steps}) + @@ J.obj2 (J.req "title" text_enc) + (J.req "steps" + ( J.list + @@ J.conv + (fun t -> (t.step_title, t.step_contents)) + (fun (step_title, step_contents) -> + {step_title; step_contents} ) + @@ J.( + obj2 (req "title" text_enc) (req "contents" (list phrase_enc))) + )) module Index = struct + type entry = {name : string; title : text} - type entry = { - name: string; - title: text; - } - - type series = { - series_title: string; - series_tutorials: entry list; - } + type series = {series_title : string; series_tutorials : entry list} type t = (id * series) list let enc = let entry_enc = - J.union [ - J.case - J.(tup2 string text_enc) - (function ({title = []; _}: entry) -> None - | {name; title} -> Some (name, title)) - (fun (name, title) -> {name; title}); - J.case - J.string - (function {name; title = []} -> Some name - | _ -> None) - (fun name -> {name; title = []}); - ] + J.union + [ J.case + J.(tup2 string text_enc) + (function + | ({title = []; _} : entry) -> None + | {name; title} -> Some (name, title)) + (fun (name, title) -> {name; title}) + ; J.case J.string + (function {name; title = []} -> Some name | _ -> None) + (fun name -> {name; title = []}) ] in let series_enc = J.conv - (fun t -> - (t.series_title, t.series_tutorials)) + (fun t -> (t.series_title, t.series_tutorials)) (fun (series_title, series_tutorials) -> - {series_title; series_tutorials}) @@ - J.obj2 - J.(req "title" string) - J.(req "tutorials" (list entry_enc)) in - enc_check_version_1 @@ - J.(obj1 (req "series" (assoc series_enc))) - + {series_title; series_tutorials} ) + @@ J.obj2 J.(req "title" string) J.(req "tutorials" (list entry_enc)) + in + enc_check_version_1 @@ J.(obj1 (req "series" (assoc series_enc))) end end module Partition = struct type t = - { - not_graded : Token.t list; - bad_type : Token.t list; - partition_by_grade : - (int * - (((Token.t * string) list) Asak.Wtree.wtree list)) - list; - } + { not_graded : Token.t list + ; bad_type : Token.t list + ; partition_by_grade : + (int * (Token.t * string) list Asak.Wtree.wtree list) list } let token_list = J.list Token.enc let tree_enc leaf_enc = let open Asak.Wtree in - J.mu "tree" @@ fun self -> - J.union - [ J.case (J.obj1 (J.req "leaf" leaf_enc)) - (function Leaf x -> Some x | Node _ -> None) - (fun x -> Leaf x) ; - J.case (J.obj3 (J.req "coef" J.int) (J.req "left" self) (J.req "right" self)) - (function Node (t,l,r) -> Some (t,l,r) | Leaf _ -> None) - (fun (t,l,r) -> Node (t,l,r)) ] - - let leaf_enc = - J.list (J.tup2 Token.enc J.string) + J.mu "tree" + @@ fun self -> + J.union + [ J.case + (J.obj1 (J.req "leaf" leaf_enc)) + (function Leaf x -> Some x | Node _ -> None) + (fun x -> Leaf x) + ; J.case + (J.obj3 (J.req "coef" J.int) (J.req "left" self) (J.req "right" self)) + (function Node (t, l, r) -> Some (t, l, r) | Leaf _ -> None) + (fun (t, l, r) -> Node (t, l, r)) ] + + let leaf_enc = J.list (J.tup2 Token.enc J.string) let innerlist = J.list (tree_enc leaf_enc) - let int_assoc = - J.tup2 J.int innerlist + let int_assoc = J.tup2 J.int innerlist let enc = J.conv - (fun t -> - (t.not_graded, t.bad_type, t.partition_by_grade)) + (fun t -> (t.not_graded, t.bad_type, t.partition_by_grade)) (fun (not_graded, bad_type, partition_by_grade) -> - {not_graded; bad_type; partition_by_grade}) @@ - J.obj3 - J.(req "not_graded" token_list) - J.(req "bad_type" token_list) - J.(req "patition_by_grade" (J.list int_assoc)) + {not_graded; bad_type; partition_by_grade} ) + @@ J.obj3 + J.(req "not_graded" token_list) + J.(req "bad_type" token_list) + J.(req "patition_by_grade" (J.list int_assoc)) end module Playground = struct type id = string - type t = - { id : id ; - prelude : string ; - template : string ; - } + type t = {id : id; prelude : string; template : string} let enc = J.conv - (fun { id; prelude; template } -> - id, prelude, template) - (fun (id, prelude, template) -> - { id ; prelude ; template }) - (J.obj3 - (J.req "id" J.string) - (J.req "prelude" J.string) - (J.req "template" J.string)) + (fun {id; prelude; template} -> (id, prelude, template)) + (fun (id, prelude, template) -> {id; prelude; template}) + (J.obj3 (J.req "id" J.string) (J.req "prelude" J.string) + (J.req "template" J.string)) module Meta = struct - type t = - { - title: string; - short_description: string option; - } + type t = {title : string; short_description : string option} - let default id = {title=id; short_description=None} + let default id = {title = id; short_description = None} let enc = - J.conv - (fun { title; short_description } -> - title, short_description) - (fun (title, short_description) -> - { title; short_description }) - (J.obj2 - (J.req "title" J.string) - (J.req "short_description" (J.option J.string))) + J.conv + (fun {title; short_description} -> (title, short_description)) + (fun (title, short_description) -> {title; short_description}) + (J.obj2 (J.req "title" J.string) + (J.req "short_description" (J.option J.string))) end module Index = struct - type t = (id * Meta.t) list let enc = J.list (J.tup2 J.string Meta.enc) - end end diff --git a/src/state/learnocaml_data.mli b/src/state/learnocaml_data.mli index 2408c5eec..f97d12c89 100644 --- a/src/state/learnocaml_data.mli +++ b/src/state/learnocaml_data.mli @@ -5,281 +5,248 @@ * * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) - -module SMap: sig - +module SMap : sig include Map.S with type key = string - val enc: 'a Json_encoding.encoding -> 'a t Json_encoding.encoding - + val enc : 'a Json_encoding.encoding -> 'a t Json_encoding.encoding end -module SSet: sig - +module SSet : sig include Set.S with type elt = string - val enc: t Json_encoding.encoding + val enc : t Json_encoding.encoding + val merge3 : ancestor:t -> theirs:t -> ours:t -> t (** Three-way merge. [ours] always wins if it was modified from [ancestor] *) - val merge3: ancestor:t -> theirs:t -> ours:t -> t - end module Report = Learnocaml_report -module Answer: sig - - type t = { - solution: string ; - grade: int (* \in [0, 100] *) option ; - report: Report.t option ; - mtime: float - } - - val enc: t Json_encoding.encoding +module Answer : sig + type t = + { solution : string + ; grade : int (* \in [0, 100] *) option + ; report : Report.t option + ; mtime : float } + val enc : t Json_encoding.encoding end -module Save: sig - - type t = { - nickname: string ; - all_exercise_editors: (float * string) SMap.t; - all_exercise_states: Answer.t SMap.t; - all_toplevel_histories: Learnocaml_toplevel_history.snapshot SMap.t; - all_exercise_toplevel_histories: - Learnocaml_toplevel_history.snapshot SMap.t; - } +module Save : sig + type t = + { nickname : string + ; all_exercise_editors : (float * string) SMap.t + ; all_exercise_states : Answer.t SMap.t + ; all_toplevel_histories : Learnocaml_toplevel_history.snapshot SMap.t + ; all_exercise_toplevel_histories : + Learnocaml_toplevel_history.snapshot SMap.t } - val enc: t Json_encoding.encoding + val enc : t Json_encoding.encoding + val sync : t -> t -> t (** Merges two save files, trusting the [mtime] fields to take the most recent versions of every item. All other things equal, the fields from the second argument are preferred. *) - val sync: t -> t -> t + val fix_mtimes : t -> t (** Checks all [mtime] fields to get them back to now in case they are in the future. Needed for save files that come from clients with possibly bad clocks. *) - val fix_mtimes: t -> t - - val empty: t + val empty : t end -module Token: sig +module Token : sig type t - val enc: t Json_encoding.encoding + val enc : t Json_encoding.encoding + + val to_path : t -> string + + val to_string : t -> string + + val parse : string -> t + + val check : string -> bool + + val random : unit -> t - val to_path: t -> string - val to_string: t -> string - val parse: string -> t - val check: string -> bool - val random: unit -> t - val random_teacher: unit -> t - val is_teacher: t -> bool - val is_student: t -> bool + val random_teacher : unit -> t + val is_teacher : t -> bool + + val is_student : t -> bool + + val teacher_tokens_path : string (** The relative path containing teacher tokens *) - val teacher_tokens_path: string - module Set: Set.S with type elt = t + module Set : Set.S with type elt = t - module Map: Map.S with type key = t + module Map : Map.S with type key = t end type 'a token = Token.t type student -type teacher - -module Student: sig - type t = { - token: student token; - nickname: string option; - results: (float * int option) SMap.t; - creation_date: float; - tags: SSet.t; - } +type teacher - val enc: t Json_encoding.encoding +module Student : sig + type t = + { token : student token + ; nickname : string option + ; results : (float * int option) SMap.t + ; creation_date : float + ; tags : SSet.t } - val default: student token -> t + val enc : t Json_encoding.encoding - val three_way_merge: ancestor:t -> theirs:t -> ours:t -> t + val default : student token -> t - module Index: sig + val three_way_merge : ancestor:t -> theirs:t -> ours:t -> t + module Index : sig type nonrec t = t list - val enc: t Json_encoding.encoding - + val enc : t Json_encoding.encoding end - end module Server : sig (* preconfig: the type of configuration files in the corpus repository, where users can pre-set some of the server settings. *) - type preconfig = { - secret : string option; - } + type preconfig = {secret : string option} + val empty_preconfig : preconfig (* config: the type of configuration of a running server, generated from the preconfig during the 'build' stage. *) - type config = { - secret : string option; (* maybe a secret *) - server_id : int; (* random integer generated each building time *) - } + type config = + { secret : string option + ; (* maybe a secret *) + server_id : int + (* random integer generated each building time *) } val build_config : preconfig -> config - val preconfig_enc: preconfig Json_encoding.encoding - val config_enc: config Json_encoding.encoding -end + val preconfig_enc : preconfig Json_encoding.encoding -module Exercise: sig + val config_enc : config Json_encoding.encoding +end +module Exercise : sig type id = string type t = Learnocaml_exercise.t - val enc: t Json_encoding.encoding - - module Meta: sig + val enc : t Json_encoding.encoding - type kind = - | Project - | Problem - | Exercise - - type t = { - kind: kind; - title: string; - short_description: string option; - stars: float (** \in [0.,4.] *); - id: id option; - author: (string * string) list; - focus: string list; - requirements: string list; - forward: id list; - backward: id list; - } - - val enc: t Json_encoding.encoding + module Meta : sig + type kind = Project | Problem | Exercise + type t = + { kind : kind + ; title : string + ; short_description : string option + ; stars : float (** \in [0.,4.] *) + ; id : id option + ; author : (string * string) list + ; focus : string list + ; requirements : string list + ; forward : id list + ; backward : id list } + + val enc : t Json_encoding.encoding end - module Status: sig - + module Status : sig type skill - type status = - | Open - | Closed - | Assigned of {start: float; stop: float} + type status = Open | Closed | Assigned of {start : float; stop : float} - type assignments = { - token_map: status Token.Map.t; - default: status; - } + type assignments = {token_map : status Token.Map.t; default : status} - type t = { - id: id; - skills_prereq: skill list; - skills_focus: skill list; - assignments: assignments; - } + type t = + { id : id + ; skills_prereq : skill list + ; skills_focus : skill list + ; assignments : assignments } - val default: id -> t + val default : id -> t - val default_assignment: assignments -> status + val default_assignment : assignments -> status - val set_default_assignment: assignments -> status -> assignments + val set_default_assignment : assignments -> status -> assignments - val get_status: - Token.t -> assignments -> status + val get_status : Token.t -> assignments -> status - val is_open_assignment: + val is_open_assignment : Token.t -> assignments -> [> `Open | `Closed | `Deadline of float] - val by_status: - Token.Set.t -> assignments -> (status * Token.Set.t) list + val by_status : Token.Set.t -> assignments -> (status * Token.Set.t) list + val get_skills : base:string list -> skill list -> string list (** Computes the current set of skills from the base list (from Meta.t), using the mutable changes in the Status.skill list. E.g. {[ get_skills ~base:meta.Meta.requirements st.skills_prereq ]} *) - val get_skills: base:string list -> skill list -> string list + val skills_base : current:string list -> skill list -> string list (** The opposite of [get_skills]: retrieves the base from the already updated version and the skill list that has been applied to it. Since the server provides [skills] (= [get_skills meta_base status_skills]), this is useful to recover [meta_base]. *) - val skills_base: current:string list -> skill list -> string list - val skills_prereq: Meta.t -> t -> string list + val skills_prereq : Meta.t -> t -> string list - val skills_focus: Meta.t -> t -> string list + val skills_focus : Meta.t -> t -> string list + val make_skills : base:string list -> string list -> skill list (** Generates a skill list that can be saved, such that {[get_skills ~base (make_skills ~base l) = l]}. Remember to call [skills_base] first on the base if you got the skills from the meta returned by the server. *) - val make_skills: base:string list -> string list -> skill list + val three_way_merge : ancestor:t -> theirs:t -> ours:t -> t (** Merges all changes from [theirs] and [ours], based on [ancestor]. [ours] is privileged in case of any conflict (e.g. different affectation of the same student) *) - val three_way_merge: - ancestor:t -> theirs:t -> ours:t -> t - val make_assignments: - status Token.Map.t -> status -> assignments - - val enc: t Json_encoding.encoding + val make_assignments : status Token.Map.t -> status -> assignments + val enc : t Json_encoding.encoding end - module Index: sig - + module Index : sig type t = | Exercises of (id * Meta.t option) list | Groups of (string * group) list - and group = - { title : string; - contents : t } - val enc: t Json_encoding.encoding + and group = {title : string; contents : t} - val find: t -> id -> Meta.t + val enc : t Json_encoding.encoding - val find_opt: t -> id -> Meta.t option + val find : t -> id -> Meta.t - val map_exercises: (id -> Meta.t -> Meta.t) -> t -> t + val find_opt : t -> id -> Meta.t option - val fold_exercises: ('a -> id -> Meta.t -> 'a) -> 'a -> t -> 'a + val map_exercises : (id -> Meta.t -> Meta.t) -> t -> t - val filter: (id -> Meta.t -> bool) -> t -> t + val fold_exercises : ('a -> id -> Meta.t -> 'a) -> 'a -> t -> 'a + val filter : (id -> Meta.t -> bool) -> t -> t + + val mapk_exercises : + (id -> Meta.t -> (Meta.t -> 'a) -> 'a) -> t -> (t -> 'a) -> 'a (** CPS version of [map_exercises] *) - val mapk_exercises: - (id -> Meta.t -> (Meta.t -> 'a) -> 'a) -> - t -> - (t -> 'a) -> 'a + val filterk : (id -> Meta.t -> (bool -> 'a) -> 'a) -> t -> (t -> 'a) -> 'a (** CPS version of [filter] *) - val filterk: (id -> Meta.t -> (bool -> 'a) -> 'a) -> t -> (t -> 'a) -> 'a - end (** Dependency graph of exercises *) module Graph : sig - (** Two exercises can be related either by a skill dependency, or backward relationship *) type relation = Skill of string | Exercise of id @@ -290,151 +257,104 @@ module Exercise: sig type node val node_exercise : node -> id + val node_children : node -> (node * relation list) list + val compute_graph : ?filters:relation list -> Index.t -> node list (** Computes the dependency graph of exercises, and filters out exercises or skills if any are given. *) - val compute_graph : ?filters:relation list -> Index.t -> node list + val compute_exercise_set : node -> string list (** Computes a set of exercises that appear as dependencies of the given exercise. *) - val compute_exercise_set : node -> string list - (** Dumps the graph as a `dot` representation, into the given formatter. *) val dump_dot : Format.formatter -> node list -> unit - + (** Dumps the graph as a `dot` representation, into the given formatter. *) end - end -module Lesson: sig - +module Lesson : sig type id = string - type phrase = - | Text of string - | Code of string - - type step = { - step_title: string; - step_phrases: phrase list; - } + type phrase = Text of string | Code of string - type t = { - title: string; - steps: step list; - } + type step = {step_title : string; step_phrases : phrase list} - val enc: t Json_encoding.encoding + type t = {title : string; steps : step list} - module Index: sig + val enc : t Json_encoding.encoding + module Index : sig type t = (id * string) list - val enc: t Json_encoding.encoding - + val enc : t Json_encoding.encoding end - end -module Tutorial: sig - +module Tutorial : sig type id = string - type code = { - code: string; - runnable: bool; - } + type code = {code : string; runnable : bool} type word = | Text of string | Code of code | Emph of text - | Image of { alt : string ; mime : string ; contents : bytes } + | Image of {alt : string; mime : string; contents : bytes} | Math of string - and text = - word list + and text = word list type phrase = | Paragraph of text | Enum of phrase list list | Code_block of code - type step = { - step_title: text; - step_contents: phrase list; - } - - type t = { - title: text; - steps: step list; - } + type step = {step_title : text; step_contents : phrase list} - val enc: t Json_encoding.encoding + type t = {title : text; steps : step list} - module Index: sig + val enc : t Json_encoding.encoding - type entry = { - name: string; - title: text; - } + module Index : sig + type entry = {name : string; title : text} - type series = { - series_title: string; - series_tutorials: entry list; - } + type series = {series_title : string; series_tutorials : entry list} type t = (id * series) list - val enc: t Json_encoding.encoding - + val enc : t Json_encoding.encoding end - end module Partition : sig type t = - { - not_graded : Token.t list; - bad_type : Token.t list; - partition_by_grade : - (int * - (((Token.t * string) list) Asak.Wtree.wtree list)) - list; - } - - val enc: t Json_encoding.encoding + { not_graded : Token.t list + ; bad_type : Token.t list + ; partition_by_grade : + (int * (Token.t * string) list Asak.Wtree.wtree list) list } + + val enc : t Json_encoding.encoding end module Playground : sig type id = string - type t = - { id : id ; - prelude : string ; - template : string ; - } + type t = {id : id; prelude : string; template : string} - val enc: t Json_encoding.encoding + val enc : t Json_encoding.encoding module Meta : sig - type t = - { - title: string; - short_description: string option; - } + type t = {title : string; short_description : string option} val default : string -> t - val enc: t Json_encoding.encoding + val enc : t Json_encoding.encoding end - module Index: sig - + module Index : sig type t = (id * Meta.t) list - val enc: t Json_encoding.encoding - + val enc : t Json_encoding.encoding end end diff --git a/src/state/learnocaml_store.ml b/src/state/learnocaml_store.ml index 06635abcb..eb2a254cd 100644 --- a/src/state/learnocaml_store.ml +++ b/src/state/learnocaml_store.ml @@ -8,7 +8,6 @@ open Lwt.Infix open Learnocaml_data - module J = Json_encoding let static_dir = ref (Filename.concat (Sys.getcwd ()) "www") @@ -17,20 +16,17 @@ let sync_dir = ref (Filename.concat (Sys.getcwd ()) "sync") module Json_codec = struct let decode enc s = - (match s with - | "" -> `O [] - | s -> Ezjsonm.from_string s) - |> J.destruct enc + (match s with "" -> `O [] | s -> Ezjsonm.from_string s) |> J.destruct enc let encode ?minify enc x = match J.construct enc x with - | `A _ | `O _ as json -> Ezjsonm.to_string ?minify json + | (`A _ | `O _) as json -> Ezjsonm.to_string ?minify json | `Null -> "" | _ -> assert false end + let get_from_file enc p = - Lwt_io.(with_file ~mode: Input p read) >|= - Json_codec.decode enc + Lwt_io.(with_file ~mode:Input p read) >|= Json_codec.decode enc let write_to_file enc s p = let open Lwt_io in @@ -42,10 +38,8 @@ let sanitise_path prefix subpath = | [] -> List.rev acc | "" :: rest -> resolve acc rest | "." :: rest -> resolve acc rest - | ".." :: rest -> - begin match acc with - | [] -> resolve [] rest - | _ :: acc -> resolve acc rest end + | ".." :: rest -> ( + match acc with [] -> resolve [] rest | _ :: acc -> resolve acc rest ) | name :: rest -> resolve (name :: acc) rest in String.concat Filename.dir_sep (prefix :: resolve [] subpath) @@ -56,133 +50,132 @@ let read_static_file path enc = let with_git_register = let dir_mutex = Lwt_utils.gen_mutex_table () in - fun dir (f: unit -> string list Lwt.t) -> + fun dir (f : unit -> string list Lwt.t) -> let git args () = - Lwt_process.exec ("", Array.of_list ("git"::"-C"::dir::args)) >>= function + Lwt_process.exec ("", Array.of_list ("git" :: "-C" :: dir :: args)) + >>= function | Unix.WEXITED 0 -> Lwt.return_unit | _ -> Lwt.fail_with ("git command failed: " ^ String.concat " " args) in - dir_mutex.Lwt_utils.with_lock dir @@ fun () -> - (if Sys.file_exists (Filename.concat dir ".git") then - git ["reset";"--hard"] () - else - git ["init"] () >>= - git ["config";"--local";"user.name";"Learn-OCaml user"] >>= - git ["config";"--local";"user.email";"none@learn-ocaml.org"]) >>= - f >>= fun files -> - git ("add"::"--"::files) () >>= - git ["commit";"--allow-empty";"-m";"Update"] >>= - git ["update-server-info"] - -let write ?(no_create=false) file ?(extra=[]) contents = + dir_mutex.Lwt_utils.with_lock dir + @@ fun () -> + ( if Sys.file_exists (Filename.concat dir ".git") then + git ["reset"; "--hard"] () + else + git ["init"] () + >>= git ["config"; "--local"; "user.name"; "Learn-OCaml user"] + >>= git ["config"; "--local"; "user.email"; "none@learn-ocaml.org"] ) + >>= f + >>= fun files -> + git ("add" :: "--" :: files) () + >>= git ["commit"; "--allow-empty"; "-m"; "Update"] + >>= git ["update-server-info"] + +let write ?(no_create = false) file ?(extra = []) contents = let dir = Filename.dirname file in - (if not (Sys.file_exists file) then - if no_create then Lwt.fail Not_found - else Lwt_utils.mkdir_p ~perm:0o700 dir - else Lwt.return_unit) + ( if not (Sys.file_exists file) then + if no_create then Lwt.fail Not_found else Lwt_utils.mkdir_p ~perm:0o700 dir + else Lwt.return_unit ) >>= fun () -> let file = Filename.basename file in - let write_file ?(flags=[Unix.O_TRUNC]) (fname, contents) = + let write_file ?(flags = [Unix.O_TRUNC]) (fname, contents) = let file = sanitise_path dir (String.split_on_char '/' fname) in - Lwt_utils.mkdir_p ~perm:0o700 (Filename.dirname file) >>= fun () -> - Lwt_io.(with_file file - ~flags:Unix.(O_WRONLY::O_NONBLOCK::O_CREAT::flags) - ~mode:Output - (fun chan -> write chan contents)) >|= fun () -> - fname + Lwt_utils.mkdir_p ~perm:0o700 (Filename.dirname file) + >>= fun () -> + Lwt_io.( + with_file file + ~flags:Unix.(O_WRONLY :: O_NONBLOCK :: O_CREAT :: flags) + ~mode:Output + (fun chan -> write chan contents)) + >|= fun () -> fname in let rec write_tmp () = let tmpfile = Printf.sprintf "%s.%07x.tmp" file (Random.int 0x0fffffff) in Lwt.catch (fun () -> write_file ~flags:[Unix.O_EXCL] (tmpfile, contents)) - (function Unix.Unix_error (Unix.EEXIST, _, _) -> write_tmp () - | e -> Lwt.fail e) + (function + | Unix.Unix_error (Unix.EEXIST, _, _) -> write_tmp () | e -> Lwt.fail e) in - with_git_register dir @@ fun () -> - write_tmp () >>= fun tmpfile -> + with_git_register dir + @@ fun () -> + write_tmp () + >>= fun tmpfile -> Lwt_unix.rename (Filename.concat dir tmpfile) (Filename.concat dir file) >>= fun () -> - Lwt_list.map_s write_file extra >>= fun extra -> - Lwt.return (file :: extra) - + Lwt_list.map_s write_file extra >>= fun extra -> Lwt.return (file :: extra) module Lesson = struct - module Index = struct - include Lesson.Index - let get () = - read_static_file Learnocaml_index.lesson_index_path enc - + let get () = read_static_file Learnocaml_index.lesson_index_path enc end - include (Lesson: module type of struct include Lesson end - with module Index := Index) - - let get id = - read_static_file (Learnocaml_index.lesson_path id) enc + include ( + Lesson : + module type of struct + include Lesson + end + with module Index := Index ) + let get id = read_static_file (Learnocaml_index.lesson_path id) enc end module Playground = struct - module Index = struct - include Playground.Index - let get () = - read_static_file Learnocaml_index.playground_index_path enc - + let get () = read_static_file Learnocaml_index.playground_index_path enc end - include (Playground: module type of struct include Playground end - with module Index := Index) - - let get id = - read_static_file (Learnocaml_index.playground_path id) enc + include ( + Playground : + module type of struct + include Playground + end + with module Index := Index ) + let get id = read_static_file (Learnocaml_index.playground_path id) enc end module Server = struct let get () = Lwt.catch - (fun () -> read_static_file Learnocaml_index.server_config_path Server.config_enc) + (fun () -> + read_static_file Learnocaml_index.server_config_path Server.config_enc + ) (fun e -> match e with - | Unix.Unix_error (Unix.ENOENT,_,_) -> - Lwt.return @@ Server.build_config Server.empty_preconfig - | e -> raise e - ) + | Unix.Unix_error (Unix.ENOENT, _, _) -> + Lwt.return @@ Server.build_config Server.empty_preconfig + | e -> raise e ) end module Tutorial = struct - module Index = struct - include Tutorial.Index - let get () = - read_static_file Learnocaml_index.tutorial_index_path enc - + let get () = read_static_file Learnocaml_index.tutorial_index_path enc end - include (Tutorial: module type of struct include Tutorial end - with module Index := Index) - - let get id = - read_static_file (Learnocaml_index.tutorial_path id) enc + include ( + Tutorial : + module type of struct + include Tutorial + end + with module Index := Index ) + let get id = read_static_file (Learnocaml_index.tutorial_path id) enc end module Exercise = struct - type id = Exercise.id let index = - ref (lazy ( - read_static_file Learnocaml_index.exercise_index_path Exercise.Index.enc - )) + ref + ( lazy + (read_static_file Learnocaml_index.exercise_index_path + Exercise.Index.enc) ) module Meta0 = struct include Exercise.Meta @@ -196,185 +189,189 @@ module Exercise = struct let store_file () = Filename.concat !sync_dir "exercises.json" - let tbl = lazy ( - let tbl = Hashtbl.create 223 in - Lwt.catch (fun () -> - get_from_file (J.list enc) (store_file ()) >|= fun l -> - List.iter (fun st -> Hashtbl.add tbl st.id st) l; - tbl) - @@ function - | Unix.Unix_error (Unix.ENOENT, _, _) -> Lwt.return tbl - | e -> Lwt.fail e - ) + let tbl = + lazy + (let tbl = Hashtbl.create 223 in + Lwt.catch (fun () -> + get_from_file (J.list enc) (store_file ()) + >|= fun l -> + List.iter (fun st -> Hashtbl.add tbl st.id st) l; + tbl ) + @@ function + | Unix.Unix_error (Unix.ENOENT, _, _) -> Lwt.return tbl + | e -> Lwt.fail e) let save () = - Lazy.force tbl >>= fun tbl -> - let l = Hashtbl.fold (fun _ s acc -> s::acc) tbl [] in + Lazy.force tbl + >>= fun tbl -> + let l = Hashtbl.fold (fun _ s acc -> s :: acc) tbl [] in let s = Json_codec.encode (J.list enc) l in write (store_file ()) s let get id = - Lazy.force tbl >>= fun tbl -> - try Lwt.return (Hashtbl.find tbl id) - with Not_found -> Meta0.get id >|= fun _ -> default id + Lazy.force tbl + >>= fun tbl -> + try Lwt.return (Hashtbl.find tbl id) with Not_found -> + Meta0.get id >|= fun _ -> default id let set = let mutex = Lwt_mutex.create () in fun x -> - Lwt_mutex.with_lock mutex @@ fun () -> - Lazy.force tbl >>= fun tbl -> - Hashtbl.replace tbl x.id x; - save () + Lwt_mutex.with_lock mutex + @@ fun () -> + Lazy.force tbl >>= fun tbl -> Hashtbl.replace tbl x.id x; save () let all () = - Lazy.force tbl >|= fun tbl -> - Hashtbl.fold (fun _ t acc -> t::acc) tbl [] + Lazy.force tbl + >|= fun tbl -> Hashtbl.fold (fun _ t acc -> t :: acc) tbl [] let is_open id token = - if Token.is_teacher token then Lwt.return `Open else - Lazy.force tbl >|= fun tbl -> - let assignments = - match Hashtbl.find_opt tbl id with - | None -> (default id).assignments - | Some ex -> ex.assignments - in - is_open_assignment token assignments - + if Token.is_teacher token then Lwt.return `Open + else + Lazy.force tbl + >|= fun tbl -> + let assignments = + match Hashtbl.find_opt tbl id with + | None -> (default id).assignments + | Some ex -> ex.assignments + in + is_open_assignment token assignments end module Meta = struct include Meta0 let get id = - get id >>= fun m -> - Status.get id >|= fun s -> + get id + >>= fun m -> + Status.get id + >|= fun s -> { m with - requirements = Status.skills_prereq m s; - focus = Status.skills_focus m s } - + requirements = Status.skills_prereq m s + ; focus = Status.skills_focus m s } end module Index = struct include Exercise.Index let get_from_index index = - Exercise.Index.mapk_exercises (fun id m k -> - Status.get id >>= fun s -> - { m with Meta.requirements = Status.skills_prereq m s; - Meta.focus = Status.skills_focus m s } - |> k) - index - Lwt.return - - let get () = - Lazy.force !index >>= get_from_index + Exercise.Index.mapk_exercises + (fun id m k -> + Status.get id + >>= fun s -> + { m with + Meta.requirements = Status.skills_prereq m s + ; Meta.focus = Status.skills_focus m s } + |> k ) + index Lwt.return + + let get () = Lazy.force !index >>= get_from_index let reload () = read_static_file Learnocaml_index.exercise_index_path Exercise.Index.enc >|= fun i -> index := lazy (Lwt.return i) - end - include (Exercise: module type of struct include Exercise end - with type id := id - and module Meta := Meta - and module Status := Status - and module Index := Index) + include ( + Exercise : + module type of struct + include Exercise + end + with type id := id + and module Meta := Meta + and module Status := Status + and module Index := Index ) let get id = Lwt.catch - (fun () -> read_static_file (Learnocaml_index.exercise_path id) - J.(tup3 Meta.enc enc (option float)) >>= fun (_, ex, _) -> - Lwt.return ex) - (function - | Unix.Unix_error _ -> Lwt.fail Not_found - | e -> Lwt.fail e) - + (fun () -> + read_static_file + (Learnocaml_index.exercise_path id) + J.(tup3 Meta.enc enc (option float)) + >>= fun (_, ex, _) -> Lwt.return ex ) + (function Unix.Unix_error _ -> Lwt.fail Not_found | e -> Lwt.fail e) end module Token = struct - include Token - let path token = - Filename.concat !sync_dir (Token.to_path token) + let path token = Filename.concat !sync_dir (Token.to_path token) let save_path token = Filename.concat (path token) "save.json" let find_save token = let save = save_path token in - Lwt_unix.file_exists save >>= function + Lwt_unix.file_exists save + >>= function | true -> Lwt.return_some save | false -> (* old layout: json stored directly as [path] instead of [path/save.json] *) let old_save = path token in - Lwt_unix.file_exists old_save >>= fun ex -> - if ex && not (Sys.is_directory old_save) then - Lwt.return_some old_save - else - Lwt.return_none + Lwt_unix.file_exists old_save + >>= fun ex -> + if ex && not (Sys.is_directory old_save) then Lwt.return_some old_save + else Lwt.return_none let exists token = find_save token >|= function None -> false | Some _ -> true let check_teacher token = - if is_teacher token then exists token - else Lwt.return_false + if is_teacher token then exists token else Lwt.return_false let create_gen rnd = let rec aux () = let token = rnd () in let file = save_path token in - Lwt_utils.mkdir_p ~perm:0o700 (Filename.dirname file) >>= fun () -> + Lwt_utils.mkdir_p ~perm:0o700 (Filename.dirname file) + >>= fun () -> Lwt.catch (fun () -> Lwt_io.with_file ~mode:Lwt_io.Output ~perm:0o700 file - ~flags:Unix.([O_WRONLY; O_NONBLOCK; O_CREAT; O_EXCL]) - (fun _chan -> Lwt.return token)) + ~flags:Unix.[O_WRONLY; O_NONBLOCK; O_CREAT; O_EXCL] + (fun _chan -> Lwt.return token) ) @@ function - | Unix.Unix_error (Unix.EEXIST, _, _) -> aux () - | e -> Lwt.fail e + | Unix.Unix_error (Unix.EEXIST, _, _) -> aux () | e -> Lwt.fail e in aux () - let register ?(allow_teacher=false) token = - if not allow_teacher && is_teacher token then + let register ?(allow_teacher = false) token = + if (not allow_teacher) && is_teacher token then Lwt.fail - (Invalid_argument "Registration of teacher token forbidden. \ - Logout and use a new teacher token?") + (Invalid_argument + "Registration of teacher token forbidden. Logout and use a new \ + teacher token?") else Lwt.catch (fun () -> Lwt_io.with_file ~mode:Lwt_io.Output ~perm:0o700 (save_path token) - ~flags:Unix.([O_WRONLY; O_NONBLOCK; O_CREAT; O_EXCL]) - (fun _chan -> Lwt.return_unit)) + ~flags:Unix.[O_WRONLY; O_NONBLOCK; O_CREAT; O_EXCL] + (fun _chan -> Lwt.return_unit) ) @@ function | Unix.Unix_error (Unix.EEXIST, _, _) -> Lwt.fail_with "token already exists" | e -> Lwt.fail e - let create_student () = - create_gen random + let create_student () = create_gen random let create_teacher () = create_gen random_teacher let delete token = let rec rec_rmdir d = - Lwt.catch (fun () -> - Lwt_unix.rmdir d >>= fun () -> + Lwt.catch + (fun () -> + Lwt_unix.rmdir d + >>= fun () -> let parent = Filename.dirname d in - if parent = d then Lwt.return_unit else rec_rmdir parent) + if parent = d then Lwt.return_unit else rec_rmdir parent ) (function | Unix.Unix_error (Unix.EINVAL, _, _) -> Lwt.return_unit | e -> Lwt.fail e) in - find_save token >>= function + find_save token + >>= function | None -> Lwt.return_unit - | Some f -> - Lwt_unix.unlink f >>= fun () -> - rec_rmdir (Filename.dirname f) + | Some f -> Lwt_unix.unlink f >>= fun () -> rec_rmdir (Filename.dirname f) module Index = struct - type nonrec t = t list let enc = J.(list enc) @@ -385,10 +382,11 @@ module Token = struct let rec scan f d acc = let rec aux s acc = Lwt.catch (fun () -> - Lwt_stream.get s >>= function + Lwt_stream.get s + >>= function | Some ("." | "..") -> aux s acc | Some x -> scan f (d / x) acc >>= aux s - | None -> Lwt.return acc) + | None -> Lwt.return acc ) @@ function | Unix.Unix_error (Unix.ENOTDIR, _, _) -> f d acc | Unix.Unix_error _ -> Lwt.return acc @@ -396,76 +394,72 @@ module Token = struct in aux (Lwt_unix.files_of_directory (base / d)) acc in - scan (fun d acc -> + scan + (fun d acc -> let d = - if Filename.basename d = "save.json" then Filename.dirname d - else d + if Filename.basename d = "save.json" then Filename.dirname d else d in let stok = String.map (function '/' | '\\' -> '-' | c -> c) d in - try Lwt.return (Token.parse stok :: acc) - with Failure _ -> Lwt.return acc - ) "" [] - + try Lwt.return (Token.parse stok :: acc) with Failure _ -> + Lwt.return acc ) + "" [] end - end module Save = struct - include Save let get token = - Token.find_save token >>= function - | Some save -> get_from_file (J.option enc) save - | None -> Lwt.return_none + Token.find_save token + >>= function + | Some save -> get_from_file (J.option enc) save | None -> Lwt.return_none let set token save = let file = Token.save_path token in - (Token.find_save token >>= function - | Some f when f <> file -> - (* save file uses an old layout, move it to preserve attrs *) - let tmp = f ^ ".tmp" in - Lwt_unix.rename f tmp >>= fun () -> - Lwt_utils.mkdir_p (Filename.dirname file) >>= fun () -> - Lwt_unix.rename tmp file - | _ -> Lwt.return_unit) + Token.find_save token + >>= (function + | Some f when f <> file -> + (* save file uses an old layout, move it to preserve attrs *) + let tmp = f ^ ".tmp" in + Lwt_unix.rename f tmp + >>= fun () -> + Lwt_utils.mkdir_p (Filename.dirname file) + >>= fun () -> Lwt_unix.rename tmp file + | _ -> Lwt.return_unit) >>= fun () -> let extra = - SMap.fold (fun ex ans acc -> + SMap.fold + (fun ex ans acc -> let filename = ex ^ ".ml" in let contents = - String.concat "\n" [ - Printf.sprintf "(* GRADE: % 02d%% *)" - (match ans.Answer.grade with Some g -> g | None -> 0); - ans.Answer.solution; - "" - ] + String.concat "\n" + [ Printf.sprintf "(* GRADE: % 02d%% *)" + (match ans.Answer.grade with Some g -> g | None -> 0) + ; ans.Answer.solution + ; "" ] in - (filename, contents) :: acc) - save.all_exercise_states - [] + (filename, contents) :: acc ) + save.all_exercise_states [] in - Lwt.catch (fun () -> + Lwt.catch + (fun () -> write ~no_create:(Token.is_teacher token) ~extra file - (Json_codec.encode ~minify:false enc save)) + (Json_codec.encode ~minify:false enc save) ) (function | Not_found -> Lwt.fail_with "Unregistered teacher token" | e -> Lwt.fail e) - end module Student = struct - open Student let get_saved token = - Save.get token >>= function - | None -> - Lwt.return (default token) + Save.get token + >>= function + | None -> Lwt.return (default token) | Some save -> - let nickname = match save.Save.nickname with - | "" -> None - | n -> Some n + let nickname = + match save.Save.nickname with "" -> None | n -> Some n in let results = SMap.map @@ -473,14 +467,13 @@ module Student = struct save.Save.all_exercise_states in let tags = SSet.empty in - (Token.find_save token >>= function - | None -> Lwt.return 0. - | Some f -> Lwt_unix.stat f >|= fun st -> st.Unix.st_ctime) - >|= fun creation_date -> - {token; nickname; results; creation_date; tags} + Token.find_save token + >>= (function + | None -> Lwt.return 0. + | Some f -> Lwt_unix.stat f >|= fun st -> st.Unix.st_ctime) + >|= fun creation_date -> {token; nickname; results; creation_date; tags} module Index = struct - include Index (* Results and nickname are stored as part of the student's save, only the @@ -488,14 +481,16 @@ module Student = struct let store_enc = J.(assoc (obj1 (dft "tags" (list string) []))) |> J.conv - (fun ttmap -> - Token.Map.fold (fun tok tags l -> - (Token.to_string tok, SSet.elements tags) :: l) - ttmap []) - (fun ttl -> - List.fold_left (fun map (tok, tags) -> - Token.Map.add (Token.parse tok) (SSet.of_list tags) map) - Token.Map.empty ttl) + (fun ttmap -> + Token.Map.fold + (fun tok tags l -> + (Token.to_string tok, SSet.elements tags) :: l ) + ttmap [] ) + (fun ttl -> + List.fold_left + (fun map (tok, tags) -> + Token.Map.add (Token.parse tok) (SSet.of_list tags) map ) + Token.Map.empty ttl ) let store_file () = Filename.concat !sync_dir "students.json" @@ -509,24 +504,26 @@ module Student = struct let map = lazy (load () >|= fun m -> ref m) let save () = - Lazy.force map >>= fun map -> + Lazy.force map + >>= fun map -> let s = Json_codec.encode store_enc !map in write (store_file ()) s let get_student map token = - Lwt.try_bind (fun () -> get_saved token) + Lwt.try_bind + (fun () -> get_saved token) (fun std -> - match Token.Map.find_opt token map with - | Some tags -> Lwt.return_some {std with tags} - | None -> Lwt.return_some std) + match Token.Map.find_opt token map with + | Some tags -> Lwt.return_some {std with tags} + | None -> Lwt.return_some std ) (fun e -> - Format.eprintf "[ERROR] Corrupt save, cannot load %s: %s@." - (Token.to_string token) - (Printexc.to_string e); - Lwt.return_none) + Format.eprintf "[ERROR] Corrupt save, cannot load %s: %s@." + (Token.to_string token) (Printexc.to_string e); + Lwt.return_none ) let get () = - Lazy.force map >>= fun map -> + Lazy.force map + >>= fun map -> Token.Index.get () >|= List.filter Token.is_student >>= Lwt_list.filter_map_p (get_student !map) @@ -534,22 +531,26 @@ module Student = struct let set = let map_mutex = Lwt_mutex.create () in fun l -> - Lwt_mutex.with_lock map_mutex @@ fun () -> - Lazy.force map >>= fun map -> - map := List.fold_left + Lwt_mutex.with_lock map_mutex + @@ fun () -> + Lazy.force map + >>= fun map -> + map := + List.fold_left (fun map std -> Token.Map.add std.token std.tags map) !map l; save () - end let get token = - Lazy.force Index.map >>= fun map -> - Index.get_student !map token + Lazy.force Index.map >>= fun map -> Index.get_student !map token let set std = Index.set [std] - include (Student: module type of struct include Student end - with module Index := Index) - + include ( + Student : + module type of struct + include Student + end + with module Index := Index ) end diff --git a/src/state/learnocaml_store.mli b/src/state/learnocaml_store.mli index f6bbd440b..6105cd49c 100644 --- a/src/state/learnocaml_store.mli +++ b/src/state/learnocaml_store.mli @@ -10,173 +10,204 @@ open Learnocaml_data (** {2 Configuration options} *) +val static_dir : string ref (** All static data accesses will be made relative to this directory *) -val static_dir: string ref +val sync_dir : string ref (** All mutable data access will be made relative to this directory *) -val sync_dir: string ref (** {2 Utility server-side conversion functions} *) (** Used both for file i/o and request handling *) -module Json_codec: Learnocaml_api.JSON_CODEC +module Json_codec : Learnocaml_api.JSON_CODEC + val get_from_file : 'a Json_encoding.encoding -> string -> 'a Lwt.t + val write_to_file : 'a Json_encoding.encoding -> 'a -> string -> unit Lwt.t (* [sanitise_path prefix subdir] simplifies "." and ".." references in [subdir], and returns the concatenation, but guaranteeing the result remains below [prefix] (not accounting for symlinks of course, this is purely syntaxical) *) -val sanitise_path: string -> string list -> string +val sanitise_path : string -> string list -> string (** {2 Static data} *) -module Lesson: sig +module Lesson : sig + module Index : sig + include module type of struct + include Lesson.Index + end - module Index: sig - include module type of struct include Lesson.Index end - val get: unit -> t Lwt.t + val get : unit -> t Lwt.t end - include module type of struct include Lesson end with module Index := Index - - val get: id -> t Lwt.t + include + module type of struct + include Lesson + end + with module Index := Index + val get : id -> t Lwt.t end -module Playground: sig +module Playground : sig + module Index : sig + include module type of struct + include Playground.Index + end - module Index: sig - include module type of struct include Playground.Index end - val get: unit -> t Lwt.t + val get : unit -> t Lwt.t end - include module type of struct include Playground end with module Index := Index - - val get: id -> t Lwt.t + include + module type of struct + include Playground + end + with module Index := Index + val get : id -> t Lwt.t end module Server : sig val get : unit -> Server.config Lwt.t end -module Tutorial: sig +module Tutorial : sig + module Index : sig + include module type of struct + include Tutorial.Index + end - module Index: sig - include module type of struct include Tutorial.Index end - val get: unit -> t Lwt.t + val get : unit -> t Lwt.t end - include module type of struct include Tutorial end with module Index := Index - - val get: id -> t Lwt.t + include + module type of struct + include Tutorial + end + with module Index := Index + val get : id -> t Lwt.t end -module Exercise: sig +module Exercise : sig + module Meta : sig + include module type of struct + include Exercise.Meta + end - module Meta: sig - include module type of struct include Exercise.Meta end - val get: Exercise.id -> t Lwt.t + val get : Exercise.id -> t Lwt.t end - module Index: sig - include module type of struct include Exercise.Index end - val get_from_index: t -> t Lwt.t - val get: unit -> t Lwt.t - val reload: unit -> unit Lwt.t - end + module Index : sig + include module type of struct + include Exercise.Index + end - module Status: sig - include module type of struct include Exercise.Status end + val get_from_index : t -> t Lwt.t - val is_open: - Exercise.id -> Token.t -> - [`Open | `Closed | `Deadline of float] Lwt.t - val get: Exercise.id -> t Lwt.t - val set: t -> unit Lwt.t - val all: unit -> t list Lwt.t + val get : unit -> t Lwt.t + + val reload : unit -> unit Lwt.t end - include module type of struct include Exercise end - with module Meta := Meta - and module Status := Status - and module Index := Index + module Status : sig + include module type of struct + include Exercise.Status + end - val get: id -> t Lwt.t + val is_open : + Exercise.id -> Token.t -> [`Open | `Closed | `Deadline of float] Lwt.t -end + val get : Exercise.id -> t Lwt.t + val set : t -> unit Lwt.t + val all : unit -> t list Lwt.t + end -(** {2 Dynamic data} *) + include + module type of struct + include Exercise + end + with module Meta := Meta + and module Status := Status + and module Index := Index -module Token: sig + val get : id -> t Lwt.t +end - include module type of struct include Token end +(** {2 Dynamic data} *) + +module Token : sig + include module type of struct + include Token + end + val create_student : unit -> t Lwt.t (** Initialise and register a new student token *) - val create_student: unit -> t Lwt.t + val create_teacher : unit -> t Lwt.t (** Initialise and register a new student token *) - val create_teacher: unit -> t Lwt.t + val register : ?allow_teacher:bool -> t -> unit Lwt.t (** Registers the given token. By default, only registration of student tokens is allowed. [Failure] is raised if the token exists already. *) - val register: ?allow_teacher:bool -> t -> unit Lwt.t + val exists : t -> bool Lwt.t (** Check if the token has been registered *) - val exists: t -> bool Lwt.t - val delete: t -> unit Lwt.t + val delete : t -> unit Lwt.t + val check_teacher : t -> bool Lwt.t (** True for registered teacher tokens only *) - val check_teacher: t -> bool Lwt.t - - module Index: sig + module Index : sig type nonrec t = t list - val enc: t Json_encoding.encoding - - val get: unit -> t Lwt.t + val enc : t Json_encoding.encoding + val get : unit -> t Lwt.t end - end -module Save: sig - - include module type of struct include Save end +module Save : sig + include module type of struct + include Save + end - val get: Token.t -> t option Lwt.t + val get : Token.t -> t option Lwt.t + val set : Token.t -> t -> unit Lwt.t (** Writes the given save to disk. Note: writing to an unregistered teacher token will be rejected with [Failure], writing to an unregistered student token registers it. *) - val set: Token.t -> t -> unit Lwt.t - end -module Student: sig +module Student : sig + module Index : sig + include module type of struct + include Student.Index + end - module Index: sig - include module type of struct include Student.Index end - val get: unit -> t Lwt.t + val get : unit -> t Lwt.t + val set : Student.t list -> unit Lwt.t (** Does not affect the registered students absent from the list. Only the tags can be updated this way, the rest needs to be set through [Save.set] *) - val set: Student.t list -> unit Lwt.t end - include module type of struct include Student end with module Index := Index + include + module type of struct + include Student + end + with module Index := Index - val get: student token -> t option Lwt.t + val get : student token -> t option Lwt.t + val set : t -> unit Lwt.t (** Only updates the tags at the moment, the rest is stored with [Save.set]. Use [Index.set] to set multiple students at once. *) - val set: t -> unit Lwt.t - end diff --git a/src/toplevel/learnocaml_toplevel.ml b/src/toplevel/learnocaml_toplevel.ml index 20fdfa239..14cc04d8a 100644 --- a/src/toplevel/learnocaml_toplevel.ml +++ b/src/toplevel/learnocaml_toplevel.ml @@ -9,7 +9,8 @@ open Js_utils open Tyxml_js -let (>>=) = Lwt.(>>=) +let ( >>= ) = Lwt.( >>= ) + (* let (>|=) = Lwt.(>|=) * * let map_option f = function @@ -19,24 +20,25 @@ let (>>=) = Lwt.(>>=) * * type 'a result = Success of 'a | Timeout of float *) -type t = { - timeout_delay: float; - mutable timeout_prompt: t -> unit Lwt.t; - mutable current_timeout_prompt: unit Lwt.t; - flood_limit: int; - mutable flood_prompt: t -> Html_types.nmtoken -> (unit -> int) -> bool Lwt.t; - mutable current_flood_prompt: unit Lwt.t; - flood_reset: t -> unit; - worker: Learnocaml_toplevel_worker_caller.t; - container: [ `Div ] Html5.elt; - oldify: bool; - mutable status: [ `Reset of (unit Lwt.t * unit Lwt.u) | `Execute of unit Lwt.t | `Idle ] ; - mutable on_enable_input: t -> unit; - mutable on_disable_input: t -> unit; - mutable disabled : int; - output: Learnocaml_toplevel_output.output; - input: Learnocaml_toplevel_input.input; -} +type t = + { timeout_delay : float + ; mutable timeout_prompt : t -> unit Lwt.t + ; mutable current_timeout_prompt : unit Lwt.t + ; flood_limit : int + ; mutable flood_prompt : + t -> Html_types.nmtoken -> (unit -> int) -> bool Lwt.t + ; mutable current_flood_prompt : unit Lwt.t + ; flood_reset : t -> unit + ; worker : Learnocaml_toplevel_worker_caller.t + ; container : [`Div] Html5.elt + ; oldify : bool + ; mutable status : + [`Reset of unit Lwt.t * unit Lwt.u | `Execute of unit Lwt.t | `Idle] + ; mutable on_enable_input : t -> unit + ; mutable on_disable_input : t -> unit + ; mutable disabled : int + ; output : Learnocaml_toplevel_output.output + ; input : Learnocaml_toplevel_input.input } (* let set_timeout_prompt t f = t.timeout_prompt <- f * let set_flood_prompt t f = t.flood_prompt <- f @@ -55,26 +57,23 @@ type t = { * String.sub s !start (!stop - !start + 1) *) let disable_input top = - top.disabled <- top.disabled + 1 ; - if top.disabled = 1 then begin - top.on_disable_input top ; - Learnocaml_toplevel_input.disable top.input - end + top.disabled <- top.disabled + 1; + if top.disabled = 1 then ( + top.on_disable_input top; + Learnocaml_toplevel_input.disable top.input ) let enable_input top = - top.disabled <- top.disabled - 1 ; - if top.disabled = 0 then begin - top.on_enable_input top ; - Learnocaml_toplevel_input.enable top.input - end + top.disabled <- top.disabled - 1; + if top.disabled = 0 then ( + top.on_enable_input top; + Learnocaml_toplevel_input.enable top.input ) -let scroll { output; _ } = - Learnocaml_toplevel_output.scroll output +let scroll {output; _} = Learnocaml_toplevel_output.scroll output -let clear { output; _ } = - Learnocaml_toplevel_output.clear output ; +let clear {output; _} = + Learnocaml_toplevel_output.clear output; Learnocaml_toplevel_output.output_stdout output - [%i"The toplevel has been cleared.\n"] + [%i "The toplevel has been cleared.\n"] (* let never_ending = * let t = fst (Lwt.wait ()) in @@ -84,301 +83,324 @@ let wait_for_prompts top = Lwt.join [ Lwt.catch (fun () -> top.current_timeout_prompt) - Lwt.(function Canceled -> return () | exn -> fail exn) ; - Lwt.catch + Lwt.(function Canceled -> return () | exn -> fail exn) + ; Lwt.catch (fun () -> top.current_flood_prompt) Lwt.(function Canceled -> return () | exn -> fail exn) ] let start_timeout top _name timeout = - Lwt.cancel top.current_timeout_prompt ; + Lwt.cancel top.current_timeout_prompt; match timeout with | Some timeout -> timeout top | None -> - Lwt_js.sleep top.timeout_delay >>= fun () -> - wait_for_prompts top >>= fun () -> - top.current_timeout_prompt <- top.timeout_prompt top ; + Lwt_js.sleep top.timeout_delay + >>= fun () -> + wait_for_prompts top + >>= fun () -> + top.current_timeout_prompt <- top.timeout_prompt top; top.current_timeout_prompt let input_focus top f = - f () >>= fun r -> Learnocaml_toplevel_input.focus top.input; Lwt.return r + f () + >>= fun r -> + Learnocaml_toplevel_input.focus top.input; + Lwt.return r let reset_with_timeout top ?timeout () = - input_focus top @@ fun () -> + input_focus top + @@ fun () -> match top.status with | `Reset (t, _) -> t | `Idle -> let t, u = Lwt.wait () in - Lwt.cancel top.current_timeout_prompt ; - Lwt.cancel top.current_flood_prompt ; - wait_for_prompts top >>= fun () -> - top.status <- `Reset (t, u) ; + Lwt.cancel top.current_timeout_prompt; + Lwt.cancel top.current_flood_prompt; + wait_for_prompts top + >>= fun () -> + top.status <- `Reset (t, u); let timeout () = start_timeout top "reset" timeout in disable_input top; - Learnocaml_toplevel_worker_caller.reset ~timeout top.worker () >>= fun () -> - t + Learnocaml_toplevel_worker_caller.reset ~timeout top.worker () + >>= fun () -> t | `Execute task -> let t, u = Lwt.wait () in - Lwt.cancel top.current_timeout_prompt ; - Lwt.cancel top.current_flood_prompt ; - wait_for_prompts top >>= fun () -> - top.status <- `Reset (t, u) ; + Lwt.cancel top.current_timeout_prompt; + Lwt.cancel top.current_flood_prompt; + wait_for_prompts top + >>= fun () -> + top.status <- `Reset (t, u); let timeout () = start_timeout top "reset" timeout in disable_input top; - Learnocaml_toplevel_worker_caller.reset ~timeout top.worker () >>= fun () -> - t >>= fun () -> - Lwt.cancel task ; - Lwt.return () + Learnocaml_toplevel_worker_caller.reset ~timeout top.worker () + >>= fun () -> t >>= fun () -> Lwt.cancel task; Lwt.return () let reset top = let timeout _ = Lwt_js.sleep 2. in reset_with_timeout top ~timeout () let protect_execution top exec = - input_focus top @@ fun () -> - wait_for_prompts top >>= fun () -> + input_focus top + @@ fun () -> + wait_for_prompts top + >>= fun () -> match top.status with | `Reset _ | `Execute _ -> Lwt.fail_invalid_arg "Learnocaml_toplevel.protect_execution" | `Idle -> let t, u = Lwt.task () in - top.status <- `Execute t ; + top.status <- `Execute t; disable_input top; - top.flood_reset top ; - let thread = t >>= fun () -> + top.flood_reset top; + let thread = + t + >>= fun () -> Lwt.catch (fun () -> - exec () >>= fun res -> - match top.status with - | `Execute t' -> - assert (t == t') ; - top.status <- `Idle; - wait_for_prompts top >>= fun () -> - enable_input top; - Lwt.return res - | `Idle | `Reset _ -> - (* The task successfully ended between a reset order + exec () + >>= fun res -> + match top.status with + | `Execute t' -> + assert (t == t'); + top.status <- `Idle; + wait_for_prompts top + >>= fun () -> enable_input top; Lwt.return res + | `Idle | `Reset _ -> + (* The task successfully ended between a reset order and its ack, we fake its cancellation. *) - Lwt.fail Lwt.Canceled) + Lwt.fail Lwt.Canceled ) (function | Lwt.Canceled -> enable_input top; - begin match top.status with - | `Reset (t, _) -> t - | _ -> Lwt.return () - end >>= fun () -> Lwt.fail Lwt.Canceled - | exn -> - enable_input top; - Lwt.fail exn) in - Lwt.wakeup u () ; - thread + ( match top.status with + | `Reset (t, _) -> t + | _ -> Lwt.return () ) + >>= fun () -> Lwt.fail Lwt.Canceled + | exn -> enable_input top; Lwt.fail exn) + in + Lwt.wakeup u (); thread let execute_phrase top ?timeout content = - input_focus top @@ fun () -> + input_focus top + @@ fun () -> let phrase = Learnocaml_toplevel_output.phrase () in let pp_code = Learnocaml_toplevel_output.output_code ~phrase top.output in - let pp_answer = Learnocaml_toplevel_output.output_answer ~phrase top.output in + let pp_answer = + Learnocaml_toplevel_output.output_answer ~phrase top.output + in let t = - Learnocaml_toplevel_worker_caller.execute - top.worker ~pp_code ~pp_answer ~print_outcome:true content in - Lwt.pick [ - (Lwt.protected t >>= fun _ -> Lwt.return_unit) ; - (start_timeout top "execute" timeout >>= fun () -> - let timeout _ = Lwt.return () in - reset_with_timeout top ~timeout ()); - ] >>= fun () -> - t >>= fun result -> - let warnings, result = match result with - | Toploop_results.Ok (result, warnings) -> warnings, result + Learnocaml_toplevel_worker_caller.execute top.worker ~pp_code ~pp_answer + ~print_outcome:true content + in + Lwt.pick + [ (Lwt.protected t >>= fun _ -> Lwt.return_unit) + ; ( start_timeout top "execute" timeout + >>= fun () -> + let timeout _ = Lwt.return () in + reset_with_timeout top ~timeout () ) ] + >>= fun () -> + t + >>= fun result -> + let warnings, result = + match result with + | Toploop_results.Ok (result, warnings) -> (warnings, result) | Toploop_results.Error (error, warnings) -> - Learnocaml_toplevel_output.output_error ~phrase top.output error ; - warnings, false in + Learnocaml_toplevel_output.output_error ~phrase top.output error; + (warnings, false) + in List.iter (Learnocaml_toplevel_output.output_warning ~phrase top.output) - warnings ; + warnings; Lwt.return result -let execute top = - Learnocaml_toplevel_input.execute top.input +let execute top = Learnocaml_toplevel_input.execute top.input -let go_backward top = - Learnocaml_toplevel_input.go_backward top.input +let go_backward top = Learnocaml_toplevel_input.go_backward top.input -let go_forward top = - Learnocaml_toplevel_input.go_forward top.input +let go_forward top = Learnocaml_toplevel_input.go_forward top.input let check top code = - protect_execution top @@ fun () -> - Learnocaml_toplevel_worker_caller.check top.worker code + protect_execution top + @@ fun () -> Learnocaml_toplevel_worker_caller.check top.worker code let set_checking_environment top = - protect_execution top @@ fun () -> - Learnocaml_toplevel_worker_caller.set_checking_environment top.worker >>= fun _ -> - Lwt.return () + protect_execution top + @@ fun () -> + Learnocaml_toplevel_worker_caller.set_checking_environment top.worker + >>= fun _ -> Lwt.return () let execute_phrase top ?timeout content = - protect_execution top @@ fun () -> - execute_phrase top ?timeout content + protect_execution top @@ fun () -> execute_phrase top ?timeout content let load top ?(print_outcome = true) ?timeout ?message content = let phrase = Learnocaml_toplevel_output.phrase () in - protect_execution top @@ fun () -> - begin match message with - | None -> () - | Some message -> - Learnocaml_toplevel_output.output_code ~phrase top.output - ("(* " ^ message ^ "*)") - end ; + protect_execution top + @@ fun () -> + ( match message with + | None -> () + | Some message -> + Learnocaml_toplevel_output.output_code ~phrase top.output + ("(* " ^ message ^ "*)") ); let pp_answer = if print_outcome then Learnocaml_toplevel_output.output_answer ~phrase top.output - else - ignore in + else ignore + in let t = - Learnocaml_toplevel_worker_caller.use_string - top.worker ~pp_answer ~print_outcome content in - Lwt.pick [ - (Lwt.protected t >>= fun _ -> Lwt.return_unit) ; - (start_timeout top "load" timeout >>= fun () -> - reset top); - ] >>= fun () -> - t >>= fun result -> - let warnings, result = match result with - | Toploop_results.Ok (result, warnings) -> warnings, result + Learnocaml_toplevel_worker_caller.use_string top.worker ~pp_answer + ~print_outcome content + in + Lwt.pick + [ (Lwt.protected t >>= fun _ -> Lwt.return_unit) + ; (start_timeout top "load" timeout >>= fun () -> reset top) ] + >>= fun () -> + t + >>= fun result -> + let warnings, result = + match result with + | Toploop_results.Ok (result, warnings) -> (warnings, result) | Toploop_results.Error (error, warnings) -> - Learnocaml_toplevel_output.output_error top.output error ; - warnings, false in - List.iter - (Learnocaml_toplevel_output.output_warning top.output) - warnings ; + Learnocaml_toplevel_output.output_error top.output error; + (warnings, false) + in + List.iter (Learnocaml_toplevel_output.output_warning top.output) warnings; Lwt.return result -let make_timeout_popup - ?(countdown = 10) - ?(refill_step = 10) - ?(on_show = (fun () -> ())) - () { container; _ } = +let make_timeout_popup ?(countdown = 10) ?(refill_step = 10) + ?(on_show = fun () -> ()) () {container; _} = let open Tyxml_js.Html5 in let t0 = Sys.time () in let countdown = ref countdown in let btn_continue = - let label = Format.asprintf [%if"%d seconds!"] refill_step in - button [ txt label ] in - let btn_stop = - button [ txt [%i"Kill it!"] ] in - Manip.Ev.onclick btn_continue - (fun _ -> countdown := !countdown + refill_step ; true) ; - Manip.Ev.onclick btn_stop - (fun _ -> countdown := 0 ; true) ; + let label = Format.asprintf [%if "%d seconds!"] refill_step in + button [txt label] + in + let btn_stop = button [txt [%i "Kill it!"]] in + Manip.Ev.onclick btn_continue (fun _ -> + countdown := !countdown + refill_step; + true ); + Manip.Ev.onclick btn_stop (fun _ -> + countdown := 0; + true ); let clock_span = span [] in let countdown_span = span [] in let dialog = - div ~a: [ a_class [ "dialog-container" ] ] - [ div ~a: [ a_class [ "dialog" ] ] - [ h1 [ txt [%i"Infinite loop?"] ] ; - div ~a: [ a_class [ "message" ] ] - [ txt [%i"The toplevel has not been responding for "] ; - clock_span ; - txt [%i" seconds."] ; - br () ; - txt [%i"It will be killed in "] ; - countdown_span ; - txt [%i" seconds."] ] ; - div ~a: [ a_class [ "buttons" ] ] - [ btn_continue ; btn_stop ] ] ] in + div + ~a:[a_class ["dialog-container"]] + [ div + ~a:[a_class ["dialog"]] + [ h1 [txt [%i "Infinite loop?"]] + ; div + ~a:[a_class ["message"]] + [ txt [%i "The toplevel has not been responding for "] + ; clock_span + ; txt [%i " seconds."] + ; br () + ; txt [%i "It will be killed in "] + ; countdown_span + ; txt [%i " seconds."] ] + ; div ~a:[a_class ["buttons"]] [btn_continue; btn_stop] ] ] + in Lwt.catch (fun () -> - Manip.appendChild container dialog ; - on_show () ; - let rec loop () = - let elapsed = int_of_float (Sys.time () -. t0) in - Manip.replaceChildren clock_span - [ txt (string_of_int elapsed) ] ; - Manip.replaceChildren countdown_span - [ txt (string_of_int (!countdown - elapsed)) ] ; - if elapsed >= !countdown then begin - Manip.removeChild container dialog ; - Lwt.return () - end else - Lwt_js.sleep 0.2 >>= loop in - loop ()) + Manip.appendChild container dialog; + on_show (); + let rec loop () = + let elapsed = int_of_float (Sys.time () -. t0) in + Manip.replaceChildren clock_span [txt (string_of_int elapsed)]; + Manip.replaceChildren countdown_span + [txt (string_of_int (!countdown - elapsed))]; + if elapsed >= !countdown then ( + Manip.removeChild container dialog; + Lwt.return () ) + else Lwt_js.sleep 0.2 >>= loop + in + loop () ) (fun exn -> - (try Manip.removeChild container dialog with _ -> ()) ; - Lwt.fail exn) + (try Manip.removeChild container dialog with _ -> ()); + Lwt.fail exn ) -let make_flood_popup - ?(on_show = (fun () -> ())) - () { container; _ } name amount = +let make_flood_popup ?(on_show = fun () -> ()) () {container; _} name amount = let open Tyxml_js.Html5 in let answer = ref None in - let btn_continue = - button [ txt [%i"Show anyway!"] ] in - let btn_stop = - button [ txt [%i"Hide output!"] ] in - Manip.Ev.onclick btn_continue - (fun _ -> answer := Some false ; true) ; - Manip.Ev.onclick btn_stop - (fun _ -> answer := Some true ; true) ; + let btn_continue = button [txt [%i "Show anyway!"]] in + let btn_stop = button [txt [%i "Hide output!"]] in + Manip.Ev.onclick btn_continue (fun _ -> + answer := Some false; + true ); + Manip.Ev.onclick btn_stop (fun _ -> + answer := Some true; + true ); let qty_span = span [] in let dialog = - div ~a: [ a_class [ "dialog-container" ] ] - [ div ~a: [ a_class [ "dialog" ] ] - [ h1 [ txt [%i"Flooded output!"] ] ; - div ~a: [ a_class [ "message" ] ] - [ txt (Printf.sprintf - [%if"Your code is flooding the %s channel."] name) ; - br (); - txt [%i"It has already printed "] ; - qty_span ; - txt [%i" bytes."] ] ; - div ~a: [ a_class [ "buttons" ] ] - [ btn_continue ; btn_stop ] ] ] in - Manip.appendChild container dialog ; - on_show () ; + div + ~a:[a_class ["dialog-container"]] + [ div + ~a:[a_class ["dialog"]] + [ h1 [txt [%i "Flooded output!"]] + ; div + ~a:[a_class ["message"]] + [ txt + (Printf.sprintf [%if "Your code is flooding the %s channel."] + name) + ; br () + ; txt [%i "It has already printed "] + ; qty_span + ; txt [%i " bytes."] ] + ; div ~a:[a_class ["buttons"]] [btn_continue; btn_stop] ] ] + in + Manip.appendChild container dialog; + on_show (); let rec loop () = - Manip.replaceChildren qty_span - [ txt (string_of_int (amount ())) ] ; + Manip.replaceChildren qty_span [txt (string_of_int (amount ()))]; match !answer with | Some ans -> - Manip.removeChild container dialog ; + Manip.removeChild container dialog; Lwt.return ans - | None -> - Lwt_js.sleep 0.2 >>= loop in - Lwt.catch - loop - (fun exn -> - Manip.removeChild container dialog ; - Lwt.fail exn) + | None -> Lwt_js.sleep 0.2 >>= loop + in + Lwt.catch loop (fun exn -> + Manip.removeChild container dialog; + Lwt.fail exn ) let wrap_flusher_to_prevent_flood top name hook real = let flooded = ref 0 in - hook := fun s -> - real s ; - let total = !flooded + String.length s in - if total >= top.flood_limit then begin - let buf = Buffer.create top.flood_limit in - hook := (fun s -> try flooded := !flooded + String.length s ; Buffer.add_string buf s with _ -> ()) ; - flooded := total ; - Lwt.async @@ fun () -> - Lwt.catch - (fun () -> - wait_for_prompts top >>= fun () -> - top.current_flood_prompt <- - (top.flood_prompt top name (fun () -> !flooded) >>= function + hook := + fun s -> + real s; + let total = !flooded + String.length s in + if total >= top.flood_limit then ( + let buf = Buffer.create top.flood_limit in + (hook := + fun s -> + try + flooded := !flooded + String.length s; + Buffer.add_string buf s + with _ -> ()); + flooded := total; + Lwt.async + @@ fun () -> + Lwt.catch + (fun () -> + wait_for_prompts top + >>= fun () -> + top.current_flood_prompt + <- ( top.flood_prompt top name (fun () -> !flooded) + >>= function | true -> - real (Printf.sprintf [%if"\nInterrupted output channel %s.\n"] name) ; - hook := ignore ; + real + (Printf.sprintf [%if "\nInterrupted output channel %s.\n"] + name); + hook := ignore; Lwt.return () | false -> - real (Buffer.contents buf) ; - hook := real ; - Lwt.return ()) ; - top.current_flood_prompt) - (fun _exn -> - hook := ignore ; - Lwt.return ()) - end else begin - flooded := total - end + real (Buffer.contents buf); + hook := real; + Lwt.return () ); + top.current_flood_prompt ) + (fun _exn -> + hook := ignore; + Lwt.return () ) ) + else flooded := total let load_pp err top pps = let prelude_pp = @@ -387,146 +409,129 @@ let load_pp err top pps = in let rec loading = function | [] -> Lwt.return_unit - | code :: cs -> - load ~print_outcome:false top code - >>= (fun _ -> loading cs) + | code :: cs -> load ~print_outcome:false top code >>= fun _ -> loading cs in let pps = List.map (fun pp -> Format.sprintf "#install_printer %s;;" pp) pps in - err >>= (fun _ -> loading (prelude_pp::pps)) + err >>= fun _ -> loading (prelude_pp :: pps) let welcome_phrase () = - [%i"Printf.printf \"Welcome to OCaml %s\\n%!\" (Sys.ocaml_version);\n\ - print_endline \" - type your OCaml phrase in the box below and press [Enter]\";\n\ - print_endline \" - use [Shift-Enter] to break lines without triggering execution\";\n\ - print_endline \" - use [Ctrl-\\xe2\\x86\\x91] once to reuse the previous entry\";\n\ - print_endline \" - use [Ctrl-\\xe2\\x86\\x91] / [Ctrl-\\xe2\\x86\\x93] \ - to navigate through history\" ;;"] - (* U+2191 upwards arrow, U+2193 downwards arrow*) - -let create - ?worker_js_file - ?(timeout_delay = 5.) - ~timeout_prompt - ?(flood_limit = 8000) - ~flood_prompt - ?after_init + [%i + "Printf.printf \"Welcome to OCaml %s\\n%!\" (Sys.ocaml_version);\n\ + print_endline \" - type your OCaml phrase in the box below and press \ + [Enter]\";\n\ + print_endline \" - use [Shift-Enter] to break lines without triggering \ + execution\";\n\ + print_endline \" - use [Ctrl-\\xe2\\x86\\x91] once to reuse the previous \ + entry\";\n\ + print_endline \" - use [Ctrl-\\xe2\\x86\\x91] / [Ctrl-\\xe2\\x86\\x93] \ + to navigate through history\" ;;"] + +(* U+2191 upwards arrow, U+2193 downwards arrow*) + +let create ?worker_js_file ?(timeout_delay = 5.) ~timeout_prompt + ?(flood_limit = 8000) ~flood_prompt ?after_init ?(input_sizing = - { Learnocaml_toplevel_input.line_height = 18 ; - min_lines = 1 ; max_lines = 6 }) - ?on_resize - ?(on_disable_input = fun _ -> ()) - ?(on_enable_input = fun _ -> ()) - ?history - ?(oldify = true) - ?(display_welcome = true) - ~container () = - (match get_lang() with Some l -> Ocplib_i18n.set_lang l | None -> ()); + {Learnocaml_toplevel_input.line_height = 18; min_lines = 1; max_lines = 6}) + ?on_resize ?(on_disable_input = fun _ -> ()) + ?(on_enable_input = fun _ -> ()) ?history ?(oldify = true) + ?(display_welcome = true) ~container () = + (match get_lang () with Some l -> Ocplib_i18n.set_lang l | None -> ()); let output_div = Html5.div [] in let input_div = Html5.div [] in Manip.appendChild container output_div; Manip.appendChild container input_div; let output = - Learnocaml_toplevel_output.setup - ?on_resize - ~container:output_div - () in + Learnocaml_toplevel_output.setup ?on_resize ~container:output_div () + in let execute_hook = ref (fun _code -> assert false) in let input = - Learnocaml_toplevel_input.setup - ~sizing: input_sizing - ~execute: (fun code -> !execute_hook code) - ~container:input_div - ?on_resize - ?history - () in + Learnocaml_toplevel_input.setup ~sizing:input_sizing + ~execute:(fun code -> !execute_hook code) + ~container:input_div ?on_resize ?history () + in let pp_stdout_hook = ref ignore in let pp_stdout s = !pp_stdout_hook s in let pp_stderr_hook = ref ignore in let pp_stderr s = !pp_stderr_hook s in let flood_reset top = let phrase = Learnocaml_toplevel_output.phrase () in - Lwt.cancel top.current_flood_prompt ; - wrap_flusher_to_prevent_flood top - "stdout" pp_stdout_hook - (Learnocaml_toplevel_output.output_stdout ~phrase output) ; - wrap_flusher_to_prevent_flood top - "stderr" pp_stderr_hook - (Learnocaml_toplevel_output.output_stderr ~phrase output) in - Learnocaml_toplevel_worker_caller.create - ?js_file:worker_js_file - ~pp_stdout ~pp_stderr () >>= fun worker -> - let top = { - timeout_prompt; - current_timeout_prompt = Lwt.return (); - timeout_delay; - flood_prompt; - current_flood_prompt = Lwt.return (); - flood_limit; - flood_reset; - worker; - container; - oldify; - status = `Reset (Lwt.wait ()); - on_enable_input; - on_disable_input; - disabled = 1; - input; - output; - } in - flood_reset top ; - execute_hook := - (fun code -> Lwt.async @@ fun () -> - Lwt.catch - (fun () -> execute_phrase top code) - (function - | Lwt.Canceled -> Lwt.return true - | exn -> Lwt.fail exn )) ; + Lwt.cancel top.current_flood_prompt; + wrap_flusher_to_prevent_flood top "stdout" pp_stdout_hook + (Learnocaml_toplevel_output.output_stdout ~phrase output); + wrap_flusher_to_prevent_flood top "stderr" pp_stderr_hook + (Learnocaml_toplevel_output.output_stderr ~phrase output) + in + Learnocaml_toplevel_worker_caller.create ?js_file:worker_js_file ~pp_stdout + ~pp_stderr () + >>= fun worker -> + let top = + { timeout_prompt + ; current_timeout_prompt = Lwt.return () + ; timeout_delay + ; flood_prompt + ; current_flood_prompt = Lwt.return () + ; flood_limit + ; flood_reset + ; worker + ; container + ; oldify + ; status = `Reset (Lwt.wait ()) + ; on_enable_input + ; on_disable_input + ; disabled = 1 + ; input + ; output } + in + flood_reset top; + (execute_hook := + fun code -> + Lwt.async + @@ fun () -> + Lwt.catch + (fun () -> execute_phrase top code) + (function Lwt.Canceled -> Lwt.return true | exn -> Lwt.fail exn)); let first_time = ref true in let after_init top = - if !first_time || not oldify then - Learnocaml_toplevel_output.clear output - else - Learnocaml_toplevel_output.oldify output; - enable_input top ; + if !first_time || not oldify then Learnocaml_toplevel_output.clear output + else Learnocaml_toplevel_output.oldify output; + enable_input top; top.flood_reset top; - begin match top.status with - | `Reset (_, u) -> Lwt.wakeup u () - | `Idle | `Execute _ -> assert false - end ; + ( match top.status with + | `Reset (_, u) -> Lwt.wakeup u () + | `Idle | `Execute _ -> assert false ); top.status <- `Idle; - begin if display_welcome && (!first_time || not oldify) then - Learnocaml_toplevel_worker_caller.execute - ~pp_answer: (fun _ -> ()) - ~print_outcome: false - worker (welcome_phrase ()) >>= fun _ -> - Lwt.return () - else Lwt.return () - end >>= fun _ -> + ( if display_welcome && (!first_time || not oldify) then + Learnocaml_toplevel_worker_caller.execute + ~pp_answer:(fun _ -> ()) + ~print_outcome:false worker (welcome_phrase ()) + >>= fun _ -> Lwt.return () + else Lwt.return () ) + >>= fun _ -> if not !first_time then let phrase = Learnocaml_toplevel_output.phrase () in Learnocaml_toplevel_output.output_stdout output ~phrase - [%i"The toplevel has been reset.\n"] - else - first_time := false ; + [%i "The toplevel has been reset.\n"] + else first_time := false; Learnocaml_toplevel_worker_caller.register_callback worker "print_html" (Learnocaml_toplevel_output.output_html output) >>= fun _ -> - Learnocaml_toplevel_worker_caller.register_callback worker "print_svg" - (Learnocaml_toplevel_output.output_svg output) - >>= fun err -> load_pp (Lwt.return err) top Learnocaml_toplevel_pp.pp_list + Learnocaml_toplevel_worker_caller.register_callback worker "print_svg" + (Learnocaml_toplevel_output.output_svg output) + >>= fun err -> + load_pp (Lwt.return err) top Learnocaml_toplevel_pp.pp_list >>= fun _ -> - match after_init with - | None -> Lwt.return_unit - | Some f -> f top + match after_init with None -> Lwt.return_unit | Some f -> f top in - after_init top >>= fun () -> - Learnocaml_toplevel_worker_caller.set_after_init top.worker (fun _ -> after_init top); + after_init top + >>= fun () -> + Learnocaml_toplevel_worker_caller.set_after_init top.worker (fun _ -> + after_init top ); Lwt.return top -let print_string { output; _ } = Learnocaml_toplevel_output.output_stdout output +let print_string {output; _} = Learnocaml_toplevel_output.output_stdout output -let prerr_string { output; _ } = Learnocaml_toplevel_output.output_stderr output +let prerr_string {output; _} = Learnocaml_toplevel_output.output_stderr output -let print_html { output; _ } = Learnocaml_toplevel_output.output_html output +let print_html {output; _} = Learnocaml_toplevel_output.output_html output diff --git a/src/toplevel/learnocaml_toplevel.mli b/src/toplevel/learnocaml_toplevel.mli index f2cb5a8f2..b29b1b8e9 100644 --- a/src/toplevel/learnocaml_toplevel.mli +++ b/src/toplevel/learnocaml_toplevel.mli @@ -13,6 +13,23 @@ open Tyxml_js (** An abstract type representing a toplevel instance. *) type t +val create : + ?worker_js_file:string + -> ?timeout_delay:float + -> timeout_prompt:(t -> unit Lwt.t) + -> ?flood_limit:int + -> flood_prompt:(t -> string -> (unit -> int) -> bool Lwt.t) + -> ?after_init:(t -> unit Lwt.t) + -> ?input_sizing:Learnocaml_toplevel_input.sizing + -> ?on_resize:(unit -> unit) + -> ?on_disable_input:(t -> unit) + -> ?on_enable_input:(t -> unit) + -> ?history:Learnocaml_toplevel_history.history + -> ?oldify:bool + -> ?display_welcome:bool + -> container:[`Div] Html5.elt + -> unit + -> t Lwt.t (** Create a toplevel instance in a given container [div]. @param container @@ -59,41 +76,25 @@ type t @param display_welcome Tells if the welcome message with some help and the version of OCaml is to be displayed or not. *) -val create: - ?worker_js_file:string -> - ?timeout_delay: float -> - timeout_prompt:(t -> unit Lwt.t) -> - ?flood_limit: int -> - flood_prompt: (t -> string -> (unit -> int) -> bool Lwt.t) -> - ?after_init:(t -> unit Lwt.t) -> - ?input_sizing: Learnocaml_toplevel_input.sizing -> - ?on_resize:(unit -> unit) -> - ?on_disable_input:(t -> unit) -> - ?on_enable_input:(t -> unit) -> - ?history:Learnocaml_toplevel_history.history -> - ?oldify:bool -> - ?display_welcome: bool -> - container:[`Div] Html5.elt -> - unit -> t Lwt.t +val make_timeout_popup : + ?countdown:int + -> ?refill_step:int + -> ?on_show:(unit -> unit) + -> unit + -> t + -> unit Lwt.t (** Creates a thread that displays a popup over the toplevel container with a countdown and a button to augment it manually, and terminates after the countdown. *) -val make_timeout_popup: - ?countdown: int -> - ?refill_step: int -> - ?on_show: (unit -> unit) -> - unit -> - t -> unit Lwt.t +val make_flood_popup : + ?on_show:(unit -> unit) -> unit -> t -> string -> (unit -> int) -> bool Lwt.t (** Create a thread that displays a popup over the toplevel container displaying the flood amount in real time, and asking if the display should be hidden or not. *) -val make_flood_popup: - ?on_show: (unit -> unit) -> - unit -> - (t -> string -> (unit -> int) -> bool Lwt.t) +val execute_phrase : t -> ?timeout:(t -> unit Lwt.t) -> string -> bool Lwt.t (** Execute a given piece of code. @param timeout @@ -102,10 +103,14 @@ val make_flood_popup: Returns [Success true] whenever the code was correctly typechecked and its evaluation did not raise an exception nor timeouted and [false] otherwise. *) -val execute_phrase: t -> - ?timeout:(t -> unit Lwt.t) -> - string -> bool Lwt.t +val load : + t + -> ?print_outcome:bool + -> ?timeout:(t -> unit Lwt.t) + -> ?message:string + -> string + -> bool Lwt.t (** Execute a given piece of code without displaying it. @param timeout @@ -118,51 +123,45 @@ val execute_phrase: t -> Returns [Success true] whenever the code was correctly typechecked and its evaluation did not raise an exception nor timeouted and [false] otherwise. *) -val load: - t -> - ?print_outcome:bool -> - ?timeout:(t -> unit Lwt.t) -> - ?message: string -> - string -> bool Lwt.t +val check : t -> string -> unit Toploop_results.toplevel_result Lwt.t (** Parse and typecheck a given source code. *) -val check: t -> string -> unit Toploop_results.toplevel_result Lwt.t +val set_checking_environment : t -> unit Lwt.t (** Freezes the environment for future calls to {!check}. *) -val set_checking_environment: t -> unit Lwt.t +val clear : t -> unit (** Empty the toplevel container content. *) -val clear: t -> unit +val reset : t -> unit Lwt.t (** Reset the toplevel environment. *) -val reset: t -> unit Lwt.t +val print_string : t -> string -> unit (** Print a message in the toplevel standard output. This is equivalent to calling [Pervasives.print_string] in the toplevel session. Calls {!Learnocaml_toplevel_output.output_stdout}. *) -val print_string: t -> string -> unit +val prerr_string : t -> string -> unit (** Print a message in the toplevel standard error output. This is equivalent to calling [Pervasives.prerr_string] in the toplevel session. Calls {!Learnocaml_toplevel_output.output_stderr}. *) -val prerr_string: t -> string -> unit +val print_html : t -> string -> unit (** Print a block of HTML in the toplevel output. Calls {!Learnocaml_toplevel_output.output_html}. *) -val print_html: t -> string -> unit +val scroll : t -> unit (** scroll the view to show the last phrase. Calls {!Learnocaml_toplevel_output.scroll. *) -val scroll: t -> unit +val execute : t -> unit (** Execute the content of the input [textarea]. This is equivalent to pressing [Enter] when the toplevel is focused. *) -val execute: t -> unit +val go_backward : t -> unit (** Go backward in the input's history. This is equivalent to pressing [Up] when the toplevel is focused. *) -val go_backward: t -> unit +val go_forward : t -> unit (** Go forward in the input's history. This is equivalent to pressing [Down] when the toplevel is focused. *) -val go_forward: t -> unit diff --git a/src/toplevel/learnocaml_toplevel_history.ml b/src/toplevel/learnocaml_toplevel_history.ml index a75507333..9e3470334 100644 --- a/src/toplevel/learnocaml_toplevel_history.ml +++ b/src/toplevel/learnocaml_toplevel_history.ml @@ -7,146 +7,131 @@ * included LICENSE file for details. *) type history = - { mutable storage : string array ; - mutable updated : string array ; - mutable first : int ; - mutable stored : int ; - mutable current : [ `Floating | `Index of int ] ; - mutable floating : string ; - on_update : (history -> unit) option ; - mutable mtime : float ; - gettimeofday : unit -> float } + { mutable storage : string array + ; mutable updated : string array + ; mutable first : int + ; mutable stored : int + ; mutable current : [`Floating | `Index of int] + ; mutable floating : string + ; on_update : (history -> unit) option + ; mutable mtime : float + ; gettimeofday : unit -> float } -type snapshot = - { phrases : string list ; - mtime : float } +type snapshot = {phrases : string list; mtime : float} -let empty_snapshot = - { phrases = [] ; mtime = 0. } +let empty_snapshot = {phrases = []; mtime = 0.} let snapshot_enc = let open Json_encoding in union [ case (list string) - (function { phrases ; mtime = 0. } -> Some phrases | _ -> None) - (fun phrases -> { phrases ; mtime = 0. }) ; - case (obj2 (req "phrases" (list string)) (req "mtime" float)) - (function { phrases ; mtime } -> Some (phrases, mtime)) - (fun (phrases, mtime) -> { phrases ; mtime }) ] + (function {phrases; mtime = 0.} -> Some phrases | _ -> None) + (fun phrases -> {phrases; mtime = 0.}) + ; case + (obj2 (req "phrases" (list string)) (req "mtime" float)) + (function {phrases; mtime} -> Some (phrases, mtime)) + (fun (phrases, mtime) -> {phrases; mtime}) ] -let snapshot { storage ; first ; stored ; mtime ; _} = +let snapshot {storage; first; stored; mtime; _} = let rec to_list acc i n = - if n = 0 then - List.rev acc - else - to_list - (storage.(i) :: acc) - ((i + 1) mod Array.length storage) - (n - 1) in - { phrases = to_list [] first stored ; - mtime } + if n = 0 then List.rev acc + else to_list (storage.(i) :: acc) ((i + 1) mod Array.length storage) (n - 1) + in + {phrases = to_list [] first stored; mtime} let call_on_update (self : history) = - self.mtime <- self.gettimeofday () ; - match self.on_update with - | None -> () - | Some callback -> callback self + self.mtime <- self.gettimeofday (); + match self.on_update with None -> () | Some callback -> callback self let create ~gettimeofday ?on_update ?(max_size = 0) ?snapshot () = let history = - { storage = Array.make max_size "" ; - updated = Array.make max_size "" ; - first = 0 ; - stored = 0 ; - current = `Floating ; - floating = "" ; - on_update ; - mtime = 0. ; - gettimeofday } in - begin match snapshot with - | None -> () - | Some { phrases ; mtime } -> - history.mtime <- mtime ; - List.iteri (fun i code -> - if i >= max_size then () else begin - history.storage.(i) <- code ; - history.updated.(i) <- code ; - history.stored <- history.stored + 1 - end) - phrases - end ; + { storage = Array.make max_size "" + ; updated = Array.make max_size "" + ; first = 0 + ; stored = 0 + ; current = `Floating + ; floating = "" + ; on_update + ; mtime = 0. + ; gettimeofday } + in + ( match snapshot with + | None -> () + | Some {phrases; mtime} -> + history.mtime <- mtime; + List.iteri + (fun i code -> + if i >= max_size then () + else ( + history.storage.(i) <- code; + history.updated.(i) <- code; + history.stored <- history.stored + 1 ) ) + phrases ); history let current history = match history with - | { current = `Floating ; floating ; _ } -> floating - | { current = `Index i ; updated ; _ } -> updated.(i) + | {current = `Floating; floating; _} -> floating + | {current = `Index i; updated; _} -> updated.(i) let update history text = match history with - | { current = `Floating ; _ } -> - history.floating <- text - | { current = `Index i ; updated ; _ } -> - updated.(i) <- text ; - call_on_update history + | {current = `Floating; _} -> history.floating <- text + | {current = `Index i; updated; _} -> + updated.(i) <- text; call_on_update history let go_forward history = match history with - | { current = `Floating ; _ } -> () - | { current = `Index i ; _ } -> + | {current = `Floating; _} -> () + | {current = `Index i; _} -> let size = Array.length history.storage in let last = (history.first + history.stored - 1) mod size in - if i = last then - history.current <- `Floating - else - history.current <- `Index ((i + 1) mod size) + if i = last then history.current <- `Floating + else history.current <- `Index ((i + 1) mod size) let go_backward history = match history with - | { current = `Floating ; _ } -> - if history.stored > 0 then begin + | {current = `Floating; _} -> + if history.stored > 0 then let size = Array.length history.storage in let last = (history.first + history.stored - 1) mod size in history.current <- `Index last - end - | { current = `Index i ; first ; _ } -> + | {current = `Index i; first; _} -> let size = Array.length history.storage in - if i <> first then - history.current <- `Index ((i + size - 1) mod size) + if i <> first then history.current <- `Index ((i + size - 1) mod size) let push history = let text = match history with - | { current = `Floating ; _ } -> history.floating - | { current = `Index i ; _ } -> + | {current = `Floating; _} -> history.floating + | {current = `Index i; _} -> let updated = history.updated.(i) in - history.updated.(i) <- history.storage.(i) ; - updated in - history.floating <- "" ; - history.current <- `Floating ; + history.updated.(i) <- history.storage.(i); + updated + in + history.floating <- ""; + history.current <- `Floating; let size = Array.length history.storage in - if history.stored = 0 - || (let last = (history.stored + history.first - 1) mod size in - (* don't insert duplicates *) - history.storage.(last) <> text) then begin - if text <> "" then begin - if history.stored < size then begin - history.stored <- history.stored + 1 - end else begin - history.first <- (history.first + 1) mod size - end ; + if + history.stored = 0 + || + let last = (history.stored + history.first - 1) mod size in + (* don't insert duplicates *) + history.storage.(last) <> text + then + if text <> "" then ( + if history.stored < size then history.stored <- history.stored + 1 + else history.first <- (history.first + 1) mod size; let i = (history.stored + history.first - 1) mod size in - history.storage.(i) <- text ; - call_on_update history ; - history.updated.(i) <- text - end - end + history.storage.(i) <- text; + call_on_update history; + history.updated.(i) <- text ) let discard history = match history with - | { current = `Floating ; _ } -> - history.floating <- "" ; + | {current = `Floating; _} -> + history.floating <- ""; history.current <- `Floating - | { current = `Index i ; _ } -> - history.updated.(i) <- history.storage.(i) ; + | {current = `Index i; _} -> + history.updated.(i) <- history.storage.(i); history.current <- `Floating diff --git a/src/toplevel/learnocaml_toplevel_history.mli b/src/toplevel/learnocaml_toplevel_history.mli index 72807cd5f..b5ca8ad77 100644 --- a/src/toplevel/learnocaml_toplevel_history.mli +++ b/src/toplevel/learnocaml_toplevel_history.mli @@ -8,20 +8,19 @@ type history -type snapshot = - { phrases : string list ; - mtime : float } +type snapshot = {phrases : string list; mtime : float} val snapshot_enc : snapshot Json_encoding.encoding val empty_snapshot : snapshot -val create: - gettimeofday: (unit -> float) -> - ?on_update: (history -> unit) -> - ?max_size: int -> - ?snapshot: snapshot -> - unit -> history +val create : + gettimeofday:(unit -> float) + -> ?on_update:(history -> unit) + -> ?max_size:int + -> ?snapshot:snapshot + -> unit + -> history val current : history -> string diff --git a/src/toplevel/learnocaml_toplevel_input.ml b/src/toplevel/learnocaml_toplevel_input.ml index d27ca008f..89f96ac33 100644 --- a/src/toplevel/learnocaml_toplevel_input.ml +++ b/src/toplevel/learnocaml_toplevel_input.ml @@ -9,179 +9,173 @@ open Js_of_ocaml let indent_caml s in_lines = - let output = { - IndentPrinter.debug = false; - config = IndentConfig.default; - in_lines; - indent_empty = true; - adaptive = true; - kind = IndentPrinter.Print (fun s acc -> acc ^ s) - } + let output = + { IndentPrinter.debug = false + ; config = IndentConfig.default + ; in_lines + ; indent_empty = true + ; adaptive = true + ; kind = IndentPrinter.Print (fun s acc -> acc ^ s) } in let stream = Nstream.of_string s in IndentPrinter.proceed output stream IndentBlock.empty "" let indent_ocaml_textarea textbox = - let rec loop s acc (i,pos') = + let rec loop s acc (i, pos') = try let pos = String.index_from s pos' '\n' in - loop s ((i,(pos',pos))::acc) (succ i,succ pos) - with _ -> List.rev ((i,(pos',String.length s)) :: acc) in - let rec find (l : (int * (int * int)) list ) c = + loop s ((i, (pos', pos)) :: acc) (succ i, succ pos) + with _ -> List.rev ((i, (pos', String.length s)) :: acc) + in + let rec find (l : (int * (int * int)) list) c = match l with | [] -> assert false - | (i,(lo,up))::_ when up >= c -> c,i,lo,up - | _::rem -> find rem c in + | (i, (lo, up)) :: _ when up >= c -> (c, i, lo, up) + | _ :: rem -> find rem c + in let v = textbox##.value in let pos = let c1 = (Obj.magic textbox)##.selectionStart and c2 = (Obj.magic textbox)##.selectionEnd in - if Js.Opt.test (Obj.magic c1) && Js.Opt.test (Obj.magic c2) - then begin - let l = loop (Js.to_string v) [] (0,0) in - Some (find l c1,find l c2) - end - else None in - let f = match pos with - | None -> (fun _ -> true) - | Some ((_,line1,_,_),(_,line2,_,_)) -> (fun l -> l>=(line1+1) && l<=(line2+1)) in + if Js.Opt.test (Obj.magic c1) && Js.Opt.test (Obj.magic c2) then + let l = loop (Js.to_string v) [] (0, 0) in + Some (find l c1, find l c2) + else None + in + let f = + match pos with + | None -> fun _ -> true + | Some ((_, line1, _, _), (_, line2, _, _)) -> + fun l -> l >= line1 + 1 && l <= line2 + 1 + in let v = indent_caml (Js.to_string v) f in - textbox##.value:=Js.string v; - begin match pos with - | Some ((c1,line1,_,up1),(c2,line2,_,up2)) -> - let l = loop v [] (0,0) in - let (lo1'',up1'') = List.assoc line1 l in - let (lo2'',up2'') = List.assoc line2 l in + textbox##.value := Js.string v; + match pos with + | Some ((c1, line1, _, up1), (c2, line2, _, up2)) -> + let l = loop v [] (0, 0) in + let lo1'', up1'' = List.assoc line1 l in + let lo2'', up2'' = List.assoc line2 l in let n1 = max (c1 + up1'' - up1) lo1'' in let n2 = max (c2 + up2'' - up2) lo2'' in - let () = (Obj.magic textbox)##(setSelectionRange n1 n2) in - textbox##focus; - () - | None -> () end + let () = (Obj.magic textbox) ## (setSelectionRange n1 n2) in + textbox##focus; () + | None -> () -type sizing = - { line_height : int ; - min_lines : int ; - max_lines : int } +type sizing = {line_height : int; min_lines : int; max_lines : int} type input = - { textbox : Dom_html.textAreaElement Js.t ; - sizing : sizing option ; - container : [ `Div ] Tyxml_js.Html5.elt ; - mutable focused : bool ; - mutable disabled : bool ; - history : Learnocaml_toplevel_history.history ; - on_resize : unit -> unit ; - execute : string -> unit } + { textbox : Dom_html.textAreaElement Js.t + ; sizing : sizing option + ; container : [`Div] Tyxml_js.Html5.elt + ; mutable focused : bool + ; mutable disabled : bool + ; history : Learnocaml_toplevel_history.history + ; on_resize : unit -> unit + ; execute : string -> unit } -let disable ({ textbox ; container } as input) = - textbox##.disabled := Js._true ; - Js_utils.Manip.addClass container "disabled" ; +let disable ({textbox; container} as input) = + textbox##.disabled := Js._true; + Js_utils.Manip.addClass container "disabled"; input.disabled <- true -let enable ({ textbox ; container } as input) = - textbox##.disabled := Js._false ; - Js_utils.Manip.removeClass container "disabled" ; - if input.focused then textbox##focus ; +let enable ({textbox; container} as input) = + textbox##.disabled := Js._false; + Js_utils.Manip.removeClass container "disabled"; + if input.focused then textbox##focus; input.disabled <- false -let set { textbox } text = - textbox##.value := Js.string text +let set {textbox} text = textbox##.value := Js.string text -let get { textbox } = - Js.to_string textbox##.value +let get {textbox} = Js.to_string textbox##.value -let resize { textbox ; sizing ; on_resize } = +let resize {textbox; sizing; on_resize} = match sizing with | None -> () - | Some { line_height ; min_lines ; max_lines } -> - on_resize () ; + | Some {line_height; min_lines; max_lines} -> + on_resize (); let lines = let text = textbox##.value in let res = ref 1 in for i = 0 to text##.length - 1 do - if text##(charAt i) = Js.string "\n" then incr res - done ; - !res |> min max_lines |> max min_lines in - textbox##.style##.fontSize := (Js.string (string_of_int line_height ^ "px")) ; - textbox##.style##.height := Js.string (Printf.sprintf "%dpx" (line_height * lines)) + if text ## (charAt i) = Js.string "\n" then incr res + done; + !res |> min max_lines |> max min_lines + in + textbox##.style##.fontSize := Js.string (string_of_int line_height ^ "px"); + textbox##.style##.height + := Js.string (Printf.sprintf "%dpx" (line_height * lines)) -let execute ({ history ; textbox ; execute } as input) = +let execute ({history; textbox; execute} as input) = let code = Js.to_string textbox##.value in - Learnocaml_toplevel_history.update history code ; - Learnocaml_toplevel_history.push history ; - textbox##.value := Js.string (Learnocaml_toplevel_history.current history) ; - resize input ; + Learnocaml_toplevel_history.update history code; + Learnocaml_toplevel_history.push history; + textbox##.value := Js.string (Learnocaml_toplevel_history.current history); + resize input; execute code -let go_backward ({ history ; textbox } as input) = - Learnocaml_toplevel_history.update history (Js.to_string textbox##.value) ; - Learnocaml_toplevel_history.go_backward history ; - textbox##.value := Js.string (Learnocaml_toplevel_history.current history) ; +let go_backward ({history; textbox} as input) = + Learnocaml_toplevel_history.update history (Js.to_string textbox##.value); + Learnocaml_toplevel_history.go_backward history; + textbox##.value := Js.string (Learnocaml_toplevel_history.current history); resize input -let go_forward ({ history ; textbox } as input) = - Learnocaml_toplevel_history.update history (Js.to_string textbox##.value) ; - Learnocaml_toplevel_history.go_forward history ; - textbox##.value := Js.string (Learnocaml_toplevel_history.current history) ; - resize input +let go_forward ({history; textbox} as input) = + Learnocaml_toplevel_history.update history (Js.to_string textbox##.value); + Learnocaml_toplevel_history.go_forward history; + textbox##.value := Js.string (Learnocaml_toplevel_history.current history); + resize input -let focus input = input.textbox##focus +let focus input = (input.textbox)##focus -let setup - ?sizing ?history - ?(on_resize = (fun () -> ())) +let setup ?sizing ?history ?(on_resize = fun () -> ()) ~execute:execute_callback ~container () = - let textbox = - Dom_html.createTextarea Dom_html.document in - Js_utils.Manip.addClass container "toplevel-input" ; - let history = match history with + let textbox = Dom_html.createTextarea Dom_html.document in + Js_utils.Manip.addClass container "toplevel-input"; + let history = + match history with | None -> let gettimeofday () = 0. (* unsaved history *) in - Learnocaml_toplevel_history.create ~gettimeofday ~max_size: 99 () - | Some history -> history in + Learnocaml_toplevel_history.create ~gettimeofday ~max_size:99 () + | Some history -> history + in let input = - { textbox ; sizing ; container ; history ; on_resize ; - focused = false ; disabled = false ; execute = execute_callback } in - textbox##.onkeydown := Dom_html.handler (fun e -> - let ctrl = Js.to_bool e##.ctrlKey || Js.to_bool e##.metaKey in - let shift = Js.to_bool e##.shiftKey in - match e##.keyCode with - (* Enter *) - | 13 when not (shift || ctrl) -> - execute input ; - Js._false - (* Tab *) - | 09 when not shift -> - indent_ocaml_textarea textbox ; - Js._false - (* Up arrow *) - | 38 when ctrl -> - go_backward input ; - Js._false - (* Down arrow *) - | 40 when ctrl -> - go_forward input ; - Js._false - (* Defaults *) - | 13 -> - resize input ; - Js._true - | _ -> Js._true - ); + { textbox + ; sizing + ; container + ; history + ; on_resize + ; focused = false + ; disabled = false + ; execute = execute_callback } + in + textbox##.onkeydown := + Dom_html.handler (fun e -> + let ctrl = Js.to_bool e##.ctrlKey || Js.to_bool e##.metaKey in + let shift = Js.to_bool e##.shiftKey in + match e##.keyCode with + (* Enter *) + | 13 when not (shift || ctrl) -> execute input; Js._false + (* Tab *) + | 09 when not shift -> + indent_ocaml_textarea textbox; + Js._false + (* Up arrow *) + | 38 when ctrl -> go_backward input; Js._false + (* Down arrow *) + | 40 when ctrl -> go_forward input; Js._false + (* Defaults *) + | 13 -> resize input; Js._true + | _ -> Js._true ); textbox##.onfocus := Dom_html.handler (fun _ -> - if not (input.disabled) then input.focused <- true ; - Js._true); + if not input.disabled then input.focused <- true; + Js._true ); textbox##.onblur := Dom_html.handler (fun _ -> - if not (input.disabled) then input.focused <- false ; - Js._true); - textbox##.onkeyup := - Dom_html.handler (fun _ -> resize input ; Js._true); - textbox##.onchange := - Dom_html.handler (fun _ -> resize input ; Js._true); - Js_utils.Manip.replaceChildren container - [ Tyxml_js.Of_dom.of_textArea textbox ] ; - resize input ; + if not input.disabled then input.focused <- false; + Js._true ); + textbox##.onkeyup := Dom_html.handler (fun _ -> resize input; Js._true); + textbox##.onchange := Dom_html.handler (fun _ -> resize input; Js._true); + Js_utils.Manip.replaceChildren container [Tyxml_js.Of_dom.of_textArea textbox]; + resize input; input diff --git a/src/toplevel/learnocaml_toplevel_input.mli b/src/toplevel/learnocaml_toplevel_input.mli index 8d3bd3e66..e44a6fec6 100644 --- a/src/toplevel/learnocaml_toplevel_input.mli +++ b/src/toplevel/learnocaml_toplevel_input.mli @@ -13,10 +13,19 @@ type input (** Size parameters for the input box. *) type sizing = - { line_height : int (** The height of ta line in pixels. *) ; - min_lines : int (** The minimum assigned height in terms of of lines. *) ; - max_lines : int (** The maximum assigned height in terms of of lines. *) } + { line_height : int (** The height of ta line in pixels. *) + ; min_lines : int (** The minimum assigned height in terms of of lines. *) + ; max_lines : int (** The maximum assigned height in terms of of lines. *) + } +val setup : + ?sizing:sizing + -> ?history:Learnocaml_toplevel_history.history + -> ?on_resize:(unit -> unit) + -> execute:(string -> unit) + -> container:[`Div] Tyxml_js.Html5.elt + -> unit + -> input (** Use a given div as an input box. Inserts a textarea in it and gives it the class [toplevel-input]. @@ -29,35 +38,28 @@ type sizing = @param execute The callback called whenever the [Enter] key is pressed, or the {!execute} function is called. *) -val setup : - ?sizing: sizing -> - ?history:Learnocaml_toplevel_history.history -> - ?on_resize:(unit -> unit) -> - execute: (string -> unit) -> - container: [ `Div ] Tyxml_js.Html5.elt -> - unit -> input -(** Disable the input box. *) val disable : input -> unit +(** Disable the input box. *) -(** Enable the input box. *) val enable : input -> unit +(** Enable the input box. *) +val set : input -> string -> unit (** Updates the contents of the field. Discards its current contents. *) -val set : input -> string -> unit -(** Gives the content of the input box. *) val get : input -> string +(** Gives the content of the input box. *) -(** Simulates a hit on the [Enter] key *) val execute : input -> unit +(** Simulates a hit on the [Enter] key *) -(** Simulates a hit on the [Up] key *) val go_backward : input -> unit +(** Simulates a hit on the [Up] key *) -(** Simulates a hit on the [Down] key *) val go_forward : input -> unit +(** Simulates a hit on the [Down] key *) -(** Sets focus to the text input field *) val focus : input -> unit +(** Sets focus to the text input field *) diff --git a/src/toplevel/learnocaml_toplevel_output.ml b/src/toplevel/learnocaml_toplevel_output.ml index 642300e43..efeebe68e 100644 --- a/src/toplevel/learnocaml_toplevel_output.ml +++ b/src/toplevel/learnocaml_toplevel_output.ml @@ -9,180 +9,185 @@ open Js_of_ocaml type block = - | Html of string * [ `Div ] Tyxml_js.Html5.elt - | Std of (string * [ `Out | `Err ]) list ref * [ `Pre ] Tyxml_js.Html5.elt - | Code of string * pretty list ref * [ `Pre ] Tyxml_js.Html5.elt * Nstream.snapshot option - | Answer of string * pretty list ref * [ `Pre ] Tyxml_js.Html5.elt * Nstream.snapshot option - | Error of Toploop_results.error * [ `Pre ] Tyxml_js.Html5.elt - | Warning of int * Toploop_results.warning * [ `Pre ] Tyxml_js.Html5.elt + | Html of string * [`Div] Tyxml_js.Html5.elt + | Std of (string * [`Out | `Err]) list ref * [`Pre] Tyxml_js.Html5.elt + | Code of + string + * pretty list ref + * [`Pre] Tyxml_js.Html5.elt + * Nstream.snapshot option + | Answer of + string + * pretty list ref + * [`Pre] Tyxml_js.Html5.elt + * Nstream.snapshot option + | Error of Toploop_results.error * [`Pre] Tyxml_js.Html5.elt + | Warning of int * Toploop_results.warning * [`Pre] Tyxml_js.Html5.elt | Phrase of phrase * block list ref -and pretty = - | String of string - | Ref of int - | Class of string * pretty list +and pretty = String of string | Ref of int | Class of string * pretty list -and phrase = - { mutable warnings : int } +and phrase = {mutable warnings : int} -let phrase () = - { warnings = 0 } +let phrase () = {warnings = 0} type output = - { limit : int ; - container : [ `Div ] Tyxml_js.Html5.elt ; - mutable blocks : block list ; - on_resize : unit -> unit } - -let setup - ?(limit = max_int) - ?(on_resize = (fun () -> ())) - ~container () = - Js_utils.Manip.addClass container "toplevel-output" ; - { container ; limit ; blocks = [] ; on_resize } - -let enforce_limit { limit ; container } = + { limit : int + ; container : [`Div] Tyxml_js.Html5.elt + ; mutable blocks : block list + ; on_resize : unit -> unit } + +let setup ?(limit = max_int) ?(on_resize = fun () -> ()) ~container () = + Js_utils.Manip.addClass container "toplevel-output"; + {container; limit; blocks = []; on_resize} + +let enforce_limit {limit; container} = let container = Tyxml_js.To_dom.of_div container in while container##.childNodes##.length > limit do - Js.Opt.case - (container##.firstChild) + Js.Opt.case container##.firstChild (fun () -> ()) - (fun child -> ignore (container##(removeChild child))) + (fun child -> ignore container ## (removeChild child)) done -let scroll { container ; on_resize } = +let scroll {container; on_resize} = let container = Tyxml_js.To_dom.of_div container in - Lwt.async @@ fun () -> - Lwt.bind (Lwt_js.yield ()) @@ fun () -> - container##.scrollTop := container##.scrollHeight - container##.clientHeight ; - on_resize () ; + Lwt.async + @@ fun () -> + Lwt.bind (Lwt_js.yield ()) + @@ fun () -> + container##.scrollTop := container##.scrollHeight - container##.clientHeight; + on_resize (); Lwt.return_unit -let rec pretty_html - : 'a. pretty list -> ([> `Span | `PCDATA ] as 'a) Tyxml_js.Html5.elt list - = fun pretty -> +let rec pretty_html : + 'a. pretty list + -> ([> `Span | `PCDATA] as 'a) Tyxml_js.Html5.elt list = + fun pretty -> let open Tyxml_js.Html5 in List.map (function - | String text -> - txt text - | Ref n -> - span ~a: [ a_class [ "ref" ] ] [ txt (string_of_int n) ] - | Class (cls, [ Class (cls', [ Class (cls'', toks) ]) ]) -> - span ~a: [ a_class [ cls ; cls' ; cls'' ] ] (pretty_html toks) - | Class (cls, [ Class (cls', toks) ]) -> - span ~a: [ a_class [ cls ; cls' ] ] (pretty_html toks) - | Class (cls, toks) -> - span ~a: [ a_class [ cls ] ] (pretty_html toks)) + | String text -> txt text + | Ref n -> span ~a:[a_class ["ref"]] [txt (string_of_int n)] + | Class (cls, [Class (cls', [Class (cls'', toks)])]) -> + span ~a:[a_class [cls; cls'; cls'']] (pretty_html toks) + | Class (cls, [Class (cls', toks)]) -> + span ~a:[a_class [cls; cls']] (pretty_html toks) + | Class (cls, toks) -> span ~a:[a_class [cls]] (pretty_html toks)) pretty let initial_state = - { Approx_lexer.initial_state with Approx_lexer.eof_closing = false }, - Nstream.Region.zero + ( {Approx_lexer.initial_state with Approx_lexer.eof_closing = false} + , Nstream.Region.zero ) let prettify_ocaml ?(snapshot = initial_state) code = - let stream = Nstream.of_string ~st: snapshot code in + let stream = Nstream.of_string ~st:snapshot code in let rec format snapshot stream acc = let open Approx_tokens in let open Nstream in match Nstream.next_full stream with - | None -> List.rev acc, snapshot - | Some ({token = EOF}, _snapshot, _) -> List.rev acc, snapshot + | None -> (List.rev acc, snapshot) + | Some ({token = EOF}, _snapshot, _) -> (List.rev acc, snapshot) | Some (tok, snapshot, stream) -> let this = let kind = Ocaml_mode.token_type tok.token in - Class (kind, [ String tok.substr ]) :: - if tok.between = "" then [] else [ String tok.between ] in - format (Some snapshot) stream (this @ acc) in + Class (kind, [String tok.substr]) + :: (if tok.between = "" then [] else [String tok.between]) + in + format (Some snapshot) stream (this @ acc) + in format (Some snapshot) stream [] let rec last_elt = function | [] -> raise Not_found - | Html (_, div) :: _ -> (div :> [ `Div | `Pre ] Tyxml_js.Html5.elt) + | Html (_, div) :: _ -> (div :> [`Div | `Pre] Tyxml_js.Html5.elt) | Std (_, pre) :: _ - | Code (_, _, pre, _) :: _ - | Answer (_, _, pre, _) :: _ - | Error (_, pre) :: _ - | Warning (_, _, pre) :: _ -> (pre :> [ `Div | `Pre ] Tyxml_js.Html5.elt) - | Phrase (_, { contents }) :: rest -> - try last_elt contents with Not_found -> last_elt rest + |Code (_, _, pre, _) :: _ + |Answer (_, _, pre, _) :: _ + |Error (_, pre) :: _ + |Warning (_, _, pre) :: _ -> + (pre :> [`Div | `Pre] Tyxml_js.Html5.elt) + | Phrase (_, {contents}) :: rest -> ( + try last_elt contents with Not_found -> last_elt rest ) let find_phrase output u = List.fold_left - (fun acc block -> match acc, block with - | None, Phrase (u', l) when u == u' -> Some l - | _ -> acc) + (fun acc block -> + match (acc, block) with + | None, Phrase (u', l) when u == u' -> Some l + | _ -> acc ) None output.blocks let insert output ?phrase block elt = let hr = Tyxml_js.Html5.hr () in match phrase with | None -> - output.blocks <- block :: output.blocks ; - Js_utils.Manip.appendChild output.container hr ; - Js_utils.Manip.appendChild output.container elt ; + output.blocks <- block :: output.blocks; + Js_utils.Manip.appendChild output.container hr; + Js_utils.Manip.appendChild output.container elt; scroll output - | Some u -> - match find_phrase output u with - | Some l -> - Js_utils.Manip.insertChildAfter output.container (last_elt !l) elt ; - l := block :: !l ; - scroll output - | None -> - output.blocks <- Phrase (u, ref [ block ]) :: output.blocks ; - Js_utils.Manip.appendChild output.container hr ; - Js_utils.Manip.appendChild output.container elt ; - scroll output + | Some u -> ( + match find_phrase output u with + | Some l -> + Js_utils.Manip.insertChildAfter output.container (last_elt !l) elt; + l := block :: !l; + scroll output + | None -> + output.blocks <- Phrase (u, ref [block]) :: output.blocks; + Js_utils.Manip.appendChild output.container hr; + Js_utils.Manip.appendChild output.container elt; + scroll output ) let output_std ?phrase output (str, chan) = - enforce_limit output ; + enforce_limit output; let buf, pre = match output.blocks with - | Phrase (u, l) :: _ when (Some u) = phrase -> + | Phrase (u, l) :: _ when Some u = phrase -> let rec find = function - | Std (buf, pre) :: _ -> buf, pre + | Std (buf, pre) :: _ -> (buf, pre) | _ :: rest -> find rest | [] -> let buf, pre = - ref [], - Tyxml_js.Html5.(pre ~a: [ a_class [ "toplevel-output" ] ]) [] in - Js_utils.Manip.insertChildAfter output.container (last_elt !l) pre ; - l := Std (buf, pre) :: !l ; - Js_utils.Manip.appendChild output.container pre ; - buf, pre in + ( ref [] + , Tyxml_js.Html5.(pre ~a:[a_class ["toplevel-output"]]) [] ) + in + Js_utils.Manip.insertChildAfter output.container (last_elt !l) + pre; + l := Std (buf, pre) :: !l; + Js_utils.Manip.appendChild output.container pre; + (buf, pre) + in find !l - | Std (buf, pre) :: _ -> buf, pre + | Std (buf, pre) :: _ -> (buf, pre) | _ -> let hr = Tyxml_js.Html5.hr () in let buf, pre = - ref [], - Tyxml_js.Html5.(pre ~a: [ a_class [ "toplevel-output" ] ]) [] in - output.blocks <- Std (buf, pre) :: output.blocks ; - Js_utils.Manip.appendChild output.container hr ; - Js_utils.Manip.appendChild output.container pre ; - buf, pre in + (ref [], Tyxml_js.Html5.(pre ~a:[a_class ["toplevel-output"]]) []) + in + output.blocks <- Std (buf, pre) :: output.blocks; + Js_utils.Manip.appendChild output.container hr; + Js_utils.Manip.appendChild output.container pre; + (buf, pre) + in let cls = match chan with `Err -> "stderr" | `Out -> "stdout" in Js_utils.Manip.appendChild pre - (Tyxml_js.Html5.(span ~a: [ a_class [ cls ] ] [ txt str ])) ; - buf := (str, chan) :: !buf ; + Tyxml_js.Html5.(span ~a:[a_class [cls]] [txt str]); + buf := (str, chan) :: !buf; scroll output -let output_stdout ?phrase output str = - output_std ?phrase output (str, `Out) +let output_stdout ?phrase output str = output_std ?phrase output (str, `Out) -let output_stderr ?phrase output str = - output_std ?phrase output (str, `Err) +let output_stderr ?phrase output str = output_std ?phrase output (str, `Err) let output_html ?phrase output html = - enforce_limit output ; - let div = Tyxml_js.Html5.(div ~a: [ a_class [ "toplevel-html-block" ] ]) [] in - Js_utils.Manip.setInnerHtml div html ; - Js_utils.Manip.appendChild output.container div ; + enforce_limit output; + let div = Tyxml_js.Html5.(div ~a:[a_class ["toplevel-html-block"]]) [] in + Js_utils.Manip.setInnerHtml div html; + Js_utils.Manip.appendChild output.container div; insert output ?phrase (Html (html, div)) div let get_fresh_id = let r = ref 0 in - fun () -> incr r ; !r + fun () -> incr r; !r (* It replaces markup field id by "id-" to avoid interferences between svg images when they are inserted in the same DOM. @@ -203,106 +208,103 @@ let replace_link svg = (* It cleans the svg string to be readable in the web app. *) let rewrite_svg svg = let idx = get_fresh_id () in - replace_markup idx "id" svg - |> replace_markup idx "l:href" - |> replace_link + replace_markup idx "id" svg |> replace_markup idx "l:href" |> replace_link let output_svg ?phrase output svg = - let svg = rewrite_svg svg in + let svg = rewrite_svg svg in output_html ?phrase output svg let output_code ?phrase output code = let snapshot = - let blocks = match phrase with + let blocks = + match phrase with | None -> output.blocks - | Some u -> - match find_phrase output u with - | None -> [] - | Some l -> !l in + | Some u -> ( + match find_phrase output u with None -> [] | Some l -> !l ) + in match blocks with | Code (_, _, _, snapshot) :: _ -> snapshot - | [] | _ -> None in - enforce_limit output ; + | [] | _ -> None + in + enforce_limit output; let pretty, snapshot = prettify_ocaml ?snapshot code in let pre = - Tyxml_js.Html5.(pre ~a: [ a_class [ "toplevel-code" ] ]) - (pretty_html pretty) in + Tyxml_js.Html5.(pre ~a:[a_class ["toplevel-code"]]) (pretty_html pretty) + in insert output ?phrase (Code (code, ref pretty, pre, snapshot)) pre let output_answer ?phrase output answer = let snapshot = - let blocks = match phrase with + let blocks = + match phrase with | None -> output.blocks - | Some u -> - match find_phrase output u with - | None -> [] - | Some l -> !l in + | Some u -> ( + match find_phrase output u with None -> [] | Some l -> !l ) + in match blocks with | Answer (_, _, _, snapshot) :: _ -> snapshot - | [] | _ -> None in - enforce_limit output ; + | [] | _ -> None + in + enforce_limit output; let pretty, snapshot = prettify_ocaml ?snapshot answer in let pre = - Tyxml_js.Html5.(pre ~a: [ a_class [ "toplevel-answer" ] ]) - (pretty_html pretty) in + Tyxml_js.Html5.(pre ~a:[a_class ["toplevel-answer"]]) (pretty_html pretty) + in insert output ?phrase (Answer (answer, ref pretty, pre, snapshot)) pre open Toploop_results -let inside (l, c) { loc_start = (sl, sc) ; loc_end = (el, ec) } = - ((l > sl) || (l = sl && c >= sc)) - && ((l < el) || (l = el && c < ec)) +let inside (l, c) {loc_start = sl, sc; loc_end = el, ec} = + (l > sl || (l = sl && c >= sc)) && (l < el || (l = el && c < ec)) -let last (l, c) { loc_end = (el, ec) } = - l = el && c = ec - 1 +let last (l, c) {loc_end = el, ec} = l = el && c = ec - 1 let hilight_pretty cls pretty locs lbl = let hilight_one pretty loc = - let rec hilight_one pretty pos acc = match pretty with - | [] -> List.rev acc, pos + let rec hilight_one pretty pos acc = + match pretty with + | [] -> (List.rev acc, pos) | Class (cls, toks) :: rest -> let toks, pos = hilight_one toks pos [] in hilight_one rest pos (Class (cls, toks) :: acc) - | String "" :: rest -> - hilight_one rest pos acc - | Ref n :: rest -> - hilight_one rest pos (Ref n :: acc) + | String "" :: rest -> hilight_one rest pos acc + | Ref n :: rest -> hilight_one rest pos (Ref n :: acc) | String s :: rest -> let next (l, c) = function '\n' -> (l + 1, 0) | _ -> (l, c + 1) in let rec loop was_inside was_last p i acc pos = let tok p i = if was_inside then (if was_last then lbl else []) - @ [ Class (cls, [ String (String.sub s p (i - p)) ]) ] - else - [ String (String.sub s p (i - p)) ] in - if i = String.length s then - tok p i @ acc, pos - else if was_inside <> (inside pos loc) then + @ [Class (cls, [String (String.sub s p (i - p))])] + else [String (String.sub s p (i - p))] + in + if i = String.length s then (tok p i @ acc, pos) + else if was_inside <> inside pos loc then let acc = if p < i then tok p i @ acc else acc in loop (not was_inside) was_last i i acc pos - else - loop was_inside (last pos loc) p (i + 1) acc (next pos (String.get s i)) in + else loop was_inside (last pos loc) p (i + 1) acc (next pos s.[i]) + in let toks, pos = loop false false 0 0 [] pos in - hilight_one rest pos (toks @ acc) in - fst (hilight_one pretty (1, 0) []) in + hilight_one rest pos (toks @ acc) + in + fst (hilight_one pretty (1, 0) []) + in List.fold_left hilight_one pretty locs let advance_loc code loc = let rec loop i sl sc el ec = - if i >= String.length code then - { loc_start = (sl, sc) ; loc_end = (el, ec) } + if i >= String.length code then {loc_start = (sl, sc); loc_end = (el, ec)} else let next l c = (* should work even with ignored '\n's *) - if l > 1 then - (if String.get code i = '\n' then l - 1 else l), c - else - 1, max 0 (c - 1) in + if l > 1 then ((if code.[i] = '\n' then l - 1 else l), c) + else (1, max 0 (c - 1)) + in let sl, sc = next sl sc in let el, ec = next el ec in - loop (i + 1) sl sc el ec in - let { loc_start = (sl, sc) ; loc_end = (el, ec) } = loc in + loop (i + 1) sl sc el ec + in + let {loc_start = sl, sc; loc_end = el, ec} = loc in loop 0 sl sc el ec let hilight cls output u locs lbl = @@ -311,56 +313,57 @@ let hilight cls output u locs lbl = | Some l -> let rec loop locs = function | Code (code, pretty, pre, _) :: rest -> - pretty := hilight_pretty cls !pretty locs lbl ; - Js_utils.Manip.replaceChildren pre (pretty_html !pretty) ; + pretty := hilight_pretty cls !pretty locs lbl; + Js_utils.Manip.replaceChildren pre (pretty_html !pretty); let locs = List.map (advance_loc code) locs in loop locs rest | _ :: rest -> loop locs rest - | [] -> () in + | [] -> () + in loop locs (List.rev !l) let output_error ?phrase output error = - let { Toploop_results.locs ; msg ; if_highlight } = error in + let {Toploop_results.locs; msg; if_highlight} = error in let content = [ Tyxml_js.Html5.txt - (match phrase with None -> msg | Some _ -> if_highlight) ] in - let pre = - Tyxml_js.Html5.(pre ~a: [ a_class [ "toplevel-error" ] ]) content in - begin match phrase, locs with - | None, _ | _, [] -> () - | Some u, _ -> hilight "toplevel-hilighted-error" output u locs [] - end ; + (match phrase with None -> msg | Some _ -> if_highlight) ] + in + let pre = Tyxml_js.Html5.(pre ~a:[a_class ["toplevel-error"]]) content in + ( match (phrase, locs) with + | None, _ | _, [] -> () + | Some u, _ -> hilight "toplevel-hilighted-error" output u locs [] ); insert output ?phrase (Error (error, pre)) pre let output_warning ?phrase output warning = - let { Toploop_results.locs ; msg ; if_highlight } = warning in - match phrase, locs with + let {Toploop_results.locs; msg; if_highlight} = warning in + match (phrase, locs) with | None, _ | _, [] -> let pre = - Tyxml_js.Html5.(pre ~a: [ a_class [ "toplevel-warning" ] ] - [ txt msg ]) in + Tyxml_js.Html5.(pre ~a:[a_class ["toplevel-warning"]] [txt msg]) + in insert output ?phrase (Warning (0, warning, pre)) pre | Some phrase, _ -> - phrase.warnings <- phrase.warnings + 1 ; + phrase.warnings <- phrase.warnings + 1; hilight "toplevel-hilighted-warning" output phrase locs - [ Ref phrase.warnings ] ; + [Ref phrase.warnings]; let pre = - Tyxml_js.Html5.(pre ~a: [ a_class [ "toplevel-warning" ] ] - [ span ~a: [ a_class [ "ref" ] ] - [ txt (string_of_int phrase.warnings) ] ; - txt ":" ; - txt if_highlight ]) in + Tyxml_js.Html5.( + pre + ~a:[a_class ["toplevel-warning"]] + [ span ~a:[a_class ["ref"]] [txt (string_of_int phrase.warnings)] + ; txt ":" + ; txt if_highlight ]) + in insert output ~phrase (Warning (phrase.warnings, warning, pre)) pre let clear output = - Js_utils.Manip.removeChildren output.container ; + Js_utils.Manip.removeChildren output.container; output.blocks <- [] let oldify output = List.iter (fun elt -> Js_utils.Manip.addClass elt "old") - (Js_utils.Manip.children output.container) ; + (Js_utils.Manip.children output.container); output.blocks <- [] -let format_ocaml_code code = - pretty_html (fst (prettify_ocaml code)) +let format_ocaml_code code = pretty_html (fst (prettify_ocaml code)) diff --git a/src/toplevel/learnocaml_toplevel_output.mli b/src/toplevel/learnocaml_toplevel_output.mli index e30d7bff7..c8f9049e9 100644 --- a/src/toplevel/learnocaml_toplevel_output.mli +++ b/src/toplevel/learnocaml_toplevel_output.mli @@ -11,6 +11,12 @@ (** A toplevel output console handle. *) type output +val setup : + ?limit:int + -> ?on_resize:(unit -> unit) + -> container:[`Div] Tyxml_js.Html5.elt + -> unit + -> output (** Use a given div as an output console. Gives it the class [toplevel-output]. @@ -18,28 +24,24 @@ type output A callback, called every time the contents of the console is updated. @param limit The maximum number of blocks displayed. *) -val setup : - ?limit: int -> - ?on_resize:(unit -> unit) -> - container: [ `Div ] Tyxml_js.Html5.elt -> - unit -> output -(** Empty the console. *) val clear : output -> unit +(** Empty the console. *) +val scroll : output -> unit (** Make the last element of the console visible on the screen. This works if the div itself is scrollable (CSS attribute [overflow-y]). If not, the [on_resize] parameter of {!setup} can be used to scroll a parent scrollable area. The console is automatically scrolled after each output.*) -val scroll : output -> unit -(** Make the current elements of the console greyed and unselectable. *) val oldify : output -> unit +(** Make the current elements of the console greyed and unselectable. *) (** Represents a sequence of unseparated output blocks. *) type phrase +val phrase : unit -> phrase (** Forges a new phrase identifier. Phrases are subsequences of outputs in the console, separated by @@ -61,43 +63,43 @@ type phrase is specified, it is considered only if it is the last one in the console. Otherwise, it is appended to the console, as if no phrase was specified. *) -val phrase : unit -> phrase +val output_stdout : ?phrase:phrase -> output -> string -> unit (** Output verbatim text to the console. Successive outputs are grouped, mixing {!output_stdout} and {!output_stderr}. The output block is a direct child of the console, a [pre] element with class [toplevel-output]. The text is wrapped in a [span] element with class [stdout]. *) -val output_stdout : ?phrase: phrase -> output -> string -> unit +val output_stderr : ?phrase:phrase -> output -> string -> unit (** Output verbatim error text. See {!output_stdout}. The text is wrapped in a [span] element with class [stdout]. *) -val output_stderr : ?phrase: phrase -> output -> string -> unit +val output_html : ?phrase:phrase -> output -> string -> unit (** Output HTML in a [div] element with class [toplevel-html-block]. *) -val output_html : ?phrase: phrase -> output -> string -> unit -val output_svg : ?phrase: phrase -> output -> string -> unit +val output_svg : ?phrase:phrase -> output -> string -> unit +val output_code : ?phrase:phrase -> output -> string -> unit (** Output ocaml code in a [pre] element with class [toplevel-code]. Code tokens are wrapped in [span] elements with classes as documented in {!Ocaml_mode.token_type}. An intermediate level of [span] elements with classes [toplevel-hilighted-error] and [toplevel-hilighted-warning] are used for errors and warnings. A [span] with class [ref] is used for location labels. *) -val output_code : ?phrase: phrase -> output -> string -> unit +val output_answer : ?phrase:phrase -> output -> string -> unit (** Output an ocaml toplevel answer in a [pre] element with class [toplevel-answer]. *) -val output_answer : ?phrase: phrase -> output -> string -> unit +val output_error : ?phrase:phrase -> output -> Toploop_results.error -> unit (** Output an error in a [pre] element with class [toplevel-error]. A [span] with class [ref] is used for location labels. *) -val output_error : ?phrase: phrase -> output -> Toploop_results.error -> unit +val output_warning : + ?phrase:phrase -> output -> Toploop_results.warning -> unit (** Output a warning in a [pre] element with class [toplevel-warning]. A [span] with class [ref] is used for location labels. *) -val output_warning : ?phrase: phrase -> output -> Toploop_results.warning -> unit +val format_ocaml_code : string -> [> `Span | `PCDATA] Tyxml_js.Html5.elt list (** Format OCaml code in the style of {!output_code}. *) -val format_ocaml_code : string -> [> `Span | `PCDATA ] Tyxml_js.Html5.elt list diff --git a/src/toplevel/learnocaml_toplevel_pp.ml b/src/toplevel/learnocaml_toplevel_pp.ml index 8ee1bd6af..0eeb8fe35 100644 --- a/src/toplevel/learnocaml_toplevel_pp.ml +++ b/src/toplevel/learnocaml_toplevel_pp.ml @@ -6,7 +6,6 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) - (** [construct_image img] renders an image as a svg code string. This code is used to display the image in the toplevel, using the toplevel directive "#install_printer". @@ -26,8 +25,5 @@ let construct_image i = (* Prelude for pretty printers *) let prelude_pp = "let pp_svg _ i = construct_image i |> print_svg;;" - (* List of pretty printers to deploy in toplevel *) -let pp_list = [ - "pp_svg"; -] +let pp_list = ["pp_svg"] diff --git a/src/toplevel/learnocaml_toplevel_worker_caller.ml b/src/toplevel/learnocaml_toplevel_worker_caller.ml index a39e7d82a..d0dc3b01d 100644 --- a/src/toplevel/learnocaml_toplevel_worker_caller.ml +++ b/src/toplevel/learnocaml_toplevel_worker_caller.ml @@ -10,64 +10,68 @@ open Js_of_ocaml let debug = ref false -let (>>=) = Lwt.bind -let (>>?) o f = +let ( >>= ) = Lwt.bind + +let ( >>? ) o f = let open Toploop_results in - o >>= function + o + >>= function | Error (err, w) -> Lwt.return (Error (err, w)) - | Ok (x, w) -> - f x >>= function + | Ok (x, w) -> ( + f x + >>= function | Error (err, w') -> Lwt.return (Error (err, w @ w')) - | Ok (x, w') -> Lwt.return (Ok (x, w @ w')) + | Ok (x, w') -> Lwt.return (Ok (x, w @ w')) ) let return_success e = Lwt.return (Toploop_results.Ok (e, [])) + let return_unit_success = return_success () let wrap pp = let buf = Buffer.create 503 in let flush () = let s = Buffer.contents buf in - if s <> "" then begin - Buffer.reset buf; - pp s - end in + if s <> "" then ( Buffer.reset buf; pp s ) + in Format.make_formatter (Buffer.add_substring buf) flush -module IntMap = Map.Make(struct - type t = int - let compare (x:int) (y:int) = Pervasives.compare x y - end) -let map_option f o = match o with | None -> None | Some o -> Some (f o) -let iter_option f o = match o with | None -> () | Some o -> f o +module IntMap = Map.Make (struct + type t = int + + let compare (x : int) (y : int) = Pervasives.compare x y +end) +let map_option f o = match o with None -> None | Some o -> Some (f o) + +let iter_option f o = match o with None -> () | Some o -> f o open Worker open Learnocaml_toplevel_worker_messages type u = - U : 'a msg_ty * 'a Toploop_results.toplevel_result Lwt.u - * 'a Toploop_results.toplevel_result Lwt.t -> u - -type t = { - js_file: string; - mutable worker: (Js.js_string Js.t, Js.js_string Js.t) worker Js.t; - mutable wakeners: u IntMap.t; - mutable counter: int; - mutable fds: (string -> unit) IntMap.t; - mutable fd_counter: int; - mutable reset_worker: t -> unit Lwt.t; - mutable after_init: t -> unit Lwt.t; - pp_stdout: string -> unit; - pp_stderr: string -> unit; -} + | U : + 'a msg_ty + * 'a Toploop_results.toplevel_result Lwt.u + * 'a Toploop_results.toplevel_result Lwt.t + -> u +type t = + { js_file : string + ; mutable worker : (Js.js_string Js.t, Js.js_string Js.t) worker Js.t + ; mutable wakeners : u IntMap.t + ; mutable counter : int + ; mutable fds : (string -> unit) IntMap.t + ; mutable fd_counter : int + ; mutable reset_worker : t -> unit Lwt.t + ; mutable after_init : t -> unit Lwt.t + ; pp_stdout : string -> unit + ; pp_stderr : string -> unit } exception Not_equal -let check_equal - : type t1 t2. - t1 msg_ty -> - t2 msg_ty -> (t1, t2) eq = fun ty1 ty2 -> - match ty1, ty2 with + +let check_equal : type t1 t2. t1 msg_ty -> t2 msg_ty -> (t1, t2) eq = + fun ty1 ty2 -> + match (ty1, ty2) with | Unit, Unit -> Eq | Bool, Bool -> Eq | Int, Int -> Eq @@ -79,58 +83,54 @@ let check_equal let onmessage worker (ev : _ Worker.messageEvent Js.t) = match Json.unsafe_input ev##.data with - | Write (fd, s) -> begin + | Write (fd, s) -> ( if !debug then Js_utils.debug "Host: Write %d %S" fd s; try - IntMap.find fd worker.fds s ; + IntMap.find fd worker.fds s; Js._false - with - | Not_found -> - Firebug.console##(warn - (Js.string (Printf.sprintf "Missing channels (%d)" fd))); - Js._false - end - | ReturnSuccess (id, ty_v, v, w) -> begin + with Not_found -> + Firebug.console + ## (warn (Js.string (Printf.sprintf "Missing channels (%d)" fd))); + Js._false ) + | ReturnSuccess (id, ty_v, v, w) -> ( if !debug then Js_utils.debug "Host: ReturnOk %d" id; try - let U (ty_u, u, _) = IntMap.find id worker.wakeners in + let (U (ty_u, u, _)) = IntMap.find id worker.wakeners in let Eq = check_equal ty_u ty_v in worker.wakeners <- IntMap.remove id worker.wakeners; Lwt.wakeup u (Toploop_results.Ok (v, w)); Js._false with | Not_found -> - Firebug.console##(warn - (Js.string (Printf.sprintf "Missing wakeners (%d)" id))); + Firebug.console + ## (warn (Js.string (Printf.sprintf "Missing wakeners (%d)" id))); Js._false | Not_equal -> - Firebug.console##(warn - (Js.string (Printf.sprintf "Unexpected wakeners (%d)" id))); - Js._false - end - | ReturnError (id, e, w) -> begin + Firebug.console + ## (warn (Js.string (Printf.sprintf "Unexpected wakeners (%d)" id))); + Js._false ) + | ReturnError (id, e, w) -> ( if !debug then Js_utils.debug "Host: Error %d" worker.counter; try - let U (_, u, _) = IntMap.find id worker.wakeners in + let (U (_, u, _)) = IntMap.find id worker.wakeners in worker.wakeners <- IntMap.remove id worker.wakeners; Lwt.wakeup u (Toploop_results.Error (e, w)); Js._false with Not_found -> - Firebug.console##(warn - (Js.string (Printf.sprintf "Missing wakeners (%d)" id))); - Js._false - end + Firebug.console + ## (warn (Js.string (Printf.sprintf "Missing wakeners (%d)" id))); + Js._false ) let terminate worker = - (worker.worker)##terminate ; + (worker.worker)##terminate; IntMap.iter (fun id (U (_, _, t)) -> - worker.wakeners <- IntMap.remove id worker.wakeners; - Lwt.cancel t) + worker.wakeners <- IntMap.remove id worker.wakeners; + Lwt.cancel t ) worker.wakeners - -let never_ending = (* and not cancellable. *) +let never_ending = + (* and not cancellable. *) fst (Lwt.wait ()) let ty_of_host_msg : type t. t host_msg -> t msg_ty = function @@ -148,68 +148,66 @@ let ty_of_host_msg : type t. t host_msg -> t msg_ty = function [onmessage] by calling [Lwt.wakeup]. They should never end with an exception, unless canceled. When canceled, the worker is killed and a new one is spawned. *) -let rec post : type a. t -> a host_msg -> a Toploop_results.toplevel_result Lwt.t = - fun worker msg -> - let msg_id = worker.counter in - let msg_ty = ty_of_host_msg msg in - if !debug then Js_utils.debug "Host: queuing %d" msg_id; - let (t, u) = Lwt.task () in - Lwt.on_cancel t - (fun () -> Lwt.async (fun () -> worker.reset_worker worker)); - worker.wakeners <- IntMap.add msg_id (U (msg_ty, u, t)) worker.wakeners; - worker.counter <- msg_id + 1; - worker.worker##(postMessage (Json.output (msg_id, msg))); - t +let rec post : type a. + t -> a host_msg -> a Toploop_results.toplevel_result Lwt.t = + fun worker msg -> + let msg_id = worker.counter in + let msg_ty = ty_of_host_msg msg in + if !debug then Js_utils.debug "Host: queuing %d" msg_id; + let t, u = Lwt.task () in + Lwt.on_cancel t (fun () -> Lwt.async (fun () -> worker.reset_worker worker)); + worker.wakeners <- IntMap.add msg_id (U (msg_ty, u, t)) worker.wakeners; + worker.counter <- msg_id + 1; + (worker.worker) ## (postMessage (Json.output (msg_id, msg))); + t and do_reset_worker () = let running = ref true in fun worker -> - if !running then begin + if !running then ( if !debug then Js_utils.debug "Host: do_reset_worker"; running := false; terminate worker; IntMap.iter (* GRGR: Peut-on 'cancel' directement le Lwt.u ? *) - (fun _ (U (_, _, t)) -> Lwt.cancel t) + (fun _ (U (_, _, t)) -> Lwt.cancel t ) worker.wakeners; - worker.worker <- Worker.create (worker.js_file); - worker.fds <- - IntMap.empty |> - IntMap.add 0 (IntMap.find 0 worker.fds) |> - IntMap.add 1 (IntMap.find 1 worker.fds); + worker.worker <- Worker.create worker.js_file; + worker.fds + <- IntMap.empty + |> IntMap.add 0 (IntMap.find 0 worker.fds) + |> IntMap.add 1 (IntMap.find 1 worker.fds); worker.fd_counter <- 2; worker.wakeners <- IntMap.empty; worker.counter <- 0; worker.reset_worker <- do_reset_worker (); - (Obj.magic worker.worker)##.onmessage := - Js.wrap_callback (onmessage worker); - post worker @@ Init >>= fun _ -> - worker.after_init worker >>= fun _ -> - Lwt.return_unit - end else - Lwt.return_unit - -let create - ?(js_file = "/js/learnocaml-toplevel-worker.js") + (Obj.magic worker.worker)##.onmessage + := Js.wrap_callback (onmessage worker); + post worker @@ Init + >>= fun _ -> worker.after_init worker >>= fun _ -> Lwt.return_unit ) + else Lwt.return_unit + +let create ?(js_file = "/js/learnocaml-toplevel-worker.js") ?(after_init = fun _ -> Lwt.return_unit) - ?(pp_stdout = (fun text -> Firebug.console##(log (Js.string text)))) - ?(pp_stderr = (fun text -> Firebug.console##(log (Js.string text)))) - () = + ?(pp_stdout = fun text -> Firebug.console ## (log (Js.string text))) + ?(pp_stderr = fun text -> Firebug.console ## (log (Js.string text))) () = let worker = Worker.create js_file in - let fds = - IntMap.empty |> - IntMap.add 0 pp_stdout |> - IntMap.add 1 pp_stderr in + let fds = IntMap.empty |> IntMap.add 0 pp_stdout |> IntMap.add 1 pp_stderr in let worker = - { worker; js_file; - wakeners = IntMap.empty; counter = 0; fds; fd_counter = 2; - reset_worker = do_reset_worker (); - after_init; pp_stdout; pp_stderr; - } in + { worker + ; js_file + ; wakeners = IntMap.empty + ; counter = 0 + ; fds + ; fd_counter = 2 + ; reset_worker = do_reset_worker () + ; after_init + ; pp_stdout + ; pp_stderr } + in (Obj.magic worker.worker)##.onmessage := Js.wrap_callback (onmessage worker); - post worker @@ Init >>= fun _ -> - worker.after_init worker >>= fun () -> - Lwt.return worker + post worker @@ Init + >>= fun _ -> worker.after_init worker >>= fun () -> Lwt.return worker let create_fd worker pp = worker.fds <- IntMap.add worker.fd_counter pp worker.fds; @@ -217,20 +215,18 @@ let create_fd worker pp = worker.fd_counter <- fd + 1; fd -let close_fd worker fd = - worker.fds <- IntMap.remove fd worker.fds +let close_fd worker fd = worker.fds <- IntMap.remove fd worker.fds let reset worker ?(timeout = fun () -> never_ending) () = if !debug then Js_utils.debug "Host: reset"; let timeout = timeout () in - Lwt.choose [ - ( post worker Reset >>= fun res -> Lwt.return (`Reset res) ); - ( timeout >>= fun () -> Lwt.return `Timeout ); - ] >>= function - | `Reset Toploop_results.Ok ((), _) -> - Lwt.cancel timeout; - worker.after_init worker - | `Reset Toploop_results.Error (err, _) -> + Lwt.choose + [ (post worker Reset >>= fun res -> Lwt.return (`Reset res)) + ; (timeout >>= fun () -> Lwt.return `Timeout) ] + >>= function + | `Reset (Toploop_results.Ok ((), _)) -> + Lwt.cancel timeout; worker.after_init worker + | `Reset (Toploop_results.Error (err, _)) -> Lwt.cancel timeout; worker.pp_stderr err.Toploop_results.msg; worker.reset_worker worker @@ -238,40 +234,33 @@ let reset worker ?(timeout = fun () -> never_ending) () = (* Not canceling the Reset thread, but manually resetting. *) worker.reset_worker worker -let check worker code = - post worker @@ Check code +let check worker code = post worker @@ Check code -let set_checking_environment worker = - post worker @@ Set_checking_environment +let set_checking_environment worker = post worker @@ Set_checking_environment let execute worker ?pp_code ~pp_answer ~print_outcome code = let pp_code = map_option (create_fd worker) pp_code in let pp_answer = create_fd worker pp_answer in - post worker @@ - Execute (pp_code, print_outcome, pp_answer, code) >>= fun result -> + post worker @@ Execute (pp_code, print_outcome, pp_answer, code) + >>= fun result -> iter_option (close_fd worker) pp_code; close_fd worker pp_answer; Lwt.return result let use_string worker ?filename ~pp_answer ~print_outcome code = let pp_answer = create_fd worker pp_answer in - post worker @@ - Use_string (filename, print_outcome, pp_answer, code) >>= fun result -> - close_fd worker pp_answer; - Lwt.return result + post worker @@ Use_string (filename, print_outcome, pp_answer, code) + >>= fun result -> close_fd worker pp_answer; Lwt.return result -let use_mod_string worker - ~pp_answer ~print_outcome ~modname ?sig_code impl_code = +let use_mod_string worker ~pp_answer ~print_outcome ~modname ?sig_code + impl_code = let pp_answer = create_fd worker pp_answer in - post worker @@ - Use_mod_string (pp_answer, print_outcome, modname, sig_code, impl_code) - >>= fun result -> - close_fd worker pp_answer; - Lwt.return result + post worker + @@ Use_mod_string (pp_answer, print_outcome, modname, sig_code, impl_code) + >>= fun result -> close_fd worker pp_answer; Lwt.return result let set_after_init w after_init = w.after_init <- after_init let register_callback worker name callback = let fd = create_fd worker callback in - post worker (Register_callback (name, fd)) >>? fun () -> - return_unit_success + post worker (Register_callback (name, fd)) >>? fun () -> return_unit_success diff --git a/src/toplevel/learnocaml_toplevel_worker_caller.mli b/src/toplevel/learnocaml_toplevel_worker_caller.mli index 6a4f24692..24b90dd51 100644 --- a/src/toplevel/learnocaml_toplevel_worker_caller.mli +++ b/src/toplevel/learnocaml_toplevel_worker_caller.mli @@ -18,7 +18,13 @@ open Toploop_results (** An abstract type representing a toplevel instance. *) type t - +val create : + ?js_file:string + -> ?after_init:(t -> unit Lwt.t) + -> ?pp_stdout:(string -> unit) + -> ?pp_stderr:(string -> unit) + -> unit + -> t Lwt.t (** Create a toplevel instance. @param after_init a function that will be called whenever the @@ -33,23 +39,22 @@ type t @param js_file the web worker [.js] file. (default: ["/js/learnocaml-toplevel-worker.js"]). *) -val create: - ?js_file: string -> - ?after_init:(t -> unit Lwt.t) -> - ?pp_stdout:(string -> unit) -> - ?pp_stderr:(string -> unit) -> - unit -> t Lwt.t - +val check : t -> string -> unit toplevel_result Lwt.t (** Parse and typecheck a given source code @return [Success ()] in case of success and [Error err] where [err] contains the error message otherwise. *) -val check: t -> string -> unit toplevel_result Lwt.t - +val execute : + t + -> ?pp_code:(string -> unit) + -> pp_answer:(string -> unit) + -> print_outcome:bool + -> string + -> bool toplevel_result Lwt.t (** Execute a given source code. The evaluation stops after the first toplevel phrase (as terminated by ";;") that fails to compile or for which the evaluation raises an uncaught exception. @@ -71,17 +76,17 @@ val check: t -> string -> unit toplevel_result Lwt.t exception, and [Success false] otherwise. *) -val execute: - t -> - ?pp_code:(string -> unit) -> - pp_answer:(string -> unit) -> - print_outcome:bool -> - string -> bool toplevel_result Lwt.t +val set_checking_environment : t -> unit toplevel_result Lwt.t (** Freezes the environment for future calls to {!check}. *) -val set_checking_environment: - t -> unit toplevel_result Lwt.t +val use_string : + t + -> ?filename:string + -> pp_answer:(string -> unit) + -> print_outcome:bool + -> string + -> bool toplevel_result Lwt.t (** Execute a given source code. The code is parsed and typechecked all at once before to start the evaluation. @@ -92,14 +97,15 @@ val set_checking_environment: @return as {!val:execute}. *) -val use_string: - t -> - ?filename: string -> - pp_answer:(string -> unit) -> - print_outcome:bool -> - string -> bool toplevel_result Lwt.t - +val use_mod_string : + t + -> pp_answer:(string -> unit) + -> print_outcome:bool + -> modname:string + -> ?sig_code:string + -> string + -> bool toplevel_result Lwt.t (** Wrap a given source code into a module and bind it with a given name. @param pp_answer see {!val:execute}. @@ -114,29 +120,23 @@ val use_string: @return as {!val:execute}. *) -val use_mod_string: - t -> - pp_answer:(string -> unit) -> - print_outcome:bool -> - modname:string -> - ?sig_code:string -> - string -> bool toplevel_result Lwt.t +val register_callback : + t -> string -> (string -> unit) -> unit toplevel_result Lwt.t (** Insert a callback in the toplevel environment. *) -val register_callback : t -> string -> (string -> unit) -> unit toplevel_result Lwt.t +val reset : t -> ?timeout:(unit -> unit Lwt.t) -> unit -> unit Lwt.t (** Reset the current toplevel environment to the initial environment. *) -val reset: t -> ?timeout:(unit -> unit Lwt.t) -> unit -> unit Lwt.t - +val terminate : t -> unit (** Terminate the toplevel, i.e. destroy the Web Worker. It does nothing if the toplevel as been created with [async=false]. *) -val terminate: t -> unit -val set_after_init: t -> (t -> unit Lwt.t) -> unit +val set_after_init : t -> (t -> unit Lwt.t) -> unit (**/**) -val debug: bool ref -val wrap: (string -> unit) -> Format.formatter +val debug : bool ref + +val wrap : (string -> unit) -> Format.formatter diff --git a/src/toplevel/learnocaml_toplevel_worker_main.ml b/src/toplevel/learnocaml_toplevel_worker_main.ml index 03d0eeacc..3d8fc4671 100644 --- a/src/toplevel/learnocaml_toplevel_worker_main.ml +++ b/src/toplevel/learnocaml_toplevel_worker_main.ml @@ -11,7 +11,7 @@ open Learnocaml_toplevel_worker_messages let debug = ref false -let (>>=) = Lwt.bind +let ( >>= ) = Lwt.bind let is_success = function | Toploop_ext.Ok _ -> true @@ -22,21 +22,23 @@ type 'a return = | ReturnError of Toploop_ext.error * Toploop_ext.warning list let return_success v w = Lwt.return (ReturnSuccess (v, w)) + let return_unit_success = return_success () [] + let return_error e w = Lwt.return (ReturnError (e, w)) let unwrap_result = let open Toploop_ext in function - | Ok (b, w) -> return_success b w - | Error (err, w) -> return_error err w + | Ok (b, w) -> return_success b w | Error (err, w) -> return_error err w (** File descriptors *) -module IntMap = Map.Make(struct - type t = int - let compare (x:int) (y:int) = Pervasives.compare x y - end) +module IntMap = Map.Make (struct + type t = int + + let compare (x : int) (y : int) = Pervasives.compare x y +end) (* Limit the frequency of sent messages to one per ms, using an active loop (yuck) because, well, there is no other concurrency primitive @@ -55,77 +57,74 @@ module IntMap = Map.Make(struct still need some kind of active waiting to limit throughput. All in all this spinwait is not that ugly. *) let last = ref 0. + let rec wait () = let now = Sys.time () (* let's hope this yields a bit *) in - if now -. !last > 0.001 then - last := now - else wait () + if now -. !last > 0.001 then last := now else wait () -let post_message (m: toploop_msg) = - wait () ; +let post_message (m : toploop_msg) = + wait (); Worker.post_message (Json.output m) -let (wrap_fd, close_fd, clear_fds) = +let wrap_fd, close_fd, clear_fds = let fds = ref IntMap.empty in let wrap_fd fd = - try IntMap.find fd !fds - with Not_found -> + try IntMap.find fd !fds with Not_found -> let buf = Buffer.create 503 in let flush () = let s = Buffer.contents buf in - if s <> "" then begin + if s <> "" then ( Buffer.reset buf; if !debug then Js_utils.debug "Worker: <- Write %d %S" fd s; - post_message (Write (fd, s)) - end in + post_message (Write (fd, s)) ) + in let ppf = Format.make_formatter (Buffer.add_substring buf) flush in fds := IntMap.add fd ppf !fds; - ppf in + ppf + in let close_fd fd = - if IntMap.mem fd !fds then (Format.pp_print_flush (IntMap.find fd !fds) ()); - fds := IntMap.remove fd !fds in + if IntMap.mem fd !fds then Format.pp_print_flush (IntMap.find fd !fds) (); + fds := IntMap.remove fd !fds + in let clear_fds () = fds := IntMap.fold (fun id ppf fds -> - Format.pp_print_flush ppf (); - if id = 0 || id = 1 then - IntMap.add id ppf fds - else - fds) - !fds - IntMap.empty in + Format.pp_print_flush ppf (); + if id = 0 || id = 1 then IntMap.add id ppf fds else fds ) + !fds IntMap.empty + in (wrap_fd, close_fd, clear_fds) let stdout_ppf = wrap_fd 0 + let stderr_ppf = wrap_fd 1 let () = - Sys_js.set_channel_flusher stdout - (fun s -> - Format.pp_print_string stdout_ppf s; - Format.pp_print_flush stdout_ppf ()); - Sys_js.set_channel_flusher stderr - (fun s -> - Format.pp_print_string stderr_ppf s; - Format.pp_print_flush stderr_ppf ()) + Sys_js.set_channel_flusher stdout (fun s -> + Format.pp_print_string stdout_ppf s; + Format.pp_print_flush stdout_ppf () ); + Sys_js.set_channel_flusher stderr (fun s -> + Format.pp_print_string stderr_ppf s; + Format.pp_print_flush stderr_ppf () ) let make_answer_ppf fd_answer = let orig_print_string, orig_flush = - Format.pp_get_formatter_output_functions (wrap_fd fd_answer) () in + Format.pp_get_formatter_output_functions (wrap_fd fd_answer) () + in let check_first_call = let first_call = ref true in fun () -> - if !first_call then begin - flush stdout ; - flush stderr ; - Format.(pp_print_flush std_formatter ()) ; - Format.(pp_print_flush err_formatter ()) ; - first_call := false ; - end in + if !first_call then ( + flush stdout; + flush stderr; + Format.(pp_print_flush std_formatter ()); + Format.(pp_print_flush err_formatter ()); + first_call := false ) + in Format.make_formatter - (fun str -> check_first_call () ; orig_print_string str) - (fun () -> check_first_call () ; orig_flush ()) + (fun str -> check_first_call (); orig_print_string str) + (fun () -> check_first_call (); orig_flush ()) (** Code compilation and execution *) @@ -133,17 +132,17 @@ let make_answer_ppf fd_answer = (** Message dispatcher *) -let map_option f o = match o with | None -> None | Some o -> Some (f o) -let iter_option f o = match o with | None -> () | Some o -> f o +let map_option f o = match o with None -> None | Some o -> Some (f o) + +let iter_option f o = match o with None -> () | Some o -> f o let checking_environment = ref !Toploop.toplevel_env let handler : type a. a host_msg -> a return Lwt.t = function | Set_checking_environment -> - checking_environment := !Toploop.toplevel_env ; - return_unit_success - | Init -> + checking_environment := !Toploop.toplevel_env; return_unit_success + | Init -> return_unit_success | Reset -> if !debug then Js_utils.debug "Worker: -> Reset"; clear_fds (); @@ -154,16 +153,20 @@ let handler : type a. a host_msg -> a return Lwt.t = function let ppf_code = map_option wrap_fd fd_code in let ppf_answer = make_answer_ppf fd_answer in if !debug then Js_utils.debug "Worker: -> Execute (%S)" code; - let result = Toploop_ext.execute ?ppf_code ~print_outcome ~ppf_answer code in - if !debug then Js_utils.debug "Worker: <- Execute (%B)" (is_success result); + let result = + Toploop_ext.execute ?ppf_code ~print_outcome ~ppf_answer code + in + if !debug then + Js_utils.debug "Worker: <- Execute (%B)" (is_success result); iter_option close_fd fd_code; close_fd fd_answer; unwrap_result result | Use_string (filename, print_outcome, fd_answer, code) -> let ppf_answer = make_answer_ppf fd_answer in - if !debug then - Js_utils.debug "Worker: -> Use_string (%S)" code; - let result = Toploop_ext.use_string ?filename ~print_outcome ~ppf_answer code in + if !debug then Js_utils.debug "Worker: -> Use_string (%S)" code; + let result = + Toploop_ext.use_string ?filename ~print_outcome ~ppf_answer code + in if !debug then Js_utils.debug "Worker: <- Use_string (%B)" (is_success result); close_fd fd_answer; @@ -171,13 +174,14 @@ let handler : type a. a host_msg -> a return Lwt.t = function | Use_mod_string (fd_answer, print_outcome, modname, sig_code, impl_code) -> let ppf_answer = make_answer_ppf fd_answer in if !debug then - Js_utils.debug - "Worker: -> Use_mod_string %s (%S)" modname impl_code; - let result = Toploop_ext.use_mod_string - ~ppf_answer ~print_outcome ~modname ?sig_code impl_code in + Js_utils.debug "Worker: -> Use_mod_string %s (%S)" modname impl_code; + let result = + Toploop_ext.use_mod_string ~ppf_answer ~print_outcome ~modname + ?sig_code impl_code + in if !debug then - Js_utils.debug - "Worker: <- Use_mod_string %s (%B)" modname (is_success result); + Js_utils.debug "Worker: <- Use_mod_string %s (%B)" modname + (is_success result); close_fd fd_answer; unwrap_result result | Set_debug b -> @@ -185,33 +189,40 @@ let handler : type a. a host_msg -> a return Lwt.t = function return_unit_success | Register_callback (name, fd) -> let callback text = - post_message (Write (fd, text)) ; () in + post_message (Write (fd, text)); + () + in let ty = let ast = let arg = - Ast_helper.(Typ.constr (Location.mknoloc (Longident.Lident "string")) []) in + Ast_helper.( + Typ.constr (Location.mknoloc (Longident.Lident "string")) []) + in let ret = - Ast_helper.(Typ.constr (Location.mknoloc (Longident.Lident "unit")) []) in - { Parsetree.ptyp_desc = Parsetree.Ptyp_arrow (Asttypes.Nolabel, arg, ret) ; - ptyp_loc = Location.none ; - ptyp_attributes = [] } in - Typetexp.transl_type_scheme !Toploop.toplevel_env ast in + Ast_helper.( + Typ.constr (Location.mknoloc (Longident.Lident "unit")) []) + in + { Parsetree.ptyp_desc = + Parsetree.Ptyp_arrow (Asttypes.Nolabel, arg, ret) + ; ptyp_loc = Location.none + ; ptyp_attributes = [] } + in + Typetexp.transl_type_scheme !Toploop.toplevel_env ast + in Toploop.toplevel_env := - Env.add_value - (Ident.create name) - { Types. - val_type = ty.Typedtree.ctyp_type; - val_kind = Types.Val_reg; - val_attributes = []; - val_loc = Location.none } - !Toploop.toplevel_env ; - Toploop.setvalue name (Obj.repr callback) ; + Env.add_value (Ident.create name) + { Types.val_type = ty.Typedtree.ctyp_type + ; val_kind = Types.Val_reg + ; val_attributes = [] + ; val_loc = Location.none } + !Toploop.toplevel_env; + Toploop.setvalue name (Obj.repr callback); return_unit_success | Check code -> let saved = !Toploop.toplevel_env in - Toploop.toplevel_env := !checking_environment ; + Toploop.toplevel_env := !checking_environment; let result = Toploop_ext.check code in - Toploop.toplevel_env := saved ; + Toploop.toplevel_env := saved; unwrap_result result let ty_of_host_msg : type t. t host_msg -> t msg_ty = function @@ -227,29 +238,30 @@ let ty_of_host_msg : type t. t host_msg -> t msg_ty = function let () = let handler (type t) data = - let (id, data) : (int * t host_msg) = Json.unsafe_input data in + let (id, data) : int * t host_msg = Json.unsafe_input data in let ty = ty_of_host_msg data in - handler data >>= function + handler data + >>= function | ReturnSuccess (v, w) -> post_message (ReturnSuccess (id, ty, v, w)); Lwt.return_unit | ReturnError (res, w) -> - if !debug then - Js_utils.debug "Worker: <- ReturnError %d" id; + if !debug then Js_utils.debug "Worker: <- ReturnError %d" id; post_message (ReturnError (id, res, w)); Lwt.return_unit in let path = "/worker_cmis" in - Sys_js.mount ~path - (fun ~prefix:_ ~path -> - match OCamlRes.Res.find (OCamlRes.Path.of_string path) Embedded_cmis.root with - | cmi -> - Js.Unsafe.set cmi (Js.string "t") 9 ; (* XXX hack *) - Some cmi - | exception Not_found -> None) ; - Config.load_path := [ path ] ; + Sys_js.mount ~path (fun ~prefix:_ ~path -> + match + OCamlRes.Res.find (OCamlRes.Path.of_string path) Embedded_cmis.root + with + | cmi -> + Js.Unsafe.set cmi (Js.string "t") 9; + (* XXX hack *) + Some cmi + | exception Not_found -> None ); + Config.load_path := [path]; Toploop_jsoo.initialize (); - Hashtbl.add Toploop.directive_table - "debug_worker" + Hashtbl.add Toploop.directive_table "debug_worker" (Toploop.Directive_bool (fun b -> debug := b)); Worker.set_onmessage (fun s -> Lwt.async (fun () -> handler s)) diff --git a/src/toplevel/learnocaml_toplevel_worker_main.mli b/src/toplevel/learnocaml_toplevel_worker_main.mli index f53cf8310..a753a5d70 100644 --- a/src/toplevel/learnocaml_toplevel_worker_main.mli +++ b/src/toplevel/learnocaml_toplevel_worker_main.mli @@ -7,5 +7,4 @@ * included LICENSE file for details. *) (** Events loop for the toplevel's Web Worker. *) - (* Empty *) diff --git a/src/toplevel/learnocaml_toplevel_worker_messages.mli b/src/toplevel/learnocaml_toplevel_worker_messages.mli index 1906d2520..a748a5c73 100644 --- a/src/toplevel/learnocaml_toplevel_worker_messages.mli +++ b/src/toplevel/learnocaml_toplevel_worker_messages.mli @@ -15,7 +15,9 @@ type _ host_msg = | Reset : unit host_msg | Execute : int option * bool * int * string -> bool host_msg | Use_string : string option * bool * int * string -> bool host_msg - | Use_mod_string : int * bool * string * string option * string -> bool host_msg + | Use_mod_string : + int * bool * string * string option * string + -> bool host_msg | Set_debug : bool -> unit host_msg | Register_callback : string * int -> unit host_msg | Set_checking_environment : unit host_msg @@ -30,8 +32,11 @@ type _ msg_ty = type (_, _) eq = Eq : ('a, 'a) eq type toploop_msg = - | Write : int * string -> toploop_msg (* pseudo file descriptor * content *) + | Write : int * string -> toploop_msg + (* pseudo file descriptor * content *) | ReturnSuccess : - int * 'a msg_ty * 'a * Toploop_results.warning list -> toploop_msg + int * 'a msg_ty * 'a * Toploop_results.warning list + -> toploop_msg | ReturnError : - int * Toploop_results.error * Toploop_results.warning list -> toploop_msg + int * Toploop_results.error * Toploop_results.warning list + -> toploop_msg diff --git a/src/toploop/toploop_ext.ml b/src/toploop/toploop_ext.ml index f8c2084ab..006c604e8 100644 --- a/src/toploop/toploop_ext.ml +++ b/src/toploop/toploop_ext.ml @@ -12,76 +12,68 @@ type 'a toplevel_result = 'a Toploop_results.toplevel_result = | Error of error * warning list and error = Toploop_results.error = - { msg: string; - locs: loc list; - if_highlight: string; } + {msg : string; locs : loc list; if_highlight : string} and warning = error -and loc = Toploop_results.loc = { - loc_start: int * int; - loc_end: int * int; -} +and loc = Toploop_results.loc = {loc_start : int * int; loc_end : int * int} module Ppx = struct - let ppx_rewriters = ref [] let () = Ast_mapper.register_function := - (fun _ f -> ppx_rewriters := f :: !ppx_rewriters) + fun _ f -> ppx_rewriters := f :: !ppx_rewriters let preprocess_structure str = let open Ast_mapper in List.fold_right (fun ppx_rewriter str -> - let mapper = ppx_rewriter [] in - mapper.structure mapper str) - !ppx_rewriters - str + let mapper = ppx_rewriter [] in + mapper.structure mapper str ) + !ppx_rewriters str let preprocess_signature str = let open Ast_mapper in List.fold_right (fun ppx_rewriter str -> - let mapper = ppx_rewriter [] in - mapper.signature mapper str) - !ppx_rewriters - str + let mapper = ppx_rewriter [] in + mapper.signature mapper str ) + !ppx_rewriters str let preprocess_phrase phrase = let open Parsetree in match phrase with | Ptop_def str -> Ptop_def (preprocess_structure str) | Ptop_dir _ as x -> x - end let warnings = ref [] let convert_loc loc = - let _file1,line1,col1 = Location.get_pos_info (loc.Location.loc_start) in - let _file2,line2,col2 = Location.get_pos_info (loc.Location.loc_end) in - { loc_start = (line1, col1) ; loc_end = (line2, col2) } + let _file1, line1, col1 = Location.get_pos_info loc.Location.loc_start in + let _file2, line2, col2 = Location.get_pos_info loc.Location.loc_end in + {loc_start = (line1, col1); loc_end = (line2, col2)} let () = Location.warning_printer := - (fun loc _fmt w -> - if Warnings.is_active w then begin - let buf = Buffer.create 503 in - let ppf = Format.formatter_of_buffer buf in - Location.print ppf loc; - Format.fprintf ppf "Warning %a@." Warnings.print w; - let msg = Buffer.contents buf in - Buffer.reset buf; - Format.fprintf ppf "Warning %a@." Warnings.print w; - let if_highlight = Buffer.contents buf in - let loc = convert_loc loc in - warnings := { msg; locs = [loc]; if_highlight } :: !warnings - end) - -let return_success (e: 'a) : 'a toplevel_result = Ok (e, !warnings) -let return_error e : 'a toplevel_result = Error (e, !warnings) + fun loc _fmt w -> + if Warnings.is_active w then ( + let buf = Buffer.create 503 in + let ppf = Format.formatter_of_buffer buf in + Location.print ppf loc; + Format.fprintf ppf "Warning %a@." Warnings.print w; + let msg = Buffer.contents buf in + Buffer.reset buf; + Format.fprintf ppf "Warning %a@." Warnings.print w; + let if_highlight = Buffer.contents buf in + let loc = convert_loc loc in + warnings := {msg; locs = [loc]; if_highlight} :: !warnings ) + +let return_success (e : 'a) : 'a toplevel_result = Ok (e, !warnings) + +let return_error e : 'a toplevel_result = Error (e, !warnings) + (* let return_unit_success = return_success () *) (** Error handling *) @@ -91,20 +83,23 @@ let rec report_error_rec hg_ppf ppf {Location.loc; msg; sub; if_highlight} = Location.print ppf loc; Format.pp_print_string ppf msg; let hg_ppf = - if if_highlight <> "" then - (Format.pp_print_string hg_ppf if_highlight; dummy_ppf) - else - (Format.pp_print_string hg_ppf msg; hg_ppf) in + if if_highlight <> "" then ( + Format.pp_print_string hg_ppf if_highlight; + dummy_ppf ) + else ( + Format.pp_print_string hg_ppf msg; + hg_ppf ) + in let locs = - List.concat @@ - List.map - (fun err -> - Format.pp_force_newline ppf (); - Format.pp_open_box ppf 2; - let locs = report_error_rec hg_ppf ppf err in - Format.pp_close_box ppf (); - locs) - sub in + List.concat + @@ List.map + (fun err -> + Format.pp_force_newline ppf (); + Format.pp_open_box ppf 2; + let locs = report_error_rec hg_ppf ppf err in + Format.pp_close_box ppf (); locs ) + sub + in convert_loc loc :: locs let report_error err = @@ -117,16 +112,15 @@ let report_error err = Format.pp_print_flush hg_ppf (); let msg = Buffer.contents buf in let if_highlight = Buffer.contents hg_buf in - { msg; locs; if_highlight; } + {msg; locs; if_highlight} let error_of_exn exn = match Location.error_of_exn exn with | None -> - let msg = match exn with - | Failure msg -> msg - | exn -> Printexc.to_string exn + let msg = + match exn with Failure msg -> msg | exn -> Printexc.to_string exn in - { msg; locs = []; if_highlight = msg } + {msg; locs = []; if_highlight = msg} | Some error -> report_error error let return_exn exn = return_error (error_of_exn exn) @@ -137,38 +131,34 @@ let trim_end s = let ws c = c = ' ' || c = '\t' || c = '\n' in let len = String.length s in let stop = ref (len - 1) in - while !stop > 0 && (ws s.[!stop]) - do decr stop done; + while !stop > 0 && ws s.[!stop] do + decr stop + done; String.sub s 0 (!stop + 1) let normalize code = let content = trim_end code in let len = String.length content in - if content = "" then - content - else if (len > 2 - && content.[len - 2] = ';' - && content.[len - 1] = ';') then + if content = "" then content + else if len > 2 && content.[len - 2] = ';' && content.[len - 1] = ';' then content ^ "\n" - else - content ^ " ;;\n" + else content ^ " ;;\n" let refill_lexbuf src ppf = let i = ref 0 in let max_i = String.length src in fun buf len -> - if max_i <= !i then - 0 + if max_i <= !i then 0 else - let (len, nl) = - try min len (String.index_from src !i '\n' - !i + 1), false - with Not_found | Invalid_argument _ -> - min len (max_i - !i), true in - String.blit src !i buf 0 len ; - Format.pp_print_string ppf (Bytes.sub_string buf 0 len) ; - if nl then Format.pp_print_newline ppf () ; - Format.pp_print_flush ppf () ; - i := !i + len ; + let len, nl = + try (min len (String.index_from src !i '\n' - !i + 1), false) with + | Not_found | Invalid_argument _ -> (min len (max_i - !i), true) + in + String.blit src !i buf 0 len; + Format.pp_print_string ppf (Bytes.sub_string buf 0 len); + if nl then Format.pp_print_newline ppf (); + Format.pp_print_flush ppf (); + i := !i + len; len let init_loc lb filename = @@ -178,12 +168,13 @@ let init_loc lb filename = (** *) -let execute ?ppf_code ?(print_outcome = true) ~ppf_answer code = +let execute ?ppf_code ?(print_outcome = true) ~ppf_answer code = let code = normalize code in let lb = match ppf_code with | Some ppf_code -> Lexing.from_function (refill_lexbuf code ppf_code) - | None -> Lexing.from_string code in + | None -> Lexing.from_string code + in init_loc lb "//toplevel//"; warnings := []; let rec loop () = @@ -191,29 +182,29 @@ let execute ?ppf_code ?(print_outcome = true) ~ppf_answer code = let phr = Ppx.preprocess_phrase phr in let success = Toploop.execute_phrase print_outcome ppf_answer phr in Format.pp_print_flush ppf_answer (); - if success then loop () else return_success false in - try let res = loop () in flush_all () ; res + if success then loop () else return_success false + in + try + let res = loop () in + flush_all (); res with - | End_of_file -> - flush_all (); - return_success true + | End_of_file -> flush_all (); return_success true | exn -> flush_all (); return_error (error_of_exn exn) -let use_string - ?(filename = "//toplevel//") ?(print_outcome = true) ~ppf_answer code = +let use_string ?(filename = "//toplevel//") ?(print_outcome = true) ~ppf_answer + code = let lb = Lexing.from_string code in init_loc lb filename; warnings := []; try List.iter (fun phr -> - if not (Toploop.execute_phrase print_outcome ppf_answer phr) then - raise Exit - else - Format.pp_print_flush ppf_answer ()) - (List.map Ppx.preprocess_phrase (!Toploop.parse_use_file lb)) ; + if not (Toploop.execute_phrase print_outcome ppf_answer phr) then + raise Exit + else Format.pp_print_flush ppf_answer () ) + (List.map Ppx.preprocess_phrase (!Toploop.parse_use_file lb)); flush_all (); return_success true with @@ -231,33 +222,34 @@ let parse_mod_string ?filename modname sig_code impl_code = let str = let impl_lb = Lexing.from_string impl_code in init_loc impl_lb - (match filename with - | None -> String.uncapitalize_ascii modname ^ ".ml" - | Some f -> f); - Parse.implementation impl_lb in + ( match filename with + | None -> String.uncapitalize_ascii modname ^ ".ml" + | Some f -> f ); + Parse.implementation impl_lb + in let m = match sig_code with - | None -> (Mod.structure str) + | None -> Mod.structure str | Some sig_code -> let sig_lb = Lexing.from_string sig_code in init_loc sig_lb (String.uncapitalize_ascii modname ^ ".mli"); let s = Parse.interface sig_lb in - Mod.constraint_ (Mod.structure str) (Mty.signature s) in - Ptop_def [ Str.module_ (Mb.mk (Location.mknoloc modname) m) ] + Mod.constraint_ (Mod.structure str) (Mty.signature s) + in + Ptop_def [Str.module_ (Mb.mk (Location.mknoloc modname) m)] -let use_mod_string - ?filename - ?(print_outcome = true) ~ppf_answer ~modname ?sig_code - impl_code = +let use_mod_string ?filename ?(print_outcome = true) ~ppf_answer ~modname + ?sig_code impl_code = if String.capitalize_ascii modname <> modname then invalid_arg - "Learnocaml_toplevel_toploop.use_mod_string: \ - the module name must start with a capital letter."; + "Learnocaml_toplevel_toploop.use_mod_string: the module name must start \ + with a capital letter."; warnings := []; try let phr = - Ppx.preprocess_phrase @@ - parse_mod_string ?filename modname sig_code impl_code in + Ppx.preprocess_phrase + @@ parse_mod_string ?filename modname sig_code impl_code + in let res = Toploop.execute_phrase print_outcome ppf_answer phr in Format.pp_print_flush ppf_answer (); flush_all (); @@ -270,13 +262,12 @@ let use_mod_string let check_phrase env = function | Parsetree.Ptop_def sstr -> Typecore.reset_delayed_checks (); - let (str, sg, newenv) = Typemod.type_toplevel_phrase env sstr in + let str, sg, newenv = Typemod.type_toplevel_phrase env sstr in let sg' = Typemod.simplify_signature sg in ignore (Includemod.signatures env sg sg'); Typecore.force_delayed_checks (); let _lam = Translmod.transl_toplevel_definition str in - Warnings.check_fatal (); - newenv + Warnings.check_fatal (); newenv | Parsetree.Ptop_dir _ -> env let check ?(setenv = false) code = @@ -285,15 +276,11 @@ let check ?(setenv = false) code = warnings := []; try let env = - List.fold_left - check_phrase - !Toploop.toplevel_env - (List.map - Ppx.preprocess_phrase - (!Toploop.parse_use_file lb)) in + List.fold_left check_phrase !Toploop.toplevel_env + (List.map Ppx.preprocess_phrase (!Toploop.parse_use_file lb)) + in if setenv then Toploop.toplevel_env := env; return_success () with | End_of_file -> return_success () | exn -> return_exn exn - diff --git a/src/toploop/toploop_ext.mli b/src/toploop/toploop_ext.mli index c6903d57b..22f176443 100644 --- a/src/toploop/toploop_ext.mli +++ b/src/toploop/toploop_ext.mli @@ -12,17 +12,13 @@ type 'a toplevel_result = 'a Toploop_results.toplevel_result = | Error of error * warning list and error = Toploop_results.error = - { msg: string; - locs: loc list; - if_highlight: string; } + {msg : string; locs : loc list; if_highlight : string} and warning = error -and loc = Toploop_results.loc = { - loc_start: int * int; - loc_end: int * int; -} +and loc = Toploop_results.loc = {loc_start : int * int; loc_end : int * int} +val check : ?setenv:bool -> string -> unit toplevel_result (** Parse and typecheck a given source code. @param setenv should the resulting environment replace the current @@ -32,9 +28,13 @@ and loc = Toploop_results.loc = { where [err] contains the error message otherwise. *) -val check: ?setenv:bool -> string -> unit toplevel_result - +val execute : + ?ppf_code:Format.formatter + -> ?print_outcome:bool + -> ppf_answer:Format.formatter + -> string + -> bool toplevel_result (** Execute a given source code. The evaluation stops after the first toplevel phrase (as terminated by ";;") that fails to parse/typecheck/compile or for which the evaluation raises an @@ -56,13 +56,13 @@ val check: ?setenv:bool -> string -> unit toplevel_result exception, and [Ok false] otherwise. In the last case, the exception has been pretty-printed in [ppf_answer]. *) -val execute: - ?ppf_code:Format.formatter -> - ?print_outcome:bool -> - ppf_answer:Format.formatter -> - string -> bool toplevel_result - +val use_string : + ?filename:string + -> ?print_outcome:bool + -> ppf_answer:Format.formatter + -> string + -> bool toplevel_result (** Execute a given source code. The code is parsed all at once before to typecheck/compile/evaluate phrase by phrase. @@ -75,12 +75,15 @@ val execute: @return as {!val:execute}. *) -val use_string: - ?filename:string -> - ?print_outcome:bool -> - ppf_answer:Format.formatter -> - string -> bool toplevel_result +val use_mod_string : + ?filename:string + -> ?print_outcome:bool + -> ppf_answer:Format.formatter + -> modname:string + -> ?sig_code:string + -> string + -> bool toplevel_result (** Wrap a given source code into a module and bind it with a given name. @@ -98,17 +101,13 @@ val use_string: @return as {!val:execute}. *) -val use_mod_string: - ?filename:string -> - ?print_outcome:bool -> - ppf_answer:Format.formatter -> - modname:string -> - ?sig_code:string -> - string -> bool toplevel_result (** Helpers to embed PPX into the toplevel. *) module Ppx : sig - val preprocess_structure: Parsetree.structure -> Parsetree.structure - val preprocess_signature: Parsetree.signature -> Parsetree.signature - val preprocess_phrase: Parsetree.toplevel_phrase -> Parsetree.toplevel_phrase + val preprocess_structure : Parsetree.structure -> Parsetree.structure + + val preprocess_signature : Parsetree.signature -> Parsetree.signature + + val preprocess_phrase : + Parsetree.toplevel_phrase -> Parsetree.toplevel_phrase end diff --git a/src/toploop/toploop_jsoo.ml b/src/toploop/toploop_jsoo.ml index 497bf39a1..ff758a9f1 100644 --- a/src/toploop/toploop_jsoo.ml +++ b/src/toploop/toploop_jsoo.ml @@ -15,108 +15,112 @@ let split_primitives p = if cur >= len then [] else if p.[cur] = '\000' then String.sub p beg (cur - beg) :: split (cur + 1) (cur + 1) - else - split beg (cur + 1) in - Array.of_list(split 0 0) - -let setup = lazy ( - Hashtbl.add Toploop.directive_table "enable" - (Toploop.Directive_string Option.Optim.enable); - Hashtbl.add Toploop.directive_table "disable" - (Toploop.Directive_string Option.Optim.disable); - Hashtbl.add Toploop.directive_table "debug_on" - (Toploop.Directive_string Option.Debug.enable); - Hashtbl.add Toploop.directive_table "debug_off" - (Toploop.Directive_string Option.Debug.disable); - Hashtbl.add Toploop.directive_table "tailcall" - (Toploop.Directive_string (Option.Param.set "tc")); - (* Workaround Marshal bug triggered by includemod.ml:607 *) - Clflags.error_size := 0 ; - (* Disable inlining of JSOO which may blow the JS stack *) - Option.Optim.disable "inline" ; - Topdirs.dir_directory "/cmis"; - let initial_primitive_count = - Array.length (split_primitives (Symtable.data_primitive_names ())) in - - let compile s = - let prims = - split_primitives (Symtable.data_primitive_names ()) in - let unbound_primitive p = - try ignore (Js.Unsafe.eval_string p); false with _ -> true in - let stubs = ref [] in - Array.iteri - (fun i p -> - if i >= initial_primitive_count && unbound_primitive p then - stubs := - Format.sprintf - "function %s(){caml_failwith(\"%s not implemented\")}" p p - :: !stubs) - prims; - let output_program = Driver.from_string prims s in - let b = Buffer.create 100 in - output_program (Pretty_print.to_buffer b); - Format.(pp_print_flush std_formatter ()); - Format.(pp_print_flush err_formatter ()); - flush stdout; flush stderr; - let res = Buffer.contents b in - let res = String.concat "" !stubs ^ res in - Js.Unsafe.global##(toplevelEval res) + else split beg (cur + 1) in - Js.Unsafe.global##.toplevelCompile := compile (*XXX HACK!*); - Js.Unsafe.global##.toplevelEval := (fun x -> + Array.of_list (split 0 0) + +let setup = + lazy + ( Hashtbl.add Toploop.directive_table "enable" + (Toploop.Directive_string Option.Optim.enable); + Hashtbl.add Toploop.directive_table "disable" + (Toploop.Directive_string Option.Optim.disable); + Hashtbl.add Toploop.directive_table "debug_on" + (Toploop.Directive_string Option.Debug.enable); + Hashtbl.add Toploop.directive_table "debug_off" + (Toploop.Directive_string Option.Debug.disable); + Hashtbl.add Toploop.directive_table "tailcall" + (Toploop.Directive_string (Option.Param.set "tc")); + (* Workaround Marshal bug triggered by includemod.ml:607 *) + Clflags.error_size := 0; + (* Disable inlining of JSOO which may blow the JS stack *) + Option.Optim.disable "inline"; + Topdirs.dir_directory "/cmis"; + let initial_primitive_count = + Array.length (split_primitives (Symtable.data_primitive_names ())) + in + let compile s = + let prims = split_primitives (Symtable.data_primitive_names ()) in + let unbound_primitive p = + try + ignore (Js.Unsafe.eval_string p); + false + with _ -> true + in + let stubs = ref [] in + Array.iteri + (fun i p -> + if i >= initial_primitive_count && unbound_primitive p then + stubs := + Format.sprintf + "function %s(){caml_failwith(\"%s not implemented\")}" p p + :: !stubs ) + prims; + let output_program = Driver.from_string prims s in + let b = Buffer.create 100 in + output_program (Pretty_print.to_buffer b); + Format.(pp_print_flush std_formatter ()); + Format.(pp_print_flush err_formatter ()); + flush stdout; + flush stderr; + let res = Buffer.contents b in + let res = String.concat "" !stubs ^ res in + Js.Unsafe.global ## (toplevelEval res) + in + Js.Unsafe.global##.toplevelCompile := compile (*XXX HACK!*); + Js.Unsafe.global##.toplevelEval + := fun x -> let f : < .. > Js.t -> < .. > Js.t = Js.Unsafe.eval_string x in - (fun () -> - let res = f Js.Unsafe.global in - Format.(pp_print_flush std_formatter ()); - Format.(pp_print_flush err_formatter ()); - flush stdout; flush stderr; - res))) + fun () -> + let res = f Js.Unsafe.global in + Format.(pp_print_flush std_formatter ()); + Format.(pp_print_flush err_formatter ()); + flush stdout; flush stderr; res ) let initialize () = - Lazy.force setup ; + Lazy.force setup; Toploop.initialize_toplevel_env () type redirection = - { channel : out_channel ; - name : string ; - tee : string -> string -> unit ; - callback : string -> unit ; - prev : redirection option } + { channel : out_channel + ; name : string + ; tee : string -> string -> unit + ; callback : string -> unit + ; prev : redirection option } let redirections : (out_channel * redirection ref) list ref = ref [] -let redirect_channel ?(tee = (fun _ _ -> ())) name channel callback = +let redirect_channel ?(tee = fun _ _ -> ()) name channel callback = try - flush channel ; + flush channel; let cur = List.assq channel !redirections in - cur := { channel ; name ; tee ; callback ; prev = Some !cur } ; + cur := {channel; name; tee; callback; prev = Some !cur}; !cur with Not_found -> - let cur = ref { channel ; name ; tee ; callback ; prev = None } in - redirections := (channel, cur) :: !redirections ; + let cur = ref {channel; name; tee; callback; prev = None} in + redirections := (channel, cur) :: !redirections; let append text = - let { tee ; name ; callback ; _ } = !cur in - tee name text ; - callback text in - Sys_js.set_channel_flusher channel append ; + let {tee; name; callback; _} = !cur in + tee name text; callback text + in + Sys_js.set_channel_flusher channel append; !cur -let flush_redirected_channel redir = - flush redir.channel (* [Sys_js] should do the rest *) +let flush_redirected_channel redir = flush redir.channel + +(* [Sys_js] should do the rest *) let stop_channel_redirection redir = let fail () = invalid_arg "Toploop_jsoo.stop_channel_redirection" in try let cur = List.assq redir.channel !redirections in - if !cur != redir then fail () ; - flush_redirected_channel redir ; + if !cur != redir then fail (); + flush_redirected_channel redir; match redir.prev with | Some prev -> cur := prev | None -> redirections := - List.filter (fun (ch, _) -> ch != redir.channel) !redirections ; - let append text = - Firebug.console##(log (Js.string text)) in - Sys_js.set_channel_flusher redir.channel append ; - with Not_found -> - fail () + List.filter (fun (ch, _) -> ch != redir.channel) !redirections; + let append text = Firebug.console ## (log (Js.string text)) in + Sys_js.set_channel_flusher redir.channel append + with Not_found -> fail () diff --git a/src/toploop/toploop_jsoo.mli b/src/toploop/toploop_jsoo.mli index 88fb2a428..324b8bbd6 100644 --- a/src/toploop/toploop_jsoo.mli +++ b/src/toploop/toploop_jsoo.mli @@ -6,12 +6,18 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) +val initialize : unit -> unit (** To be called before using any [Toploop] function. *) -val initialize: unit -> unit (** Materializes an output channel redirection. *) type redirection +val redirect_channel : + ?tee:(string -> string -> unit) + -> string + -> out_channel + -> (string -> unit) + -> redirection (** Redirects a channel. Instead of being output to the file underlying file descriptor, any data output to this channel will be passed to the callback @@ -19,18 +25,14 @@ type redirection The string parameter is an identifier, that is passed back to the secondary callback [tee]. This is useful to use the same [tee] callback for several channels. *) -val redirect_channel: - ?tee:(string -> string -> unit) -> - string -> out_channel -> (string -> unit) -> - redirection -(** Flushes the channel, calling the callbacks if necessary. *) val flush_redirected_channel : redirection -> unit +(** Flushes the channel, calling the callbacks if necessary. *) +val stop_channel_redirection : redirection -> unit (** Flushes the channel and then cancel the redirection. The redirection must be the last one performed, otherwise an [Invalid_argument] will be raised. A stack of redirections is maintained for all fire descriptors. So the channel is then restored to either the previous redirection or to the original file descriptor. *) -val stop_channel_redirection : redirection -> unit diff --git a/src/toploop/toploop_results.ml b/src/toploop/toploop_results.ml index 0fe6b7328..04ad7bd84 100644 --- a/src/toploop/toploop_results.ml +++ b/src/toploop/toploop_results.ml @@ -11,14 +11,8 @@ type 'a toplevel_result = | Ok of 'a * warning list | Error of error * warning list -and error = - { msg: string; - locs: loc list; - if_highlight: string; } +and error = {msg : string; locs : loc list; if_highlight : string} and warning = error -and loc = { - loc_start: int * int; - loc_end: int * int; -} +and loc = {loc_start : int * int; loc_end : int * int} diff --git a/src/toploop/toploop_results.mli b/src/toploop/toploop_results.mli index 0fe6b7328..04ad7bd84 100644 --- a/src/toploop/toploop_results.mli +++ b/src/toploop/toploop_results.mli @@ -11,14 +11,8 @@ type 'a toplevel_result = | Ok of 'a * warning list | Error of error * warning list -and error = - { msg: string; - locs: loc list; - if_highlight: string; } +and error = {msg : string; locs : loc list; if_highlight : string} and warning = error -and loc = { - loc_start: int * int; - loc_end: int * int; -} +and loc = {loc_start : int * int; loc_end : int * int} diff --git a/src/toploop/toploop_unix.ml b/src/toploop/toploop_unix.ml index c5168b2e7..2a1d9a93d 100644 --- a/src/toploop/toploop_unix.ml +++ b/src/toploop/toploop_unix.ml @@ -6,67 +6,62 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -let map_opt f = function - | None -> None - | Some x -> Some (f x) -let iter_opt f = function - | None -> () - | Some x -> f x +let map_opt f = function None -> None | Some x -> Some (f x) + +let iter_opt f = function None -> () | Some x -> f x type redirection = - { channel : out_channel ; - target_fd : Unix.file_descr ; - backup_fd : Unix.file_descr ; - read_fd : Unix.file_descr ; - append : string -> unit } + { channel : out_channel + ; target_fd : Unix.file_descr + ; backup_fd : Unix.file_descr + ; read_fd : Unix.file_descr + ; append : string -> unit } let redirections = ref [] let redirect_channel ?tee name channel append = - flush channel ; + flush channel; let append data = let tee = map_opt (fun tee -> tee name) tee in - iter_opt (fun tee -> tee data) tee ; - append data in + iter_opt (fun tee -> tee data) tee; + append data + in let target_fd = Unix.descr_of_out_channel channel in let backup_fd = Unix.dup target_fd in let stack = try List.assq target_fd !redirections with Not_found -> [] in let read_fd, write_fd = Unix.pipe () in - Unix.dup2 write_fd target_fd ; - Unix.close write_fd ; - Unix.set_nonblock read_fd ; - let redirected_channel = - { target_fd ; backup_fd ; read_fd ; append ; channel } in - redirections := - List.filter (fun (fd, _) -> fd != target_fd) !redirections ; - redirections := - (target_fd, redirected_channel :: stack) :: !redirections ; + Unix.dup2 write_fd target_fd; + Unix.close write_fd; + Unix.set_nonblock read_fd; + let redirected_channel = {target_fd; backup_fd; read_fd; append; channel} in + redirections := List.filter (fun (fd, _) -> fd != target_fd) !redirections; + redirections := (target_fd, redirected_channel :: stack) :: !redirections; redirected_channel -let flush_redirected_channel { read_fd ; append ; channel ; _ } = +let flush_redirected_channel {read_fd; append; channel; _} = let buf = Bytes.create 503 in let rec loop () = let len = Unix.read read_fd buf 0 (Bytes.length buf) in let data = Bytes.sub_string buf 0 len in - append data ; - loop () in - flush channel ; try loop () with _ -> () + append data; loop () + in + flush channel; + try loop () with _ -> () -let stop_channel_redirection ({ target_fd ; read_fd ; backup_fd ; _ } as redirection) = +let stop_channel_redirection ({target_fd; read_fd; backup_fd; _} as redirection) + = let fail () = invalid_arg "Toploop_unix.stop_channel_redirection" in match List.assq target_fd !redirections with | exception Not_found -> fail () | [] -> fail () | redirection' :: rest -> - if redirection' != redirection then fail () ; - flush_redirected_channel redirection ; - Unix.dup2 backup_fd target_fd ; - Unix.close backup_fd ; - Unix.close read_fd ; + if redirection' != redirection then fail (); + flush_redirected_channel redirection; + Unix.dup2 backup_fd target_fd; + Unix.close backup_fd; + Unix.close read_fd; redirections := - List.filter (fun (fd, _) -> fd != target_fd) !redirections ; - if rest <> [] then - redirections := (target_fd, rest) :: !redirections + List.filter (fun (fd, _) -> fd != target_fd) !redirections; + if rest <> [] then redirections := (target_fd, rest) :: !redirections -let initialize () = - Toploop.initialize_toplevel_env () +let initialize () = Toploop.initialize_toplevel_env () diff --git a/src/toploop/toploop_unix.mli b/src/toploop/toploop_unix.mli index 88fb2a428..324b8bbd6 100644 --- a/src/toploop/toploop_unix.mli +++ b/src/toploop/toploop_unix.mli @@ -6,12 +6,18 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) +val initialize : unit -> unit (** To be called before using any [Toploop] function. *) -val initialize: unit -> unit (** Materializes an output channel redirection. *) type redirection +val redirect_channel : + ?tee:(string -> string -> unit) + -> string + -> out_channel + -> (string -> unit) + -> redirection (** Redirects a channel. Instead of being output to the file underlying file descriptor, any data output to this channel will be passed to the callback @@ -19,18 +25,14 @@ type redirection The string parameter is an identifier, that is passed back to the secondary callback [tee]. This is useful to use the same [tee] callback for several channels. *) -val redirect_channel: - ?tee:(string -> string -> unit) -> - string -> out_channel -> (string -> unit) -> - redirection -(** Flushes the channel, calling the callbacks if necessary. *) val flush_redirected_channel : redirection -> unit +(** Flushes the channel, calling the callbacks if necessary. *) +val stop_channel_redirection : redirection -> unit (** Flushes the channel and then cancel the redirection. The redirection must be the last one performed, otherwise an [Invalid_argument] will be raised. A stack of redirections is maintained for all fire descriptors. So the channel is then restored to either the previous redirection or to the original file descriptor. *) -val stop_channel_redirection : redirection -> unit diff --git a/src/utils/js_utils.ml b/src/utils/js_utils.ml index ea6d9be6e..c6f2c7af6 100644 --- a/src/utils/js_utils.ml +++ b/src/utils/js_utils.ml @@ -19,60 +19,64 @@ open Js_of_ocaml let doc = Dom_html.document + let window = Dom_html.window + (* let loc = Js.Unsafe.variable "location" *) -let alert s = window##(alert (Js.string s)) -let confirm s = Js.to_bool (window##(confirm (Js.string s))) +let alert s = window ## (alert (Js.string s)) + +let confirm s = Js.to_bool window ## (confirm (Js.string s)) + +let js_log obj = Firebug.console ## (log obj) + +let js_debug obj = Firebug.console ## (debug obj) -let js_log obj = Firebug.console##(log obj) -let js_debug obj = Firebug.console##(debug obj) -let js_warn obj = Firebug.console##(warn obj) -let js_error obj = Firebug.console##(error obj) +let js_warn obj = Firebug.console ## (warn obj) + +let js_error obj = Firebug.console ## (error obj) let log fmt = Format.kfprintf - (fun _ -> Firebug.console##(log (Js.string (Format.flush_str_formatter ())))) - Format.str_formatter - fmt + (fun _ -> + Firebug.console ## (log (Js.string (Format.flush_str_formatter ()))) ) + Format.str_formatter fmt + let debug fmt = Format.kfprintf - (fun _ -> Firebug.console##(debug (Js.string (Format.flush_str_formatter ())))) - Format.str_formatter - fmt + (fun _ -> + Firebug.console ## (debug (Js.string (Format.flush_str_formatter ()))) ) + Format.str_formatter fmt + let warn fmt = Format.kfprintf - (fun _ -> Firebug.console##(warn (Js.string (Format.flush_str_formatter ())))) - Format.str_formatter - fmt + (fun _ -> + Firebug.console ## (warn (Js.string (Format.flush_str_formatter ()))) ) + Format.str_formatter fmt + let error fmt = Format.kfprintf - (fun _ -> Firebug.console##(error (Js.string (Format.flush_str_formatter ())))) - Format.str_formatter - fmt + (fun _ -> + Firebug.console ## (error (Js.string (Format.flush_str_formatter ()))) ) + Format.str_formatter fmt let reload () = window##.location##reload let get_lang () = - match Js.Optdef. to_option (Dom_html.window##.navigator##.language) with + match Js.Optdef.to_option Dom_html.window##.navigator##.language with | Some l -> Some (Js.to_string l) - | None -> - match Js.Optdef.to_option (Dom_html.window##.navigator##.userLanguage) - with - | Some l -> Some (Js.to_string l) - | None -> None - + | None -> ( + match Js.Optdef.to_option Dom_html.window##.navigator##.userLanguage with + | Some l -> Some (Js.to_string l) + | None -> None ) module Manip = struct - let option_map f = function None -> None | Some x -> Some (f x) exception Error of string let manip_error fmt = - Format.ksprintf - (fun s -> debug "%s" s; raise (Error s)) - fmt + Format.ksprintf (fun s -> debug "%s" s; raise (Error s)) fmt open Tyxml_js @@ -84,21 +88,19 @@ module Manip = struct Js.Opt.case (Dom_html.CoerceTo.element (Html5.toelt elt)) (fun () -> - manip_error - "Cannot call %s on a node which is not an element" - name) + manip_error "Cannot call %s on a node which is not an element" name ) id + let html_doc_constr : Dom_html.document Js.constr = Js.Unsafe.global##._HTMLDocument let document elt = let elt = get_elt "document" elt in - let rec loop (elt : Dom.node Js.t) = - if Js.instanceof elt html_doc_constr - then (Obj.magic elt : Dom_html.document Js.t) + let rec loop (elt : Dom.node Js.t) = + if Js.instanceof elt html_doc_constr then + (Obj.magic elt : Dom_html.document Js.t) else - Js.Opt.case - (elt##.parentNode) + Js.Opt.case elt##.parentNode (fun () -> (Obj.magic elt : Dom_html.document Js.t)) loop in @@ -108,9 +110,9 @@ module Manip = struct let doc = document elt in (Obj.magic doc)##.defaultView - let clone ?(deep=false) elt = + let clone ?(deep = false) elt = let elt = get_elt "clone" elt in - Obj.magic (elt##(cloneNode (Js.bool deep))) + Obj.magic elt ## (cloneNode (Js.bool deep)) let setInnerHtml elt s = let elt = get_elt "setInnerHtml" elt in @@ -122,92 +124,99 @@ module Manip = struct let hasClass elt s = let elt = get_elt "addClass" elt in - Js.to_bool - elt##.classList##(contains (Js.string s)) + Js.to_bool elt ##. classList ## (contains (Js.string s)) + let addClass elt s = let elt = get_elt "addClass" elt in - elt##.classList##(add (Js.string s)) + elt ##. classList ## (add (Js.string s)) + let removeClass elt s = let elt = get_elt "removeClass" elt in - elt##.classList##(remove (Js.string s)) + elt ##. classList ## (remove (Js.string s)) + let toggleClass elt s = let elt = get_elt "toggleClass" elt in - Js.to_bool - elt##.classList##(toggle (Js.string s)) - + Js.to_bool elt ##. classList ## (toggle (Js.string s)) let raw_appendChild ?before node elt2 = match before with - | None -> ignore(node##(appendChild (get_node elt2))) + | None -> ignore node ## (appendChild (get_node elt2)) | Some elt3 -> - let node3 = get_node elt3 in - ignore(node##(insertBefore (get_node elt2) (Js.some node3))) + let node3 = get_node elt3 in + ignore node ## (insertBefore (get_node elt2) (Js.some node3)) let raw_appendChildren ?before node elts = match before with | None -> - List.iter (fun elt2 -> ignore(node##(appendChild (get_node elt2)))) elts + List.iter + (fun elt2 -> ignore node ## (appendChild (get_node elt2))) + elts | Some elt3 -> - let node3 = get_node elt3 in - List.iter (fun elt2 -> ignore(node##(insertBefore (get_node elt2) (Js.some node3)))) elts + let node3 = get_node elt3 in + List.iter + (fun elt2 -> + ignore node ## (insertBefore (get_node elt2) (Js.some node3)) ) + elts let raw_insertChildAfter node1 node2 elt3 = - Js.Opt.case - (node2##.nextSibling) - (fun () -> - ignore(node1##(appendChild (get_node elt3)))) + Js.Opt.case node2##.nextSibling + (fun () -> ignore node1 ## (appendChild (get_node elt3))) (fun node2 -> - ignore(node1##(insertBefore (get_node elt3) (Js.some node2)))) + ignore node1 ## (insertBefore (get_node elt3) (Js.some node2)) ) let raw_insertChildrenAfter node1 node2 elts = - Js.Opt.case - (node2##.nextSibling) + Js.Opt.case node2##.nextSibling (fun () -> - List.iter (fun elt3 -> - ignore(node1##(appendChild (get_node elt3))))) + List.iter (fun elt3 -> ignore node1 ## (appendChild (get_node elt3))) + ) (fun node2 -> - List.iter (fun elt3 -> - ignore(node1##(insertBefore (get_node elt3) (Js.some node2))))) + List.iter (fun elt3 -> + ignore node1 ## (insertBefore (get_node elt3) (Js.some node2)) ) ) elts let raw_removeChild node1 elt2 = let node2 = get_node elt2 in - ignore(node1##(removeChild node2)) + ignore node1 ## (removeChild node2) let raw_replaceChild node1 elt2 elt3 = let node2 = get_node elt2 in - ignore(node1##replaceChild node2 (get_node elt3)) + ignore (node1##replaceChild node2 (get_node elt3)) let raw_removeChildren node = - let childrens = Dom.list_of_nodeList (node##.childNodes) in - List.iter (fun c -> ignore(node##(removeChild c))) childrens + let childrens = Dom.list_of_nodeList node##.childNodes in + List.iter (fun c -> ignore node ## (removeChild c)) childrens let raw_replaceChildren node elts = raw_removeChildren node; - List.iter (fun elt -> ignore(node##(appendChild (get_node elt)))) elts + List.iter (fun elt -> ignore node ## (appendChild (get_node elt))) elts let nth elt n = let node = get_node elt in - let res = Js.Opt.bind (node##.childNodes##(item n)) (fun node -> - Js.Opt.map (Dom.CoerceTo.element node) (fun node -> - Of_dom.of_element (Dom_html.element node) - ) - ) in + let res = + Js.Opt.bind + node ##. childNodes ## (item n) + (fun node -> + Js.Opt.map (Dom.CoerceTo.element node) (fun node -> + Of_dom.of_element (Dom_html.element node) ) ) + in Js.Opt.to_option res let by_id n = - let res = Js.Opt.bind (Dom_html.window##.document##(getElementById (Js.string n))) (fun node -> - Js.Opt.map (Dom.CoerceTo.element node) (fun node -> - Of_dom.of_element (Dom_html.element node) - ) - ) in + let res = + Js.Opt.bind + Dom_html.window ##. document ## (getElementById (Js.string n)) + (fun node -> + Js.Opt.map (Dom.CoerceTo.element node) (fun node -> + Of_dom.of_element (Dom_html.element node) ) ) + in Js.Opt.to_option res let by_classname n = Dom.list_of_nodeList - (Dom_html.window##.document##(getElementsByClassName (Js.string n))) + Dom_html.window ##. document ## (getElementsByClassName (Js.string n)) |> List.map (fun n -> Of_dom.of_element (Dom_html.element n)) - (* let rec tolist acc n = + + (* let rec tolist acc n = * if n < 0 then acc * else * let acc = @@ -247,12 +256,12 @@ module Manip = struct let removeSelf elt = let node = get_node elt in - let res = Js.Opt.bind (node##.parentNode) (fun node -> - Js.Opt.map (Dom.CoerceTo.element node) (fun node -> - Of_dom.of_element (Dom_html.element node) - ) - ) in - Js.Opt.iter res (fun p -> removeChild p elt) + let res = + Js.Opt.bind node##.parentNode (fun node -> + Js.Opt.map (Dom.CoerceTo.element node) (fun node -> + Of_dom.of_element (Dom_html.element node) ) ) + in + Js.Opt.iter res (fun p -> removeChild p elt) let appendChildFirst p c = let before = nth p 0 in @@ -263,8 +272,8 @@ module Manip = struct raw_replaceChild node1 elt2 elt3 let replaceSelf elt1 elt2 = - Js.Opt.iter (get_node elt1)##.parentNode @@ fun parent -> - raw_replaceChild parent elt2 elt1 + Js.Opt.iter (get_node elt1)##.parentNode + @@ fun parent -> raw_replaceChild parent elt2 elt1 let removeChildren elt = let node = get_node elt in @@ -276,14 +285,14 @@ module Manip = struct let children elt = let node = get_node elt in - List.map Html5.tot (Dom.list_of_nodeList (node##.childNodes)) + List.map Html5.tot (Dom.list_of_nodeList node##.childNodes) let appendToBody ?before elt2 = - let body = (Of_dom.of_body Dom_html.window##.document##.body) in + let body = Of_dom.of_body Dom_html.window##.document##.body in appendChild ?before body elt2 let appendToHead ?before elt2 = - let head = (Of_dom.of_head Dom_html.window##.document##.head) in + let head = Of_dom.of_head Dom_html.window##.document##.head in appendChild ?before head elt2 let get_elt_input name elt : Dom_html.inputElement Js.t = @@ -312,51 +321,50 @@ module Manip = struct let scrollIntoView ?(bottom = false) elt = let elt = get_elt "Css.background" elt in - elt##(scrollIntoView (Js.bool (not bottom))) + elt ## (scrollIntoView (Js.bool (not bottom))) + + type disable = < disabled : bool Js.t Js.prop > - type disable = < disabled: bool Js.t Js.prop > let get_disable_elt name elt : disable Js.t = if Js.undefined == (Js.Unsafe.coerce @@ Html5.toelt elt)##.disabled then - manip_error - "Cannot call %s on a node without a 'disable' property" - name; + manip_error "Cannot call %s on a node without a 'disable' property" name; Js.Unsafe.coerce @@ Html5.toelt elt let disable elt = let elt = get_disable_elt "disable" elt in elt##.disabled := Js._true + let enable elt = let elt = get_disable_elt "enable" elt in elt##.disabled := Js._false - type focus = < focus: unit Js.meth > + type focus = < focus : unit Js.meth > + let get_focus_elt name elt : focus Js.t = if Js.undefined == (Js.Unsafe.coerce @@ Html5.toelt elt)##.focus then - manip_error - "Cannot call %s on a node without a 'focus' property" - name; + manip_error "Cannot call %s on a node without a 'focus' property" name; Js.Unsafe.coerce @@ Html5.toelt elt + let focus elt = let elt = get_focus_elt "focus" elt in elt##focus - type blur = < blur: unit Js.meth > + type blur = < blur : unit Js.meth > + let get_blur_elt name elt : blur Js.t = if Js.undefined == (Js.Unsafe.coerce @@ Html5.toelt elt)##.blur then - manip_error - "Cannot call %s on a node without a 'blur' property" - name; + manip_error "Cannot call %s on a node without a 'blur' property" name; Js.Unsafe.coerce @@ Html5.toelt elt + let blur elt = let elt = get_blur_elt "blur" elt in elt##blur - type value = < value: Js.js_string Js.t Js.prop > + type value = < value : Js.js_string Js.t Js.prop > + let get_value_elt name elt : value Js.t = if Js.undefined == (Js.Unsafe.coerce @@ Html5.toelt elt)##.value then - manip_error - "Cannot call %s on a node without a 'value' property" - name; + manip_error "Cannot call %s on a node without a 'value' property" name; Js.Unsafe.coerce @@ Html5.toelt elt let value elt = @@ -365,97 +373,127 @@ module Manip = struct module Elt = struct let body = - try Of_dom.of_body (Dom_html.window##.document##.body) - with _ -> Obj.magic Js.undefined (* For workers... *) + try Of_dom.of_body Dom_html.window##.document##.body with _ -> + Obj.magic Js.undefined + + (* For workers... *) let active () = (Js.Unsafe.coerce Dom_html.window##.document)##.activeElement end module Ev = struct type ('a, 'b) ev = 'a Html5.elt -> ('b Js.t -> bool) -> unit - type ('a,'b) ev_unit = 'a Html5.elt -> ('b Js.t -> unit) -> unit + + type ('a, 'b) ev_unit = 'a Html5.elt -> ('b Js.t -> unit) -> unit + let bool_cb f = Dom_html.handler (fun e -> Js.bool (f e)) + let onkeyup elt f = let elt = get_elt "Ev.onkeyup" elt in - elt##.onkeyup := (bool_cb f) + elt##.onkeyup := bool_cb f + let onkeydown elt f = let elt = get_elt "Ev.onkeydown" elt in - elt##.onkeydown := (bool_cb f) + elt##.onkeydown := bool_cb f + let onmouseup elt f = let elt = get_elt "Ev.onmouseup" elt in - elt##.onmouseup := (bool_cb f) + elt##.onmouseup := bool_cb f + let onmousedown elt f = let elt = get_elt "Ev.onmousedown" elt in - elt##.onmousedown := (bool_cb f) + elt##.onmousedown := bool_cb f + let onmouseout elt f = let elt = get_elt "Ev.onmouseout" elt in - elt##.onmouseout := (bool_cb f) + elt##.onmouseout := bool_cb f + let onmouseover elt f = let elt = get_elt "Ev.onmouseover" elt in - elt##.onmouseover := (bool_cb f) + elt##.onmouseover := bool_cb f + let onclick elt f = let elt = get_elt "Ev.onclick" elt in - elt##.onclick := (bool_cb f) + elt##.onclick := bool_cb f + let ondblclick elt f = let elt = get_elt "Ev.ondblclick" elt in - elt##.ondblclick := (bool_cb f) + elt##.ondblclick := bool_cb f + let onload elt f = let elt = get_elt_img "Ev.onload" elt in - elt##.onload := (bool_cb f) + elt##.onload := bool_cb f + let onerror elt f = let elt = get_elt_img "Ev.onerror" elt in - elt##.onerror := (bool_cb f) + elt##.onerror := bool_cb f + let onabort elt f = let elt = get_elt_img "Ev.onabort" elt in - elt##.onabort := (bool_cb f) + elt##.onabort := bool_cb f + let onfocus elt f = let elt = get_elt_input "Ev.onfocus" elt in - elt##.onfocus := (bool_cb f) + elt##.onfocus := bool_cb f + let onblur elt f = let elt = get_elt_input "Ev.onblur" elt in - elt##.onblur := (bool_cb f) + elt##.onblur := bool_cb f + let onfocus_textarea elt f = let elt = get_elt_textarea "Ev.onfocus" elt in - elt##.onfocus := (bool_cb f) + elt##.onfocus := bool_cb f + let onblur_textarea elt f = let elt = get_elt_textarea "Ev.onblur" elt in - elt##.onblur := (bool_cb f) + elt##.onblur := bool_cb f + let onscroll elt f = let elt = get_elt "Ev.onscroll" elt in - elt##.onscroll := (bool_cb f) + elt##.onscroll := bool_cb f + let onreturn elt f = let f ev = - let key = ev##.keyCode in - if key = 13 then f ev; - true in + let key = ev##.keyCode in + if key = 13 then f ev; + true + in onkeydown elt f + let onchange elt f = let elt = get_elt_input "Ev.onchange" elt in - elt##.onchange := (bool_cb f) + elt##.onchange := bool_cb f + let onchange_select elt f = let elt = get_elt_select "Ev.onchange_select" elt in - elt##.onchange := (bool_cb f) + elt##.onchange := bool_cb f + let oninput elt f = let elt = get_elt_input "Ev.oninput" elt in - elt##.oninput := (bool_cb f) + elt##.oninput := bool_cb f end module Attr = struct let clientWidth elt = let elt = get_elt "Attr.clientWidth" elt in elt##.clientWidth + let clientHeight elt = let elt = get_elt "Attr.clientHeight" elt in elt##.clientHeight + let offsetWidth elt = let elt = get_elt "Attr.offsetWidth" elt in elt##.offsetWidth + let offsetHeight elt = let elt = get_elt "Attr.offsetHeight" elt in elt##.offsetHeight + let clientLeft elt = let elt = get_elt "Attr.clientLeft" elt in elt##.clientLeft + let clientTop elt = let elt = get_elt "Attr.clientTop" elt in elt##.clientTop @@ -464,667 +502,902 @@ module Manip = struct module Css = struct let background elt = let elt = get_elt "Css.background" elt in - Js.to_bytestring (elt##.style##.background) + Js.to_bytestring elt##.style##.background + let backgroundAttachment elt = let elt = get_elt "Css.backgroundAttachment" elt in - Js.to_bytestring (elt##.style##.backgroundAttachment) + Js.to_bytestring elt##.style##.backgroundAttachment + let backgroundColor elt = let elt = get_elt "Css.backgroundColor" elt in - Js.to_bytestring (elt##.style##.backgroundColor) + Js.to_bytestring elt##.style##.backgroundColor + let backgroundImage elt = let elt = get_elt "Css.backgroundImage" elt in - Js.to_bytestring (elt##.style##.backgroundImage) + Js.to_bytestring elt##.style##.backgroundImage + let backgroundPosition elt = let elt = get_elt "Css.backgroundPosition" elt in - Js.to_bytestring (elt##.style##.backgroundPosition) + Js.to_bytestring elt##.style##.backgroundPosition + let backgroundRepeat elt = let elt = get_elt "Css.backgroundRepeat" elt in - Js.to_bytestring (elt##.style##.backgroundRepeat) + Js.to_bytestring elt##.style##.backgroundRepeat + let border elt = let elt = get_elt "Css.border" elt in - Js.to_bytestring (elt##.style##.border) + Js.to_bytestring elt##.style##.border + let borderBottom elt = let elt = get_elt "Css.borderBottom" elt in - Js.to_bytestring (elt##.style##.borderBottom) + Js.to_bytestring elt##.style##.borderBottom + let borderBottomColor elt = let elt = get_elt "Css.borderBottomColor" elt in - Js.to_bytestring (elt##.style##.borderBottomColor) + Js.to_bytestring elt##.style##.borderBottomColor + let borderBottomStyle elt = let elt = get_elt "Css.borderBottomStyle" elt in - Js.to_bytestring (elt##.style##.borderBottomStyle) + Js.to_bytestring elt##.style##.borderBottomStyle + let borderBottomWidth elt = let elt = get_elt "Css.borderBottomWidth" elt in - Js.to_bytestring (elt##.style##.borderBottomWidth) + Js.to_bytestring elt##.style##.borderBottomWidth + let borderBottomWidthPx elt = let elt = get_elt "Css.borderBottomWidthPx" elt in - Js.parseInt (elt##.style##.borderBottomWidth) + Js.parseInt elt##.style##.borderBottomWidth + let borderCollapse elt = let elt = get_elt "Css.borderCollapse" elt in - Js.to_bytestring (elt##.style##.borderCollapse) + Js.to_bytestring elt##.style##.borderCollapse + let borderColor elt = let elt = get_elt "Css.borderColor" elt in - Js.to_bytestring (elt##.style##.borderColor) + Js.to_bytestring elt##.style##.borderColor + let borderLeft elt = let elt = get_elt "Css.borderLeft" elt in - Js.to_bytestring (elt##.style##.borderLeft) + Js.to_bytestring elt##.style##.borderLeft + let borderLeftColor elt = let elt = get_elt "Css.borderLeftColor" elt in - Js.to_bytestring (elt##.style##.borderLeftColor) + Js.to_bytestring elt##.style##.borderLeftColor + let borderLeftStyle elt = let elt = get_elt "Css.borderLeftStyle" elt in - Js.to_bytestring (elt##.style##.borderLeftStyle) + Js.to_bytestring elt##.style##.borderLeftStyle + let borderLeftWidth elt = let elt = get_elt "Css.borderLeftWidth" elt in - Js.to_bytestring (elt##.style##.borderLeftWidth) + Js.to_bytestring elt##.style##.borderLeftWidth + let borderLeftWidthPx elt = let elt = get_elt "Css.borderLeftWidthPx" elt in - Js.parseInt (elt##.style##.borderLeftWidth) + Js.parseInt elt##.style##.borderLeftWidth + let borderRight elt = let elt = get_elt "Css.borderRight" elt in - Js.to_bytestring (elt##.style##.borderRight) + Js.to_bytestring elt##.style##.borderRight + let borderRightColor elt = let elt = get_elt "Css.borderRightColor" elt in - Js.to_bytestring (elt##.style##.borderRightColor) + Js.to_bytestring elt##.style##.borderRightColor + let borderRightStyle elt = let elt = get_elt "Css.borderRightStyle" elt in - Js.to_bytestring (elt##.style##.borderRightStyle) + Js.to_bytestring elt##.style##.borderRightStyle + let borderRightWidth elt = let elt = get_elt "Css.borderRightWidth" elt in - Js.to_bytestring (elt##.style##.borderRightWidth) + Js.to_bytestring elt##.style##.borderRightWidth + let borderRightWidthPx elt = let elt = get_elt "Css.borderRightWidthPx" elt in - Js.parseInt (elt##.style##.borderRightWidth) + Js.parseInt elt##.style##.borderRightWidth + let borderSpacing elt = let elt = get_elt "Css.borderSpacing" elt in - Js.to_bytestring (elt##.style##.borderSpacing) + Js.to_bytestring elt##.style##.borderSpacing + let borderStyle elt = let elt = get_elt "Css.borderStyle" elt in - Js.to_bytestring (elt##.style##.borderStyle) + Js.to_bytestring elt##.style##.borderStyle + let borderTop elt = let elt = get_elt "Css.borderTop" elt in - Js.to_bytestring (elt##.style##.borderTop) + Js.to_bytestring elt##.style##.borderTop + let borderTopColor elt = let elt = get_elt "Css.borderTopColor" elt in - Js.to_bytestring (elt##.style##.borderTopColor) + Js.to_bytestring elt##.style##.borderTopColor + let borderTopStyle elt = let elt = get_elt "Css.borderTopStyle" elt in - Js.to_bytestring (elt##.style##.borderTopStyle) + Js.to_bytestring elt##.style##.borderTopStyle + let borderTopWidth elt = let elt = get_elt "Css.borderTopWidth" elt in - Js.to_bytestring (elt##.style##.borderTopWidth) + Js.to_bytestring elt##.style##.borderTopWidth + let borderTopWidthPx elt = let elt = get_elt "Css.borderTopWidthPx" elt in - Js.parseInt (elt##.style##.borderTopWidth) + Js.parseInt elt##.style##.borderTopWidth + let borderWidth elt = let elt = get_elt "Css.borderWidth" elt in - Js.to_bytestring (elt##.style##.borderWidth) + Js.to_bytestring elt##.style##.borderWidth + let borderWidthPx elt = let elt = get_elt "Css.borderWidthPx" elt in - Js.parseInt (elt##.style##.borderWidth) + Js.parseInt elt##.style##.borderWidth + let bottom elt = let elt = get_elt "Css.bottom" elt in - Js.to_bytestring (elt##.style##.bottom) + Js.to_bytestring elt##.style##.bottom + let captionSide elt = let elt = get_elt "Css.captionSide" elt in - Js.to_bytestring (elt##.style##.captionSide) + Js.to_bytestring elt##.style##.captionSide + let clear elt = let elt = get_elt "Css.clear" elt in - Js.to_bytestring (elt##.style##.clear) + Js.to_bytestring elt##.style##.clear + let clip elt = let elt = get_elt "Css.clip" elt in - Js.to_bytestring (elt##.style##.clip) + Js.to_bytestring elt##.style##.clip + let color elt = let elt = get_elt "Css.color" elt in - Js.to_bytestring (elt##.style##.color) + Js.to_bytestring elt##.style##.color + let content elt = let elt = get_elt "Css.content" elt in - Js.to_bytestring (elt##.style##.content) + Js.to_bytestring elt##.style##.content + let counterIncrement elt = let elt = get_elt "Css.counterIncrement" elt in - Js.to_bytestring (elt##.style##.counterIncrement) + Js.to_bytestring elt##.style##.counterIncrement + let counterReset elt = let elt = get_elt "Css.counterReset" elt in - Js.to_bytestring (elt##.style##.counterReset) + Js.to_bytestring elt##.style##.counterReset + let cssFloat elt = let elt = get_elt "Css.cssFloat" elt in - Js.to_bytestring (elt##.style##.cssFloat) + Js.to_bytestring elt##.style##.cssFloat + let cssText elt = let elt = get_elt "Css.cssText" elt in - Js.to_bytestring (elt##.style##.cssText) + Js.to_bytestring elt##.style##.cssText + let cursor elt = let elt = get_elt "Css.cursor" elt in - Js.to_bytestring (elt##.style##.cursor) + Js.to_bytestring elt##.style##.cursor + let direction elt = let elt = get_elt "Css.direction" elt in - Js.to_bytestring (elt##.style##.direction) + Js.to_bytestring elt##.style##.direction + let display elt = let elt = get_elt "Css.display" elt in - Js.to_bytestring (elt##.style##.display) + Js.to_bytestring elt##.style##.display + let emptyCells elt = let elt = get_elt "Css.emptyCells" elt in - Js.to_bytestring (elt##.style##.emptyCells) + Js.to_bytestring elt##.style##.emptyCells + let font elt = let elt = get_elt "Css.font" elt in - Js.to_bytestring (elt##.style##.font) + Js.to_bytestring elt##.style##.font + let fontFamily elt = let elt = get_elt "Css.fontFamily" elt in - Js.to_bytestring (elt##.style##.fontFamily) + Js.to_bytestring elt##.style##.fontFamily + let fontSize elt = let elt = get_elt "Css.fontSize" elt in - Js.to_bytestring (elt##.style##.fontSize) + Js.to_bytestring elt##.style##.fontSize + let fontStyle elt = let elt = get_elt "Css.fontStyle" elt in - Js.to_bytestring (elt##.style##.fontStyle) + Js.to_bytestring elt##.style##.fontStyle + let fontVariant elt = let elt = get_elt "Css.fontVariant" elt in - Js.to_bytestring (elt##.style##.fontVariant) + Js.to_bytestring elt##.style##.fontVariant + let fontWeight elt = let elt = get_elt "Css.fontWeight" elt in - Js.to_bytestring (elt##.style##.fontWeight) + Js.to_bytestring elt##.style##.fontWeight + let height elt = let elt = get_elt "Css.height" elt in - Js.to_bytestring (elt##.style##.height) + Js.to_bytestring elt##.style##.height + let heightPx elt = let elt = get_elt "Css.heightPx" elt in - Js.parseInt (elt##.style##.height) + Js.parseInt elt##.style##.height + let left elt = let elt = get_elt "Css.left" elt in - Js.to_bytestring (elt##.style##.left) + Js.to_bytestring elt##.style##.left + let leftPx elt = let elt = get_elt "Css.leftPx" elt in - Js.parseInt (elt##.style##.left) + Js.parseInt elt##.style##.left + let letterSpacing elt = let elt = get_elt "Css.letterSpacing" elt in - Js.to_bytestring (elt##.style##.letterSpacing) + Js.to_bytestring elt##.style##.letterSpacing + let lineHeight elt = let elt = get_elt "Css.lineHeight" elt in - Js.to_bytestring (elt##.style##.lineHeight) + Js.to_bytestring elt##.style##.lineHeight + let listStyle elt = let elt = get_elt "Css.listStyle" elt in - Js.to_bytestring (elt##.style##.listStyle) + Js.to_bytestring elt##.style##.listStyle + let listStyleImage elt = let elt = get_elt "Css.listStyleImage" elt in - Js.to_bytestring (elt##.style##.listStyleImage) + Js.to_bytestring elt##.style##.listStyleImage + let listStylePosition elt = let elt = get_elt "Css.listStylePosition" elt in - Js.to_bytestring (elt##.style##.listStylePosition) + Js.to_bytestring elt##.style##.listStylePosition + let listStyleType elt = let elt = get_elt "Css.listStyleType" elt in - Js.to_bytestring (elt##.style##.listStyleType) + Js.to_bytestring elt##.style##.listStyleType + let margin elt = let elt = get_elt "Css.margin" elt in - Js.to_bytestring (elt##.style##.margin) + Js.to_bytestring elt##.style##.margin + let marginBottom elt = let elt = get_elt "Css.marginBottom" elt in - Js.to_bytestring (elt##.style##.marginBottom) + Js.to_bytestring elt##.style##.marginBottom + let marginBottomPx elt = let elt = get_elt "Css.marginBottomPx" elt in - Js.parseInt (elt##.style##.marginBottom) + Js.parseInt elt##.style##.marginBottom + let marginLeft elt = let elt = get_elt "Css.marginLeft" elt in - Js.to_bytestring (elt##.style##.marginLeft) + Js.to_bytestring elt##.style##.marginLeft + let marginLeftPx elt = let elt = get_elt "Css.marginLeftPx" elt in - Js.parseInt (elt##.style##.marginLeft) + Js.parseInt elt##.style##.marginLeft + let marginRight elt = let elt = get_elt "Css.marginRight" elt in - Js.to_bytestring (elt##.style##.marginRight) + Js.to_bytestring elt##.style##.marginRight + let marginRightPx elt = let elt = get_elt "Css.marginRightPx" elt in - Js.parseInt (elt##.style##.marginRight) + Js.parseInt elt##.style##.marginRight + let marginTop elt = let elt = get_elt "Css.marginTop" elt in - Js.to_bytestring (elt##.style##.marginTop) + Js.to_bytestring elt##.style##.marginTop + let marginTopPx elt = let elt = get_elt "Css.marginTopPx" elt in - Js.parseInt (elt##.style##.marginTop) + Js.parseInt elt##.style##.marginTop + let maxHeight elt = let elt = get_elt "Css.maxHeight" elt in - Js.to_bytestring (elt##.style##.maxHeight) + Js.to_bytestring elt##.style##.maxHeight + let maxHeightPx elt = let elt = get_elt "Css.maxHeightPx" elt in - Js.parseInt (elt##.style##.maxHeight) + Js.parseInt elt##.style##.maxHeight + let maxWidth elt = let elt = get_elt "Css.maxWidth" elt in - Js.to_bytestring (elt##.style##.maxWidth) + Js.to_bytestring elt##.style##.maxWidth + let maxWidthPx elt = let elt = get_elt "Css.maxWidthPx" elt in - Js.parseInt (elt##.style##.maxWidth) + Js.parseInt elt##.style##.maxWidth + let minHeight elt = let elt = get_elt "Css.minHeight" elt in - Js.to_bytestring (elt##.style##.minHeight) + Js.to_bytestring elt##.style##.minHeight + let minHeightPx elt = let elt = get_elt "Css.minHeightPx" elt in - Js.parseInt (elt##.style##.minHeight) + Js.parseInt elt##.style##.minHeight + let minWidth elt = let elt = get_elt "Css.minWidth" elt in - Js.to_bytestring (elt##.style##.minWidth) + Js.to_bytestring elt##.style##.minWidth + let minWidthPx elt = let elt = get_elt "Css.minWidthPx" elt in - Js.parseInt (elt##.style##.minWidth) + Js.parseInt elt##.style##.minWidth + let opacity elt = let elt = get_elt "Css.opacity" elt in - option_map Js.to_bytestring (Js.Optdef.to_option (elt##.style##.opacity)) + option_map Js.to_bytestring (Js.Optdef.to_option elt##.style##.opacity) + let outline elt = let elt = get_elt "Css.outline" elt in - Js.to_bytestring (elt##.style##.outline) + Js.to_bytestring elt##.style##.outline + let outlineColor elt = let elt = get_elt "Css.outlineColor" elt in - Js.to_bytestring (elt##.style##.outlineColor) + Js.to_bytestring elt##.style##.outlineColor + let outlineOffset elt = let elt = get_elt "Css.outlineOffset" elt in - Js.to_bytestring (elt##.style##.outlineOffset) + Js.to_bytestring elt##.style##.outlineOffset + let outlineStyle elt = let elt = get_elt "Css.outlineStyle" elt in - Js.to_bytestring (elt##.style##.outlineStyle) + Js.to_bytestring elt##.style##.outlineStyle + let outlineWidth elt = let elt = get_elt "Css.outlineWidth" elt in - Js.to_bytestring (elt##.style##.outlineWidth) + Js.to_bytestring elt##.style##.outlineWidth + let overflow elt = let elt = get_elt "Css.overflow" elt in - Js.to_bytestring (elt##.style##.overflow) + Js.to_bytestring elt##.style##.overflow + let overflowX elt = let elt = get_elt "Css.overflowX" elt in - Js.to_bytestring (elt##.style##.overflowX) + Js.to_bytestring elt##.style##.overflowX + let overflowY elt = let elt = get_elt "Css.overflowY" elt in - Js.to_bytestring (elt##.style##.overflowY) + Js.to_bytestring elt##.style##.overflowY + let padding elt = let elt = get_elt "Css.padding" elt in - Js.to_bytestring (elt##.style##.padding) + Js.to_bytestring elt##.style##.padding + let paddingBottom elt = let elt = get_elt "Css.paddingBottom" elt in - Js.to_bytestring (elt##.style##.paddingBottom) + Js.to_bytestring elt##.style##.paddingBottom + let paddingBottomPx elt = let elt = get_elt "Css.paddingBottomPx" elt in - Js.parseInt (elt##.style##.paddingBottom) + Js.parseInt elt##.style##.paddingBottom + let paddingLeft elt = let elt = get_elt "Css.paddingLeft" elt in - Js.to_bytestring (elt##.style##.paddingLeft) + Js.to_bytestring elt##.style##.paddingLeft + let paddingLeftPx elt = let elt = get_elt "Css.paddingLeftPx" elt in - Js.parseInt (elt##.style##.paddingLeft) + Js.parseInt elt##.style##.paddingLeft + let paddingRight elt = let elt = get_elt "Css.paddingRight" elt in - Js.to_bytestring (elt##.style##.paddingRight) + Js.to_bytestring elt##.style##.paddingRight + let paddingRightPx elt = let elt = get_elt "Css.paddingRightPx" elt in - Js.parseInt (elt##.style##.paddingRight) + Js.parseInt elt##.style##.paddingRight + let paddingTop elt = let elt = get_elt "Css.paddingTop" elt in - Js.to_bytestring (elt##.style##.paddingTop) + Js.to_bytestring elt##.style##.paddingTop + let paddingTopPx elt = let elt = get_elt "Css.paddingTopPx" elt in - Js.parseInt (elt##.style##.paddingTop) + Js.parseInt elt##.style##.paddingTop + let pageBreakAfter elt = let elt = get_elt "Css.pageBreakAfter" elt in - Js.to_bytestring (elt##.style##.pageBreakAfter) + Js.to_bytestring elt##.style##.pageBreakAfter + let pageBreakBefore elt = let elt = get_elt "Css.pageBreakBefore" elt in - Js.to_bytestring (elt##.style##.pageBreakBefore) + Js.to_bytestring elt##.style##.pageBreakBefore + let position elt = let elt = get_elt "Css.position" elt in - Js.to_bytestring (elt##.style##.position) + Js.to_bytestring elt##.style##.position + let right elt = let elt = get_elt "Css.right" elt in - Js.to_bytestring (elt##.style##.right) + Js.to_bytestring elt##.style##.right + let rightPx elt = let elt = get_elt "Css.rightPx" elt in - Js.parseInt (elt##.style##.right) + Js.parseInt elt##.style##.right + let tableLayout elt = let elt = get_elt "Css.tableLayout" elt in - Js.to_bytestring (elt##.style##.tableLayout) + Js.to_bytestring elt##.style##.tableLayout + let textAlign elt = let elt = get_elt "Css.textAlign" elt in - Js.to_bytestring (elt##.style##.textAlign) + Js.to_bytestring elt##.style##.textAlign + let textDecoration elt = let elt = get_elt "Css.textDecoration" elt in - Js.to_bytestring (elt##.style##.textDecoration) + Js.to_bytestring elt##.style##.textDecoration + let textIndent elt = let elt = get_elt "Css.textIndent" elt in - Js.to_bytestring (elt##.style##.textIndent) + Js.to_bytestring elt##.style##.textIndent + let textTransform elt = let elt = get_elt "Css.textTransform" elt in - Js.to_bytestring (elt##.style##.textTransform) + Js.to_bytestring elt##.style##.textTransform + let top elt = let elt = get_elt "Css.top" elt in - Js.to_bytestring (elt##.style##.top) + Js.to_bytestring elt##.style##.top + let topPx elt = let elt = get_elt "Css.topPx" elt in - Js.parseInt (elt##.style##.top) + Js.parseInt elt##.style##.top + let verticalAlign elt = let elt = get_elt "Css.verticalAlign" elt in - Js.to_bytestring (elt##.style##.verticalAlign) + Js.to_bytestring elt##.style##.verticalAlign + let visibility elt = let elt = get_elt "Css.visibility" elt in - Js.to_bytestring (elt##.style##.visibility) + Js.to_bytestring elt##.style##.visibility + let whiteSpace elt = let elt = get_elt "Css.whiteSpace" elt in - Js.to_bytestring (elt##.style##.whiteSpace) + Js.to_bytestring elt##.style##.whiteSpace + let width elt = let elt = get_elt "Css.width" elt in - Js.to_bytestring (elt##.style##.width) + Js.to_bytestring elt##.style##.width + let widthPx elt = let elt = get_elt "Css.widthPx" elt in - Js.parseInt (elt##.style##.width) + Js.parseInt elt##.style##.width + let wordSpacing elt = let elt = get_elt "Css.wordSpacing" elt in - Js.to_bytestring (elt##.style##.wordSpacing) + Js.to_bytestring elt##.style##.wordSpacing + let zIndex elt = let elt = get_elt "Css.zIndex" elt in - Js.to_bytestring (elt##.style##.zIndex) + Js.to_bytestring elt##.style##.zIndex end module SetCss = struct let background elt v = let elt = get_elt "SetCss.background" elt in elt##.style##.background := Js.bytestring v + let backgroundAttachment elt v = let elt = get_elt "SetCss.backgroundAttachment" elt in elt##.style##.backgroundAttachment := Js.bytestring v + let backgroundColor elt v = let elt = get_elt "SetCss.backgroundColor" elt in elt##.style##.backgroundColor := Js.bytestring v + let backgroundImage elt v = let elt = get_elt "SetCss.backgroundImage" elt in elt##.style##.backgroundImage := Js.bytestring v + let backgroundPosition elt v = let elt = get_elt "SetCss.backgroundPosition" elt in elt##.style##.backgroundPosition := Js.bytestring v + let backgroundRepeat elt v = let elt = get_elt "SetCss.backgroundRepeat" elt in elt##.style##.backgroundRepeat := Js.bytestring v + let border elt v = let elt = get_elt "SetCss.border" elt in elt##.style##.border := Js.bytestring v + let borderBottom elt v = let elt = get_elt "SetCss.borderBottom" elt in elt##.style##.borderBottom := Js.bytestring v + let borderBottomColor elt v = let elt = get_elt "SetCss.borderBottomColor" elt in elt##.style##.borderBottomColor := Js.bytestring v + let borderBottomStyle elt v = let elt = get_elt "SetCss.borderBottomStyle" elt in elt##.style##.borderBottomStyle := Js.bytestring v + let borderBottomWidth elt v = let elt = get_elt "SetCss.borderBottomWidth" elt in elt##.style##.borderBottomWidth := Js.bytestring v - let borderBottomWidthPx elt v = borderBottomWidth elt (Printf.sprintf "%dpx" v) + + let borderBottomWidthPx elt v = + borderBottomWidth elt (Printf.sprintf "%dpx" v) + let borderCollapse elt v = let elt = get_elt "SetCss.borderCollapse" elt in elt##.style##.borderCollapse := Js.bytestring v + let borderColor elt v = let elt = get_elt "SetCss.borderColor" elt in elt##.style##.borderColor := Js.bytestring v + let borderLeft elt v = let elt = get_elt "SetCss.borderLeft" elt in elt##.style##.borderLeft := Js.bytestring v + let borderLeftColor elt v = let elt = get_elt "SetCss.borderLeftColor" elt in elt##.style##.borderLeftColor := Js.bytestring v + let borderLeftStyle elt v = let elt = get_elt "SetCss.borderLeftStyle" elt in elt##.style##.borderLeftStyle := Js.bytestring v + let borderLeftWidth elt v = let elt = get_elt "SetCss.borderLeftWidth" elt in elt##.style##.borderLeftWidth := Js.bytestring v + let borderLeftWidthPx elt v = borderLeftWidth elt (Printf.sprintf "%dpx" v) + let borderRight elt v = let elt = get_elt "SetCss.borderRight" elt in elt##.style##.borderRight := Js.bytestring v + let borderRightColor elt v = let elt = get_elt "SetCss.borderRightColor" elt in elt##.style##.borderRightColor := Js.bytestring v + let borderRightStyle elt v = let elt = get_elt "SetCss.borderRightStyle" elt in elt##.style##.borderRightStyle := Js.bytestring v + let borderRightWidth elt v = let elt = get_elt "SetCss.borderRightWidth" elt in elt##.style##.borderRightWidth := Js.bytestring v - let borderRightWidthPx elt v = borderRightWidth elt (Printf.sprintf "%dpx" v) + + let borderRightWidthPx elt v = + borderRightWidth elt (Printf.sprintf "%dpx" v) + let borderSpacing elt v = let elt = get_elt "SetCss.borderSpacing" elt in elt##.style##.borderSpacing := Js.bytestring v + let borderStyle elt v = let elt = get_elt "SetCss.borderStyle" elt in elt##.style##.borderStyle := Js.bytestring v + let borderTop elt v = let elt = get_elt "SetCss.borderTop" elt in elt##.style##.borderTop := Js.bytestring v + let borderTopColor elt v = let elt = get_elt "SetCss.borderTopColor" elt in elt##.style##.borderTopColor := Js.bytestring v + let borderTopStyle elt v = let elt = get_elt "SetCss.borderTopStyle" elt in elt##.style##.borderTopStyle := Js.bytestring v + let borderTopWidth elt v = let elt = get_elt "SetCss.borderTopWidth" elt in elt##.style##.borderTopWidth := Js.bytestring v + let borderTopWidthPx elt v = borderTopWidth elt (Printf.sprintf "%dpx" v) + let borderWidth elt v = let elt = get_elt "SetCss.borderWidth" elt in elt##.style##.borderWidth := Js.bytestring v + let bottom elt v = let elt = get_elt "SetCss.bottom" elt in elt##.style##.bottom := Js.bytestring v + let bottomPx elt v = bottom elt (Printf.sprintf "%dpx" v) + let captionSide elt v = let elt = get_elt "SetCss.captionSide" elt in elt##.style##.captionSide := Js.bytestring v + let clear elt v = let elt = get_elt "SetCss.clear" elt in elt##.style##.clear := Js.bytestring v + let clip elt v = let elt = get_elt "SetCss.clip" elt in elt##.style##.clip := Js.bytestring v + let color elt v = let elt = get_elt "SetCss.color" elt in elt##.style##.color := Js.bytestring v + let content elt v = let elt = get_elt "SetCss.content" elt in elt##.style##.content := Js.bytestring v + let counterIncrement elt v = let elt = get_elt "SetCss.counterIncrement" elt in elt##.style##.counterIncrement := Js.bytestring v + let counterReset elt v = let elt = get_elt "SetCss.counterReset" elt in elt##.style##.counterReset := Js.bytestring v + let cssFloat elt v = let elt = get_elt "SetCss.cssFloat" elt in elt##.style##.cssFloat := Js.bytestring v + let cssText elt v = let elt = get_elt "SetCss.cssText" elt in elt##.style##.cssText := Js.bytestring v + let cursor elt v = let elt = get_elt "SetCss.cursor" elt in elt##.style##.cursor := Js.bytestring v + let direction elt v = let elt = get_elt "SetCss.direction" elt in elt##.style##.direction := Js.bytestring v + let display elt v = let elt = get_elt "SetCss.display" elt in elt##.style##.display := Js.bytestring v + let emptyCells elt v = let elt = get_elt "SetCss.emptyCells" elt in elt##.style##.emptyCells := Js.bytestring v + let font elt v = let elt = get_elt "SetCss.font" elt in elt##.style##.font := Js.bytestring v + let fontFamily elt v = let elt = get_elt "SetCss.fontFamily" elt in elt##.style##.fontFamily := Js.bytestring v + let fontSize elt v = let elt = get_elt "SetCss.fontSize" elt in elt##.style##.fontSize := Js.bytestring v + let fontStyle elt v = let elt = get_elt "SetCss.fontStyle" elt in elt##.style##.fontStyle := Js.bytestring v + let fontVariant elt v = let elt = get_elt "SetCss.fontVariant" elt in elt##.style##.fontVariant := Js.bytestring v + let fontWeight elt v = let elt = get_elt "SetCss.fontWeight" elt in elt##.style##.fontWeight := Js.bytestring v + let height elt v = let elt = get_elt "SetCss.height" elt in elt##.style##.height := Js.bytestring v + let heightPx elt v = height elt (Printf.sprintf "%dpx" v) + let left elt v = let elt = get_elt "SetCss.left" elt in elt##.style##.left := Js.bytestring v + let leftPx elt v = left elt (Printf.sprintf "%dpx" v) + let letterSpacing elt v = let elt = get_elt "SetCss.letterSpacing" elt in elt##.style##.letterSpacing := Js.bytestring v + let lineHeight elt v = let elt = get_elt "SetCss.lineHeight" elt in elt##.style##.lineHeight := Js.bytestring v + let listStyle elt v = let elt = get_elt "SetCss.listStyle" elt in elt##.style##.listStyle := Js.bytestring v + let listStyleImage elt v = let elt = get_elt "SetCss.listStyleImage" elt in elt##.style##.listStyleImage := Js.bytestring v + let listStylePosition elt v = let elt = get_elt "SetCss.listStylePosition" elt in elt##.style##.listStylePosition := Js.bytestring v + let listStyleType elt v = let elt = get_elt "SetCss.listStyleType" elt in elt##.style##.listStyleType := Js.bytestring v + let margin elt v = let elt = get_elt "SetCss.margin" elt in elt##.style##.margin := Js.bytestring v + let marginBottom elt v = let elt = get_elt "SetCss.marginBottom" elt in elt##.style##.marginBottom := Js.bytestring v + let marginBottomPx elt v = marginBottom elt (Printf.sprintf "%dpx" v) + let marginLeft elt v = let elt = get_elt "SetCss.marginLeft" elt in elt##.style##.marginLeft := Js.bytestring v + let marginLeftPx elt v = marginLeft elt (Printf.sprintf "%dpx" v) + let marginRight elt v = let elt = get_elt "SetCss.marginRight" elt in elt##.style##.marginRight := Js.bytestring v + let marginRightPx elt v = marginRight elt (Printf.sprintf "%dpx" v) + let marginTop elt v = let elt = get_elt "SetCss.marginTop" elt in elt##.style##.marginTop := Js.bytestring v + let marginTopPx elt v = marginTop elt (Printf.sprintf "%dpx" v) + let maxHeight elt v = let elt = get_elt "SetCss.maxHeight" elt in elt##.style##.maxHeight := Js.bytestring v + let maxHeightPx elt v = maxHeight elt (Printf.sprintf "%dpx" v) + let maxWidth elt v = let elt = get_elt "SetCss.maxWidth" elt in elt##.style##.maxWidth := Js.bytestring v + let maxWidthPx elt v = maxWidth elt (Printf.sprintf "%dpx" v) + let minHeight elt v = let elt = get_elt "SetCss.minHeight" elt in elt##.style##.minHeight := Js.bytestring v + let minHeightPx elt v = minHeight elt (Printf.sprintf "%dpx" v) + let minWidth elt v = let elt = get_elt "SetCss.minWidth" elt in elt##.style##.minWidth := Js.bytestring v + let minWidthPx elt v = minWidth elt (Printf.sprintf "%dpx" v) + let opacity elt v = let elt = get_elt "SetCss.opacity" elt in - elt##.style##.opacity := match v with None -> Js.undefined | Some v -> Js.def (Js.bytestring v) + elt##.style##.opacity + := + match v with None -> Js.undefined | Some v -> Js.def (Js.bytestring v) + let outline elt v = let elt = get_elt "SetCss.outline" elt in elt##.style##.outline := Js.bytestring v + let outlineColor elt v = let elt = get_elt "SetCss.outlineColor" elt in elt##.style##.outlineColor := Js.bytestring v + let outlineOffset elt v = let elt = get_elt "SetCss.outlineOffset" elt in elt##.style##.outlineOffset := Js.bytestring v + let outlineStyle elt v = let elt = get_elt "SetCss.outlineStyle" elt in elt##.style##.outlineStyle := Js.bytestring v + let outlineWidth elt v = let elt = get_elt "SetCss.outlineWidth" elt in elt##.style##.outlineWidth := Js.bytestring v + let overflow elt v = let elt = get_elt "SetCss.overflow" elt in elt##.style##.overflow := Js.bytestring v + let overflowX elt v = let elt = get_elt "SetCss.overflowX" elt in elt##.style##.overflowX := Js.bytestring v + let overflowY elt v = let elt = get_elt "SetCss.overflowY" elt in elt##.style##.overflowY := Js.bytestring v + let padding elt v = let elt = get_elt "SetCss.padding" elt in elt##.style##.padding := Js.bytestring v + let paddingBottom elt v = let elt = get_elt "SetCss.paddingBottom" elt in elt##.style##.paddingBottom := Js.bytestring v + let paddingBottomPx elt v = paddingBottom elt (Printf.sprintf "%dpx" v) + let paddingLeft elt v = let elt = get_elt "SetCss.paddingLeft" elt in elt##.style##.paddingLeft := Js.bytestring v + let paddingLeftPx elt v = paddingLeft elt (Printf.sprintf "%dpx" v) + let paddingRight elt v = let elt = get_elt "SetCss.paddingRight" elt in elt##.style##.paddingRight := Js.bytestring v + let paddingRightPx elt v = paddingRight elt (Printf.sprintf "%dpx" v) + let paddingTop elt v = let elt = get_elt "SetCss.paddingTop" elt in elt##.style##.paddingTop := Js.bytestring v + let paddingTopPx elt v = paddingTop elt (Printf.sprintf "%dpx" v) + let pageBreakAfter elt v = let elt = get_elt "SetCss.pageBreakAfter" elt in elt##.style##.pageBreakAfter := Js.bytestring v + let pageBreakBefore elt v = let elt = get_elt "SetCss.pageBreakBefore" elt in elt##.style##.pageBreakBefore := Js.bytestring v + let position elt v = let elt = get_elt "SetCss.position" elt in elt##.style##.position := Js.bytestring v + let right elt v = let elt = get_elt "SetCss.right" elt in elt##.style##.right := Js.bytestring v + let rightPx elt v = right elt (Printf.sprintf "%dpx" v) + let tableLayout elt v = let elt = get_elt "SetCss.tableLayout" elt in elt##.style##.tableLayout := Js.bytestring v + let textAlign elt v = let elt = get_elt "SetCss.textAlign" elt in elt##.style##.textAlign := Js.bytestring v + let textDecoration elt v = let elt = get_elt "SetCss.textDecoration" elt in elt##.style##.textDecoration := Js.bytestring v + let textIndent elt v = let elt = get_elt "SetCss.textIndent" elt in elt##.style##.textIndent := Js.bytestring v + let textTransform elt v = let elt = get_elt "SetCss.textTransform" elt in elt##.style##.textTransform := Js.bytestring v + let top elt v = let elt = get_elt "SetCss.top" elt in elt##.style##.top := Js.bytestring v + let topPx elt v = top elt (Printf.sprintf "%dpx" v) + let verticalAlign elt v = let elt = get_elt "SetCss.verticalAlign" elt in elt##.style##.verticalAlign := Js.bytestring v + let visibility elt v = let elt = get_elt "SetCss.visibility" elt in elt##.style##.visibility := Js.bytestring v + let whiteSpace elt v = let elt = get_elt "SetCss.whiteSpace" elt in elt##.style##.whiteSpace := Js.bytestring v + let width elt v = let elt = get_elt "SetCss.width" elt in elt##.style##.width := Js.bytestring v + let widthPx elt v = width elt (Printf.sprintf "%dpx" v) + let wordSpacing elt v = let elt = get_elt "SetCss.wordSpacing" elt in elt##.style##.wordSpacing := Js.bytestring v + let zIndex elt v = let elt = get_elt "SetCss.zIndex" elt in elt##.style##.zIndex := Js.bytestring v end - end let hide elt = Manip.SetCss.display elt "none" @@ -1132,43 +1405,48 @@ let hide elt = Manip.SetCss.display elt "none" let show elt = Manip.SetCss.display elt "" let window_open ?features url name = - let features = match features with - | None -> Js.null - | Some s -> Js.some @@ Js.string s in - window##(open_ (Js.string url) (Js.string name) features) + let features = + match features with None -> Js.null | Some s -> Js.some @@ Js.string s + in + window ## (open_ (Js.string url) (Js.string name) features) module Window = struct let close win = win##close + let body win = Tyxml_js.Of_dom.of_body win##.document##.body + let head win = Tyxml_js.Of_dom.of_head win##.document##.head + let onunload ?(win = Dom_html.window) f = win##.onunload := Dom_html.handler (fun ev -> Js.bool (f ev)) + let onresize ?(win = Dom_html.window) f = win##.onresize := Dom_html.handler (fun ev -> Js.bool (f ev)) + let prompt ?(win = Dom_html.window) ?(value = "") msg = Js.Opt.case - (win##(prompt (Js.string msg) (Js.string value))) + win ## (prompt (Js.string msg) (Js.string value)) (fun () -> "") Js.to_string + let onhashchange ?(win = Dom_html.window) f = win##.onhashchange := Dom_html.handler (fun ev -> Js.bool (f ev)) - end - +end module Document = struct - let uri () = Js.to_string (doc##._URL) + let uri () = Js.to_string doc##._URL end let parse_fragment () = - let elts = - Regexp.(split (regexp "(&|%26)") (Url.Current.get_fragment ())) in + let elts = Regexp.(split (regexp "(&|%26)") (Url.Current.get_fragment ())) in List.fold_right (fun elt acc -> - if elt = "&" || elt = "%26" || elt = "" then acc else - match Regexp.(split (regexp "(=|%3D)") elt) with - | [name] -> (name, "") :: acc - | name :: _ :: value -> (name, String.concat "" value) :: acc - | _ -> assert false) + if elt = "&" || elt = "%26" || elt = "" then acc + else + match Regexp.(split (regexp "(=|%3D)") elt) with + | [name] -> (name, "") :: acc + | name :: _ :: value -> (name, String.concat "" value) :: acc + | _ -> assert false ) elts [] let set_fragment args = @@ -1178,13 +1456,16 @@ let set_fragment args = let local_args = ref [] -module MakeLocal(V: sig type t val name: string end) = struct +module MakeLocal (V : sig + type t + val name : string +end) = +struct let () = if List.mem V.name !local_args then warn "Duplicate key in LocalStorage: %s" V.name - else - local_args := V.name :: !local_args + else local_args := V.name :: !local_args let get_storage () = try @@ -1193,38 +1474,42 @@ module MakeLocal(V: sig type t val name: string end) = struct | Some t -> t with exn -> let msg = - Format.sprintf - "Warning: can't access to localStorage.\n%s@." - (Printexc.to_string exn) in - Firebug.console##(log (Js.string msg)); + Format.sprintf "Warning: can't access to localStorage.\n%s@." + (Printexc.to_string exn) + in + Firebug.console ## (log (Js.string msg)); raise Not_found let name = Js.string V.name - let get (): V.t option = + let get () : V.t option = try let s = get_storage () in - match Js.Opt.to_option (s##(getItem name)) with + match Js.Opt.to_option s ## (getItem name) with | None -> None | Some s -> Some (Json.unsafe_input s) with Not_found -> None - let set: V.t -> unit = fun v -> + let set : V.t -> unit = + fun v -> try let s = get_storage () in let str = Json.output v in - s##(setItem name str) + s ## (setItem name str) with Not_found -> () - end let js_code_url code = - let blob = File.blob_from_string ~contentType:"application/javascript" code in + let blob = + File.blob_from_string ~contentType:"application/javascript" code + in let url = Dom_html.window##._URL##createObjectURL blob in Js.to_string url let worker_with_code code = - let blob = File.blob_from_string ~contentType:"application/javascript" code in + let blob = + File.blob_from_string ~contentType:"application/javascript" code + in let url = Dom_html.window##._URL##createObjectURL blob in Worker.create (Js.to_string url) diff --git a/src/utils/js_utils.mli b/src/utils/js_utils.mli index f3195027a..70c08a2a1 100644 --- a/src/utils/js_utils.mli +++ b/src/utils/js_utils.mli @@ -18,394 +18,693 @@ open Js_of_ocaml -val alert: string -> unit -val confirm: string -> bool +val alert : string -> unit -val log: ('a, Format.formatter, unit, unit) format4 -> 'a -val debug: ('a, Format.formatter, unit, unit) format4 -> 'a -val warn: ('a, Format.formatter, unit, unit) format4 -> 'a -val error: ('a, Format.formatter, unit, unit) format4 -> 'a +val confirm : string -> bool -val js_log: 'a -> unit -val js_debug: 'a -> unit -val js_warn: 'a -> unit -val js_error: 'a -> unit +val log : ('a, Format.formatter, unit, unit) format4 -> 'a -val reload: unit -> unit +val debug : ('a, Format.formatter, unit, unit) format4 -> 'a +val warn : ('a, Format.formatter, unit, unit) format4 -> 'a + +val error : ('a, Format.formatter, unit, unit) format4 -> 'a + +val js_log : 'a -> unit + +val js_debug : 'a -> unit + +val js_warn : 'a -> unit + +val js_error : 'a -> unit + +val reload : unit -> unit + +val get_lang : unit -> string option (** Gets the language configured in the browser *) -val get_lang: unit -> string option module Manip : sig - (* à la Eliom_content.Manip ... *) open Tyxml_js.Html5 - val window: 'a elt -> Dom_html.window Js.t + val window : 'a elt -> Dom_html.window Js.t + + val setInnerHtml : 'a elt -> string -> unit + + val setInnerText : 'a elt -> string -> unit + + val clone : ?deep:bool -> 'a elt -> 'a elt + + val appendChild : ?before:'a elt -> 'b elt -> 'c elt -> unit + + val appendToHead : ?before:'a elt -> 'c elt -> unit + + val appendToBody : ?before:'a elt -> 'c elt -> unit + + val appendChildren : ?before:'a elt -> 'b elt -> 'c elt list -> unit + + val appendChildFirst : 'b elt -> 'c elt -> unit + + val insertChildAfter : 'a elt -> 'b elt -> 'c elt -> unit + + val insertChildrenAfter : 'a elt -> 'b elt -> 'c elt list -> unit + + val nth : 'a elt -> int -> 'b elt option - val setInnerHtml: 'a elt -> string -> unit - val setInnerText: 'a elt -> string -> unit - val clone: ?deep:bool -> 'a elt -> 'a elt + val childLength : 'a elt -> int - val appendChild: ?before:'a elt -> 'b elt -> 'c elt -> unit - val appendToHead: ?before:'a elt -> 'c elt -> unit - val appendToBody: ?before:'a elt -> 'c elt -> unit - val appendChildren: ?before:'a elt -> 'b elt -> 'c elt list -> unit - val appendChildFirst: 'b elt -> 'c elt -> unit - val insertChildAfter: 'a elt -> 'b elt -> 'c elt -> unit - val insertChildrenAfter: 'a elt -> 'b elt -> 'c elt list -> unit - val nth: 'a elt -> int -> 'b elt option - val childLength: 'a elt -> int - val removeChild: 'a elt -> 'b elt -> unit - val replaceChild: 'a elt -> 'b elt -> 'c elt -> unit - val replaceChildren: 'a elt -> 'b elt list -> unit - val removeChildren: 'a elt -> unit - val removeSelf: 'a elt -> unit - val replaceSelf: 'a elt -> 'a elt -> unit + val removeChild : 'a elt -> 'b elt -> unit - val children: 'a elt -> 'b elt list - val by_id: string -> 'b elt option - val by_classname: string -> 'b elt list + val replaceChild : 'a elt -> 'b elt -> 'c elt -> unit - val disable: 'a elt -> unit - val enable: 'a elt -> unit + val replaceChildren : 'a elt -> 'b elt list -> unit - val value: 'a elt -> string + val removeChildren : 'a elt -> unit - val hasClass: 'a elt -> string -> bool - val addClass: 'a elt -> string -> unit - val removeClass: 'a elt -> string -> unit + val removeSelf : 'a elt -> unit - val scrollIntoView: ?bottom:bool -> 'a elt -> unit + val replaceSelf : 'a elt -> 'a elt -> unit + + val children : 'a elt -> 'b elt list + + val by_id : string -> 'b elt option + + val by_classname : string -> 'b elt list + + val disable : 'a elt -> unit + + val enable : 'a elt -> unit + + val value : 'a elt -> string + + val hasClass : 'a elt -> string -> bool + + val addClass : 'a elt -> string -> unit + + val removeClass : 'a elt -> string -> unit + + val scrollIntoView : ?bottom:bool -> 'a elt -> unit (* Returns [true] if the class has been set, [false] if it was unset *) - val toggleClass: 'a elt -> string -> bool + val toggleClass : 'a elt -> string -> bool - val focus: 'a elt -> unit - val blur: 'a elt -> unit + val focus : 'a elt -> unit + + val blur : 'a elt -> unit module Elt : sig val body : [`Body] elt + val active : unit -> 'a elt end module Ev : sig type ('a, 'b) ev = 'a elt -> ('b Js.t -> bool) -> unit - type ('a,'b) ev_unit = 'a elt -> ('b Js.t -> unit) -> unit - val onkeyup: ('a,Dom_html.keyboardEvent) ev - val onkeydown: ('a,Dom_html.keyboardEvent) ev - val onmouseup: ('a,Dom_html.mouseEvent) ev - val onmousedown: ('a,Dom_html.mouseEvent) ev - val onmouseout: ('a,Dom_html.mouseEvent) ev - val onmouseover: ('a,Dom_html.mouseEvent) ev - val onclick: ('a,Dom_html.mouseEvent) ev - val ondblclick: ('a,Dom_html.mouseEvent) ev - val onload: ('a,Dom_html.event) ev - val onerror: ('a,Dom_html.event) ev - val onabort: ('a,Dom_html.event) ev - val onfocus: ('a,Dom_html.event) ev - val onblur: ('a,Dom_html.event) ev - val onfocus_textarea: ('a,Dom_html.event) ev - val onblur_textarea: ('a,Dom_html.event) ev - val onscroll: ('a,Dom_html.event) ev - val onreturn: ('a,Dom_html.keyboardEvent) ev_unit - val onchange: ('a,Dom_html.event) ev - val onchange_select: ('a,Dom_html.event) ev - val oninput: ('a,Dom_html.event) ev + + type ('a, 'b) ev_unit = 'a elt -> ('b Js.t -> unit) -> unit + + val onkeyup : ('a, Dom_html.keyboardEvent) ev + + val onkeydown : ('a, Dom_html.keyboardEvent) ev + + val onmouseup : ('a, Dom_html.mouseEvent) ev + + val onmousedown : ('a, Dom_html.mouseEvent) ev + + val onmouseout : ('a, Dom_html.mouseEvent) ev + + val onmouseover : ('a, Dom_html.mouseEvent) ev + + val onclick : ('a, Dom_html.mouseEvent) ev + + val ondblclick : ('a, Dom_html.mouseEvent) ev + + val onload : ('a, Dom_html.event) ev + + val onerror : ('a, Dom_html.event) ev + + val onabort : ('a, Dom_html.event) ev + + val onfocus : ('a, Dom_html.event) ev + + val onblur : ('a, Dom_html.event) ev + + val onfocus_textarea : ('a, Dom_html.event) ev + + val onblur_textarea : ('a, Dom_html.event) ev + + val onscroll : ('a, Dom_html.event) ev + + val onreturn : ('a, Dom_html.keyboardEvent) ev_unit + + val onchange : ('a, Dom_html.event) ev + + val onchange_select : ('a, Dom_html.event) ev + + val oninput : ('a, Dom_html.event) ev end module Attr : sig - val clientWidth: 'a elt -> int - val clientHeight: 'a elt -> int - val offsetWidth: 'a elt -> int - val offsetHeight: 'a elt -> int - val clientLeft: 'a elt -> int - val clientTop: 'a elt -> int + val clientWidth : 'a elt -> int + + val clientHeight : 'a elt -> int + + val offsetWidth : 'a elt -> int + + val offsetHeight : 'a elt -> int + + val clientLeft : 'a elt -> int + + val clientTop : 'a elt -> int end (** Read the CSS properties of DOM elements. *) module Css : sig - val background: 'a elt -> string - val backgroundAttachment: 'a elt -> string - val backgroundColor: 'a elt -> string - val backgroundImage: 'a elt -> string - val backgroundPosition: 'a elt -> string - val backgroundRepeat: 'a elt -> string - val border: 'a elt -> string - val borderBottom: 'a elt -> string - val borderBottomColor: 'a elt -> string - val borderBottomStyle: 'a elt -> string - val borderBottomWidth: 'a elt -> string - val borderBottomWidthPx: 'a elt -> int - val borderCollapse: 'a elt -> string - val borderColor: 'a elt -> string - val borderLeft: 'a elt -> string - val borderLeftColor: 'a elt -> string - val borderLeftStyle: 'a elt -> string - val borderLeftWidth: 'a elt -> string - val borderLeftWidthPx: 'a elt -> int - val borderRight: 'a elt -> string - val borderRightColor: 'a elt -> string - val borderRightStyle: 'a elt -> string - val borderRightWidth: 'a elt -> string - val borderRightWidthPx: 'a elt -> int - val borderSpacing: 'a elt -> string - val borderStyle: 'a elt -> string - val borderTop: 'a elt -> string - val borderTopColor: 'a elt -> string - val borderTopStyle: 'a elt -> string - val borderTopWidth: 'a elt -> string - val borderTopWidthPx: 'a elt -> int - val borderWidth: 'a elt -> string - val borderWidthPx: 'a elt -> int - val bottom: 'a elt -> string - val captionSide: 'a elt -> string - val clear: 'a elt -> string - val clip: 'a elt -> string - val color: 'a elt -> string - val content: 'a elt -> string - val counterIncrement: 'a elt -> string - val counterReset: 'a elt -> string - val cssFloat: 'a elt -> string - val cssText: 'a elt -> string - val cursor: 'a elt -> string - val direction: 'a elt -> string - val display: 'a elt -> string - val emptyCells: 'a elt -> string - val font: 'a elt -> string - val fontFamily: 'a elt -> string - val fontSize: 'a elt -> string - val fontStyle: 'a elt -> string - val fontVariant: 'a elt -> string - val fontWeight: 'a elt -> string - val height: 'a elt -> string - val heightPx: 'a elt -> int - val left: 'a elt -> string - val leftPx: 'a elt -> int - val letterSpacing: 'a elt -> string - val lineHeight: 'a elt -> string - val listStyle: 'a elt -> string - val listStyleImage: 'a elt -> string - val listStylePosition: 'a elt -> string - val listStyleType: 'a elt -> string - val margin: 'a elt -> string - val marginBottom: 'a elt -> string - val marginBottomPx: 'a elt -> int - val marginLeft: 'a elt -> string - val marginLeftPx: 'a elt -> int - val marginRight: 'a elt -> string - val marginRightPx: 'a elt -> int - val marginTop: 'a elt -> string - val marginTopPx: 'a elt -> int - val maxHeight: 'a elt -> string - val maxHeightPx: 'a elt -> int - val maxWidth: 'a elt -> string - val maxWidthPx: 'a elt -> int - val minHeight: 'a elt -> string - val minHeightPx: 'a elt -> int - val minWidth: 'a elt -> string - val minWidthPx: 'a elt -> int - val opacity: 'a elt -> string option - val outline: 'a elt -> string - val outlineColor: 'a elt -> string - val outlineOffset: 'a elt -> string - val outlineStyle: 'a elt -> string - val outlineWidth: 'a elt -> string - val overflow: 'a elt -> string - val overflowX: 'a elt -> string - val overflowY: 'a elt -> string - val padding: 'a elt -> string - val paddingBottom: 'a elt -> string - val paddingBottomPx: 'a elt -> int - val paddingLeft: 'a elt -> string - val paddingLeftPx: 'a elt -> int - val paddingRight: 'a elt -> string - val paddingRightPx: 'a elt -> int - val paddingTop: 'a elt -> string - val paddingTopPx: 'a elt -> int - val pageBreakAfter: 'a elt -> string - val pageBreakBefore: 'a elt -> string - val position: 'a elt -> string - val right: 'a elt -> string - val rightPx: 'a elt -> int - val tableLayout: 'a elt -> string - val textAlign: 'a elt -> string - val textDecoration: 'a elt -> string - val textIndent: 'a elt -> string - val textTransform: 'a elt -> string - val top: 'a elt -> string - val topPx: 'a elt -> int - val verticalAlign: 'a elt -> string - val visibility: 'a elt -> string - val whiteSpace: 'a elt -> string - val width: 'a elt -> string - val widthPx: 'a elt -> int - val wordSpacing: 'a elt -> string - val zIndex: 'a elt -> string + val background : 'a elt -> string + + val backgroundAttachment : 'a elt -> string + + val backgroundColor : 'a elt -> string + + val backgroundImage : 'a elt -> string + + val backgroundPosition : 'a elt -> string + + val backgroundRepeat : 'a elt -> string + + val border : 'a elt -> string + + val borderBottom : 'a elt -> string + + val borderBottomColor : 'a elt -> string + + val borderBottomStyle : 'a elt -> string + + val borderBottomWidth : 'a elt -> string + + val borderBottomWidthPx : 'a elt -> int + + val borderCollapse : 'a elt -> string + + val borderColor : 'a elt -> string + + val borderLeft : 'a elt -> string + + val borderLeftColor : 'a elt -> string + + val borderLeftStyle : 'a elt -> string + + val borderLeftWidth : 'a elt -> string + + val borderLeftWidthPx : 'a elt -> int + + val borderRight : 'a elt -> string + + val borderRightColor : 'a elt -> string + + val borderRightStyle : 'a elt -> string + + val borderRightWidth : 'a elt -> string + + val borderRightWidthPx : 'a elt -> int + + val borderSpacing : 'a elt -> string + + val borderStyle : 'a elt -> string + + val borderTop : 'a elt -> string + + val borderTopColor : 'a elt -> string + + val borderTopStyle : 'a elt -> string + + val borderTopWidth : 'a elt -> string + + val borderTopWidthPx : 'a elt -> int + + val borderWidth : 'a elt -> string + + val borderWidthPx : 'a elt -> int + + val bottom : 'a elt -> string + + val captionSide : 'a elt -> string + + val clear : 'a elt -> string + + val clip : 'a elt -> string + + val color : 'a elt -> string + + val content : 'a elt -> string + + val counterIncrement : 'a elt -> string + + val counterReset : 'a elt -> string + + val cssFloat : 'a elt -> string + + val cssText : 'a elt -> string + + val cursor : 'a elt -> string + + val direction : 'a elt -> string + + val display : 'a elt -> string + + val emptyCells : 'a elt -> string + + val font : 'a elt -> string + + val fontFamily : 'a elt -> string + + val fontSize : 'a elt -> string + + val fontStyle : 'a elt -> string + + val fontVariant : 'a elt -> string + + val fontWeight : 'a elt -> string + + val height : 'a elt -> string + + val heightPx : 'a elt -> int + + val left : 'a elt -> string + + val leftPx : 'a elt -> int + + val letterSpacing : 'a elt -> string + + val lineHeight : 'a elt -> string + + val listStyle : 'a elt -> string + + val listStyleImage : 'a elt -> string + + val listStylePosition : 'a elt -> string + + val listStyleType : 'a elt -> string + + val margin : 'a elt -> string + + val marginBottom : 'a elt -> string + + val marginBottomPx : 'a elt -> int + + val marginLeft : 'a elt -> string + + val marginLeftPx : 'a elt -> int + + val marginRight : 'a elt -> string + + val marginRightPx : 'a elt -> int + + val marginTop : 'a elt -> string + + val marginTopPx : 'a elt -> int + + val maxHeight : 'a elt -> string + + val maxHeightPx : 'a elt -> int + + val maxWidth : 'a elt -> string + + val maxWidthPx : 'a elt -> int + + val minHeight : 'a elt -> string + + val minHeightPx : 'a elt -> int + + val minWidth : 'a elt -> string + + val minWidthPx : 'a elt -> int + + val opacity : 'a elt -> string option + + val outline : 'a elt -> string + + val outlineColor : 'a elt -> string + + val outlineOffset : 'a elt -> string + + val outlineStyle : 'a elt -> string + + val outlineWidth : 'a elt -> string + + val overflow : 'a elt -> string + + val overflowX : 'a elt -> string + + val overflowY : 'a elt -> string + + val padding : 'a elt -> string + + val paddingBottom : 'a elt -> string + + val paddingBottomPx : 'a elt -> int + + val paddingLeft : 'a elt -> string + + val paddingLeftPx : 'a elt -> int + + val paddingRight : 'a elt -> string + + val paddingRightPx : 'a elt -> int + + val paddingTop : 'a elt -> string + + val paddingTopPx : 'a elt -> int + + val pageBreakAfter : 'a elt -> string + + val pageBreakBefore : 'a elt -> string + + val position : 'a elt -> string + + val right : 'a elt -> string + + val rightPx : 'a elt -> int + + val tableLayout : 'a elt -> string + + val textAlign : 'a elt -> string + + val textDecoration : 'a elt -> string + + val textIndent : 'a elt -> string + + val textTransform : 'a elt -> string + + val top : 'a elt -> string + + val topPx : 'a elt -> int + + val verticalAlign : 'a elt -> string + + val visibility : 'a elt -> string + + val whiteSpace : 'a elt -> string + + val width : 'a elt -> string + + val widthPx : 'a elt -> int + + val wordSpacing : 'a elt -> string + + val zIndex : 'a elt -> string end (** Modify the CSS properties of DOM elements. *) module SetCss : sig - val background: 'a elt -> string -> unit - val backgroundAttachment: 'a elt -> string -> unit - val backgroundColor: 'a elt -> string -> unit - val backgroundImage: 'a elt -> string -> unit - val backgroundPosition: 'a elt -> string -> unit - val backgroundRepeat: 'a elt -> string -> unit - val border: 'a elt -> string -> unit - val borderBottom: 'a elt -> string -> unit - val borderBottomColor: 'a elt -> string -> unit - val borderBottomStyle: 'a elt -> string -> unit - val borderBottomWidth: 'a elt -> string -> unit - val borderBottomWidthPx: 'a elt -> int -> unit - val borderCollapse: 'a elt -> string -> unit - val borderColor: 'a elt -> string -> unit - val borderLeft: 'a elt -> string -> unit - val borderLeftColor: 'a elt -> string -> unit - val borderLeftStyle: 'a elt -> string -> unit - val borderLeftWidth: 'a elt -> string -> unit - val borderLeftWidthPx: 'a elt -> int -> unit - val borderRight: 'a elt -> string -> unit - val borderRightColor: 'a elt -> string -> unit - val borderRightStyle: 'a elt -> string -> unit - val borderRightWidth: 'a elt -> string -> unit - val borderRightWidthPx: 'a elt -> int -> unit - val borderSpacing: 'a elt -> string -> unit - val borderStyle: 'a elt -> string -> unit - val borderTop: 'a elt -> string -> unit - val borderTopColor: 'a elt -> string -> unit - val borderTopStyle: 'a elt -> string -> unit - val borderTopWidth: 'a elt -> string -> unit - val borderTopWidthPx: 'a elt -> int -> unit - val borderWidth: 'a elt -> string -> unit - val bottom: 'a elt -> string -> unit - val bottomPx: 'a elt -> int -> unit - val captionSide: 'a elt -> string -> unit - val clear: 'a elt -> string -> unit - val clip: 'a elt -> string -> unit - val color: 'a elt -> string -> unit - val content: 'a elt -> string -> unit - val counterIncrement: 'a elt -> string -> unit - val counterReset: 'a elt -> string -> unit - val cssFloat: 'a elt -> string -> unit - val cssText: 'a elt -> string -> unit - val cursor: 'a elt -> string -> unit - val direction: 'a elt -> string -> unit - val display: 'a elt -> string -> unit - val emptyCells: 'a elt -> string -> unit - val font: 'a elt -> string -> unit - val fontFamily: 'a elt -> string -> unit - val fontSize: 'a elt -> string -> unit - val fontStyle: 'a elt -> string -> unit - val fontVariant: 'a elt -> string -> unit - val fontWeight: 'a elt -> string -> unit - val height: 'a elt -> string -> unit - val heightPx: 'a elt -> int -> unit - val left: 'a elt -> string -> unit - val leftPx: 'a elt -> int -> unit - val letterSpacing: 'a elt -> string -> unit - val lineHeight: 'a elt -> string -> unit - val listStyle: 'a elt -> string -> unit - val listStyleImage: 'a elt -> string -> unit - val listStylePosition: 'a elt -> string -> unit - val listStyleType: 'a elt -> string -> unit - val margin: 'a elt -> string -> unit - val marginBottom: 'a elt -> string -> unit - val marginBottomPx: 'a elt -> int -> unit - val marginLeft: 'a elt -> string -> unit - val marginLeftPx: 'a elt -> int -> unit - val marginRight: 'a elt -> string -> unit - val marginRightPx: 'a elt -> int -> unit - val marginTop: 'a elt -> string -> unit - val marginTopPx: 'a elt -> int -> unit - val maxHeight: 'a elt -> string -> unit - val maxHeightPx: 'a elt -> int -> unit - val maxWidth: 'a elt -> string -> unit - val maxWidthPx: 'a elt -> int -> unit - val minHeight: 'a elt -> string -> unit - val minHeightPx: 'a elt -> int -> unit - val minWidth: 'a elt -> string -> unit - val minWidthPx: 'a elt -> int -> unit - val opacity: 'a elt -> string option -> unit - val outline: 'a elt -> string -> unit - val outlineColor: 'a elt -> string -> unit - val outlineOffset: 'a elt -> string -> unit - val outlineStyle: 'a elt -> string -> unit - val outlineWidth: 'a elt -> string -> unit - val overflow: 'a elt -> string -> unit - val overflowX: 'a elt -> string -> unit - val overflowY: 'a elt -> string -> unit - val padding: 'a elt -> string -> unit - val paddingBottom: 'a elt -> string -> unit - val paddingBottomPx: 'a elt -> int -> unit - val paddingLeft: 'a elt -> string -> unit - val paddingLeftPx: 'a elt -> int -> unit - val paddingRight: 'a elt -> string -> unit - val paddingRightPx: 'a elt -> int -> unit - val paddingTop: 'a elt -> string -> unit - val paddingTopPx: 'a elt -> int -> unit - val pageBreakAfter: 'a elt -> string -> unit - val pageBreakBefore: 'a elt -> string -> unit - val position: 'a elt -> string -> unit - val right: 'a elt -> string -> unit - val rightPx: 'a elt -> int -> unit - val tableLayout: 'a elt -> string -> unit - val textAlign: 'a elt -> string -> unit - val textDecoration: 'a elt -> string -> unit - val textIndent: 'a elt -> string -> unit - val textTransform: 'a elt -> string -> unit - val top: 'a elt -> string -> unit - val topPx: 'a elt -> int -> unit - val verticalAlign: 'a elt -> string -> unit - val visibility: 'a elt -> string -> unit - val whiteSpace: 'a elt -> string -> unit - val width: 'a elt -> string -> unit - val widthPx: 'a elt -> int -> unit - val wordSpacing: 'a elt -> string -> unit - val zIndex: 'a elt -> string -> unit - end + val background : 'a elt -> string -> unit + + val backgroundAttachment : 'a elt -> string -> unit + + val backgroundColor : 'a elt -> string -> unit + + val backgroundImage : 'a elt -> string -> unit + + val backgroundPosition : 'a elt -> string -> unit + + val backgroundRepeat : 'a elt -> string -> unit + + val border : 'a elt -> string -> unit + + val borderBottom : 'a elt -> string -> unit + + val borderBottomColor : 'a elt -> string -> unit + + val borderBottomStyle : 'a elt -> string -> unit + + val borderBottomWidth : 'a elt -> string -> unit + + val borderBottomWidthPx : 'a elt -> int -> unit + + val borderCollapse : 'a elt -> string -> unit + + val borderColor : 'a elt -> string -> unit + + val borderLeft : 'a elt -> string -> unit + + val borderLeftColor : 'a elt -> string -> unit + + val borderLeftStyle : 'a elt -> string -> unit + + val borderLeftWidth : 'a elt -> string -> unit + + val borderLeftWidthPx : 'a elt -> int -> unit + + val borderRight : 'a elt -> string -> unit + + val borderRightColor : 'a elt -> string -> unit + + val borderRightStyle : 'a elt -> string -> unit + + val borderRightWidth : 'a elt -> string -> unit + + val borderRightWidthPx : 'a elt -> int -> unit + + val borderSpacing : 'a elt -> string -> unit + + val borderStyle : 'a elt -> string -> unit + + val borderTop : 'a elt -> string -> unit + + val borderTopColor : 'a elt -> string -> unit + + val borderTopStyle : 'a elt -> string -> unit + + val borderTopWidth : 'a elt -> string -> unit + + val borderTopWidthPx : 'a elt -> int -> unit + + val borderWidth : 'a elt -> string -> unit + + val bottom : 'a elt -> string -> unit + + val bottomPx : 'a elt -> int -> unit + + val captionSide : 'a elt -> string -> unit + + val clear : 'a elt -> string -> unit + + val clip : 'a elt -> string -> unit + + val color : 'a elt -> string -> unit + + val content : 'a elt -> string -> unit + + val counterIncrement : 'a elt -> string -> unit + + val counterReset : 'a elt -> string -> unit + + val cssFloat : 'a elt -> string -> unit + + val cssText : 'a elt -> string -> unit + + val cursor : 'a elt -> string -> unit + + val direction : 'a elt -> string -> unit + + val display : 'a elt -> string -> unit + + val emptyCells : 'a elt -> string -> unit + + val font : 'a elt -> string -> unit + + val fontFamily : 'a elt -> string -> unit + + val fontSize : 'a elt -> string -> unit + + val fontStyle : 'a elt -> string -> unit + + val fontVariant : 'a elt -> string -> unit + + val fontWeight : 'a elt -> string -> unit + + val height : 'a elt -> string -> unit + val heightPx : 'a elt -> int -> unit + + val left : 'a elt -> string -> unit + + val leftPx : 'a elt -> int -> unit + + val letterSpacing : 'a elt -> string -> unit + + val lineHeight : 'a elt -> string -> unit + + val listStyle : 'a elt -> string -> unit + + val listStyleImage : 'a elt -> string -> unit + + val listStylePosition : 'a elt -> string -> unit + + val listStyleType : 'a elt -> string -> unit + + val margin : 'a elt -> string -> unit + + val marginBottom : 'a elt -> string -> unit + + val marginBottomPx : 'a elt -> int -> unit + + val marginLeft : 'a elt -> string -> unit + + val marginLeftPx : 'a elt -> int -> unit + + val marginRight : 'a elt -> string -> unit + + val marginRightPx : 'a elt -> int -> unit + + val marginTop : 'a elt -> string -> unit + + val marginTopPx : 'a elt -> int -> unit + + val maxHeight : 'a elt -> string -> unit + + val maxHeightPx : 'a elt -> int -> unit + + val maxWidth : 'a elt -> string -> unit + + val maxWidthPx : 'a elt -> int -> unit + + val minHeight : 'a elt -> string -> unit + + val minHeightPx : 'a elt -> int -> unit + + val minWidth : 'a elt -> string -> unit + + val minWidthPx : 'a elt -> int -> unit + + val opacity : 'a elt -> string option -> unit + + val outline : 'a elt -> string -> unit + + val outlineColor : 'a elt -> string -> unit + + val outlineOffset : 'a elt -> string -> unit + + val outlineStyle : 'a elt -> string -> unit + + val outlineWidth : 'a elt -> string -> unit + + val overflow : 'a elt -> string -> unit + + val overflowX : 'a elt -> string -> unit + + val overflowY : 'a elt -> string -> unit + + val padding : 'a elt -> string -> unit + + val paddingBottom : 'a elt -> string -> unit + + val paddingBottomPx : 'a elt -> int -> unit + + val paddingLeft : 'a elt -> string -> unit + + val paddingLeftPx : 'a elt -> int -> unit + + val paddingRight : 'a elt -> string -> unit + + val paddingRightPx : 'a elt -> int -> unit + + val paddingTop : 'a elt -> string -> unit + + val paddingTopPx : 'a elt -> int -> unit + + val pageBreakAfter : 'a elt -> string -> unit + + val pageBreakBefore : 'a elt -> string -> unit + + val position : 'a elt -> string -> unit + + val right : 'a elt -> string -> unit + + val rightPx : 'a elt -> int -> unit + + val tableLayout : 'a elt -> string -> unit + + val textAlign : 'a elt -> string -> unit + + val textDecoration : 'a elt -> string -> unit + + val textIndent : 'a elt -> string -> unit + + val textTransform : 'a elt -> string -> unit + + val top : 'a elt -> string -> unit + + val topPx : 'a elt -> int -> unit + + val verticalAlign : 'a elt -> string -> unit + + val visibility : 'a elt -> string -> unit + + val whiteSpace : 'a elt -> string -> unit + + val width : 'a elt -> string -> unit + + val widthPx : 'a elt -> int -> unit + + val wordSpacing : 'a elt -> string -> unit + + val zIndex : 'a elt -> string -> unit + end end val window : Dom_html.window Js.t -val window_open: ?features:string -> string -> string -> Dom_html.window Js.t Js.opt + +val window_open : + ?features:string -> string -> string -> Dom_html.window Js.t Js.opt module Window : sig - val close: Dom_html.window Js.t -> unit - val head: Dom_html.window Js.t -> [`Head] Tyxml_js.Html5.elt - val body: Dom_html.window Js.t -> [`Body] Tyxml_js.Html5.elt - val onresize: - ?win:Dom_html.window Js.t -> - (Dom_html.event Js.t -> bool) -> unit - val onunload: - ?win:Dom_html.window Js.t -> - (Dom_html.event Js.t -> bool) -> unit - val onhashchange: - ?win:Dom_html.window Js.t -> - (Dom_html.hashChangeEvent Js.t -> bool) -> unit - val prompt: ?win:Dom_html.window Js.t -> ?value:string -> string -> string + val close : Dom_html.window Js.t -> unit + + val head : Dom_html.window Js.t -> [`Head] Tyxml_js.Html5.elt + + val body : Dom_html.window Js.t -> [`Body] Tyxml_js.Html5.elt + + val onresize : + ?win:Dom_html.window Js.t -> (Dom_html.event Js.t -> bool) -> unit + + val onunload : + ?win:Dom_html.window Js.t -> (Dom_html.event Js.t -> bool) -> unit + + val onhashchange : + ?win:Dom_html.window Js.t + -> (Dom_html.hashChangeEvent Js.t -> bool) + -> unit + + val prompt : ?win:Dom_html.window Js.t -> ?value:string -> string -> string end val hide : 'a Tyxml_js.Html5.elt -> unit + val show : 'a Tyxml_js.Html5.elt -> unit module Document : sig - val uri: unit -> string + val uri : unit -> string end -val parse_fragment: unit -> (string * string) list -val set_fragment: (string * string) list -> unit +val parse_fragment : unit -> (string * string) list -module MakeLocal(V: sig type t val name: string end) : sig - val get: unit -> V.t option - val set: V.t -> unit +val set_fragment : (string * string) list -> unit + +module MakeLocal (V : sig + type t + + val name : string +end) : sig + val get : unit -> V.t option + + val set : V.t -> unit end +val js_code_url : string -> string (** Returns an URL that can be passed to [Worker.create] from a string containing the code *) -val js_code_url: string -> string - (* (\** Wrapper for [Worker.create] that uses JS code as a string instead of a URL*\) * val worker_with_code: string -> ('a, 'b) Worker.worker Js.t * diff --git a/src/utils/learnocaml_partition_create.ml b/src/utils/learnocaml_partition_create.ml index 8358c4922..f9c41b5b9 100644 --- a/src/utils/learnocaml_partition_create.ml +++ b/src/utils/learnocaml_partition_create.ml @@ -8,76 +8,71 @@ open Learnocaml_data open Learnocaml_data.Partition - open Learnocaml_report - open Lwt.Infix - open Asak.Monad_error -module IntMap = Map.Make(struct type t = int let compare = compare end) +module IntMap = Map.Make (struct + type t = int + + let compare = compare +end) (* Return a list of all saves with their definition of the function *) let get_all_saves exo_name prelude = - Learnocaml_store.Student.Index.get () >>= - Lwt_list.fold_left_s (* filter_map_rev *) - (fun acc t -> - let open ErrS in - let t = t.Student.token in - Learnocaml_store.Save.get t >|= fun save -> - either (fun x -> x :: acc) (fun _ -> acc) @@ run @@ - begin - err_of_option "" save - >>= fun x -> - err_of_option "" (SMap.find_opt exo_name Save.(x.all_exercise_states)) - >>= fun x -> - ret (t,x, (prelude ^ "\n" ^ Answer.(x.solution))) - end - ) [] + Learnocaml_store.Student.Index.get () + >>= Lwt_list.fold_left_s + (* filter_map_rev *) + (fun acc t -> + let open ErrS in + let t = t.Student.token in + Learnocaml_store.Save.get t + >|= fun save -> + either (fun x -> x :: acc) (fun _ -> acc) + @@ run + @@ ( err_of_option "" save + >>= fun x -> + err_of_option "" + (SMap.find_opt exo_name Save.(x.all_exercise_states)) + >>= fun x -> ret (t, x, prelude ^ "\n" ^ Answer.(x.solution)) ) ) + [] (* Return a tuple where - The first element contains answer without a grade - The second the others *) let partition_was_graded = - let aux (nonlst,acc) (a,x,b) = + let aux (nonlst, acc) (a, x, b) = match Answer.(x.report) with - | None -> a::nonlst,acc - | Some g -> nonlst,(a,g,b)::acc + | None -> (a :: nonlst, acc) + | Some g -> (nonlst, (a, g, b) :: acc) in List.fold_left aux ([], []) let partition_by_grade funname = let rec get_relative_section = function | [] -> [] - | (Message _)::xs -> get_relative_section xs - | (Section (t,res))::xs | (SectionMin (t,res, _))::xs -> - match t with - | Text _::Code fn::_ -> - if fn = funname - then res - else get_relative_section xs - | _ -> get_relative_section xs + | Message _ :: xs -> get_relative_section xs + | Section (t, res) :: xs | SectionMin (t, res, _) :: xs -> ( + match t with + | Text _ :: Code fn :: _ -> + if fn = funname then res else get_relative_section xs + | _ -> get_relative_section xs ) in let rec get_grade xs = - let aux acc = - function - | Section (_,s) -> get_grade s + let aux acc = function + | Section (_, s) -> get_grade s | SectionMin (_, s, min) -> max (get_grade s) min - | Message (_,s) -> - match s with - | Success i -> acc + i - | _ -> acc + | Message (_, s) -> ( match s with Success i -> acc + i | _ -> acc ) in List.fold_left aux 0 xs in - let aux acc ((_,x,_) as e) = + let aux acc ((_, x, _) as e) = let sec = get_relative_section x in let g = get_grade sec in let lst = - match IntMap.find_opt g acc with - | None -> [e] - | Some xs -> e::xs - in IntMap.add g lst acc + match IntMap.find_opt g acc with None -> [e] | Some xs -> e :: xs + in + IntMap.add g lst acc in List.fold_left aux IntMap.empty @@ -85,10 +80,11 @@ let asak_partition prof fun_name sol by_grade = let open Asak in IntMap.fold (fun i lst (bad_type, res) -> - let lst = List.map (fun (a,_,b) -> (a,b)) lst in + let lst = List.map (fun (a, _, b) -> (a, b)) lst in let ans = Partition.create prof fun_name sol lst in - (ans.Partition.bad_type @ bad_type, (i,ans.Partition.clusters) :: res) - ) by_grade ([],[]) + (ans.Partition.bad_type @ bad_type, (i, ans.Partition.clusters) :: res) + ) + by_grade ([], []) let partition exo_name fun_name prof = Learnocaml_store.Exercise.get exo_name @@ -100,7 +96,9 @@ let partition exo_name fun_name prof = let solution = prelude ^ "\n" ^ solution in get_all_saves exo_name prelude >|= fun saves -> - let not_graded,lst = partition_was_graded saves in + let not_graded, lst = partition_was_graded saves in let by_grade = partition_by_grade fun_name lst in - let bad_type,partition_by_grade = asak_partition prof fun_name solution by_grade in + let bad_type, partition_by_grade = + asak_partition prof fun_name solution by_grade + in {not_graded; bad_type; partition_by_grade} diff --git a/src/utils/learnocaml_partition_create.mli b/src/utils/learnocaml_partition_create.mli index cae9ca366..305363520 100644 --- a/src/utils/learnocaml_partition_create.mli +++ b/src/utils/learnocaml_partition_create.mli @@ -8,7 +8,8 @@ open Learnocaml_data -val partition : string (* Exercise name *) - -> string (* function name *) - -> int (* percent of subtrees to keep *) - -> Partition.t Lwt.t +val partition : + string (* Exercise name *) + -> string (* function name *) + -> int (* percent of subtrees to keep *) + -> Partition.t Lwt.t diff --git a/src/utils/learnocaml_xor.ml b/src/utils/learnocaml_xor.ml index b76861a57..0bee5aa43 100644 --- a/src/utils/learnocaml_xor.ml +++ b/src/utils/learnocaml_xor.ml @@ -14,32 +14,30 @@ let () = Bytes.set alphabet 26 '+'; Bytes.set alphabet 37 '/'; for i = 0 to 25 do - Bytes.set alphabet i (Char.chr @@ 65 + i); - Bytes.set alphabet (i+38) (Char.chr @@ 97 + 25 - i) + Bytes.set alphabet i (Char.chr @@ (65 + i)); + Bytes.set alphabet (i + 38) (Char.chr @@ (97 + 25 - i)) done; for i = 0 to 9 do - Bytes.set alphabet (i+27) (Char.chr @@ 48 + i) + Bytes.set alphabet (i + 27) (Char.chr @@ (48 + i)) done let xor_key = - "Caml1999I0150\153\200\232\027\154a\029u@\251\127SX\141\140\157\ - \219\195\000\228\020\180_CR\202\130\129\127\2491\130\011\183\ - \158b\022\"qB0\166+\169\212_\205\164 D\210Qn\181o\225\147q\156\ - \028u6\248b\177\002\164`\187\250\221\240o6\156\240\020\027\243o\ - \017h\218\208\168\164f\161+5\137\132ml\169\235\174\212\029" + "Caml1999I0150\153\200\232\027\154a\029u@\251\127SX\141\140\157\219\195\000\228\020\180_CR\202\130\129\127\2491\130\011\183\158b\022\"qB0\166+\169\212_\205\164 \ + D\210Qn\181o\225\147q\156\028u6\248b\177\002\164`\187\250\221\240o6\156\240\020\027\243o\017h\218\208\168\164f\161+5\137\132ml\169\235\174\212\029" let xor ?prefix str = let xor_key = - match prefix with - | None -> xor_key - | Some prefix -> prefix ^ xor_key in + match prefix with None -> xor_key | Some prefix -> prefix ^ xor_key + in let str' = Bytes.create (String.length str) in for i = 0 to String.length str - 1 do - let c = Char.code xor_key.[i mod (String.length xor_key)] in - Bytes.set str' (i) (Char.chr (c lxor (Char.code (String.get str i)))) + let c = Char.code xor_key.[i mod String.length xor_key] in + Bytes.set str' i (Char.chr (c lxor Char.code str.[i])) done; Bytes.to_string str' let alphabet = Bytes.to_string alphabet + let decode ?prefix str = xor ?prefix @@ B64.decode ~alphabet str + let encode ?prefix str = B64.encode ~alphabet @@ xor ?prefix str diff --git a/src/utils/learnocaml_xor.mli b/src/utils/learnocaml_xor.mli index 57606cfe7..89eb6a99b 100644 --- a/src/utils/learnocaml_xor.mli +++ b/src/utils/learnocaml_xor.mli @@ -8,5 +8,6 @@ (* This is trivial and dummy "encryption" for the tests and the solutions. *) -val encode: ?prefix:string -> string -> string -val decode: ?prefix:string -> string -> string +val encode : ?prefix:string -> string -> string + +val decode : ?prefix:string -> string -> string diff --git a/src/utils/lwt_request.ml b/src/utils/lwt_request.ml index 1401e5acd..d794e4dda 100644 --- a/src/utils/lwt_request.ml +++ b/src/utils/lwt_request.ml @@ -11,19 +11,23 @@ open Js_of_ocaml exception Request_failed of (int * string) let url_encode_list l = - String.concat "&" (List.map (fun (name, arg) -> - Printf.sprintf "%s=%s" name (Url.urlencode arg)) l) + String.concat "&" + (List.map + (fun (name, arg) -> Printf.sprintf "%s=%s" name (Url.urlencode arg)) + l) -let get ?(headers=[]) ~url ~args = - let (res, w) = Lwt.task () in +let get ?(headers = []) ~url ~args = + let res, w = Lwt.task () in let req = XmlHttpRequest.create () in - let url = match args with - | [] -> url - | _ -> url ^ "?" ^ (url_encode_list args) in - req##(_open (Js.string "GET") (Js.string url) (Js._true)); - req##(setRequestHeader (Js.string "Content-type") - (Js.string "application/x-www-form-urlencoded")); - List.iter (fun (n, v) -> req##(setRequestHeader (Js.string n) (Js.string v))) + let url = + match args with [] -> url | _ -> url ^ "?" ^ url_encode_list args + in + req ## (_open (Js.string "GET") (Js.string url) Js._true); + req + ## (setRequestHeader (Js.string "Content-type") + (Js.string "application/x-www-form-urlencoded")); + List.iter + (fun (n, v) -> req ## (setRequestHeader (Js.string n) (Js.string v))) headers; let callback () = match req##.status with @@ -31,38 +35,44 @@ let get ?(headers=[]) ~url ~args = | 204 -> Lwt.wakeup w "" | code (* including 0 *) -> Lwt.wakeup_exn w - (Request_failed (code, Js.to_string req##.responseText)) in - req##.onreadystatechange := Js.wrap_callback - (fun _ -> (match req##.readyState with - XmlHttpRequest.DONE -> callback () - | _ -> ())); - req##(send (Js.null)); + (Request_failed (code, Js.to_string req##.responseText)) + in + req##.onreadystatechange := + Js.wrap_callback (fun _ -> + match req##.readyState with + | XmlHttpRequest.DONE -> callback () + | _ -> () ); + req ## (send Js.null); Lwt.on_cancel res (fun () -> req##abort); res -let post ?(headers=[]) ?(get_args=[]) ~url ~body = - let (res, w) = Lwt.task () in +let post ?(headers = []) ?(get_args = []) ~url ~body = + let res, w = Lwt.task () in let req = XmlHttpRequest.create () in - let url = match get_args with - | [] -> url - | _ -> url ^ "?" ^ (url_encode_list get_args) in - req##(_open (Js.string "POST") (Js.string url) (Js._true)); - req##(setRequestHeader (Js.string "Content-type") - (Js.string "application/x-www-form-urlencoded")); - List.iter (fun (n, v) -> req##(setRequestHeader (Js.string n) (Js.string v))) + let url = + match get_args with [] -> url | _ -> url ^ "?" ^ url_encode_list get_args + in + req ## (_open (Js.string "POST") (Js.string url) Js._true); + req + ## (setRequestHeader (Js.string "Content-type") + (Js.string "application/x-www-form-urlencoded")); + List.iter + (fun (n, v) -> req ## (setRequestHeader (Js.string n) (Js.string v))) headers; let callback () = match req##.status with | 200 -> Lwt.wakeup w (Js.to_string req##.responseText) | 204 -> Lwt.wakeup w "" - | code (* including 0 *) -> Lwt.wakeup_exn w - (Request_failed (code, Js.to_string req##.responseText)) + | code (* including 0 *) -> + Lwt.wakeup_exn w + (Request_failed (code, Js.to_string req##.responseText)) in - req##.onreadystatechange := Js.wrap_callback - (fun _ -> (match req##.readyState with - XmlHttpRequest.DONE -> callback () - | _ -> ())); + req##.onreadystatechange := + Js.wrap_callback (fun _ -> + match req##.readyState with + | XmlHttpRequest.DONE -> callback () + | _ -> () ); let body = Js.Opt.map (Js.Opt.option body) Js.string in - req##(send body); + req ## (send body); Lwt.on_cancel res (fun () -> req##abort); res diff --git a/src/utils/lwt_request.mli b/src/utils/lwt_request.mli index 67a7012be..2c113fa56 100644 --- a/src/utils/lwt_request.mli +++ b/src/utils/lwt_request.mli @@ -1,18 +1,23 @@ -(* This file is part of Learn-OCaml. +exception + (* This file is part of Learn-OCaml. * * Copyright (C) 2019 OCaml Software Foundation. * Copyright (C) 2016-2018 OCamlPro. * * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) + Request_failed of + (int * string) -exception Request_failed of (int * string) +val post : + ?headers:(string * string) list + -> ?get_args:(string * string) list + -> url:string + -> body:string option + -> string Lwt.t -val post: - ?headers:(string * string) list -> - ?get_args:(string * string) list -> - url:string -> body:string option -> string Lwt.t - -val get: - ?headers:(string * string) list -> - url:string -> args:(string * string) list -> string Lwt.t +val get : + ?headers:(string * string) list + -> url:string + -> args:(string * string) list + -> string Lwt.t diff --git a/src/utils/lwt_utils.ml b/src/utils/lwt_utils.ml index 0fe27a22c..1f0b0b980 100644 --- a/src/utils/lwt_utils.ml +++ b/src/utils/lwt_utils.ml @@ -8,50 +8,53 @@ open Lwt.Infix -let rec mkdir_p ?(perm=0o755) dir = - Lwt_unix.file_exists dir >>= function +let rec mkdir_p ?(perm = 0o755) dir = + Lwt_unix.file_exists dir + >>= function | true -> - if Sys.is_directory dir then - Lwt.return () + if Sys.is_directory dir then Lwt.return () else Lwt.fail_with (Printf.sprintf "Can't create dir: file %s is in the way" dir) | false -> - mkdir_p (Filename.dirname dir) >>= fun () -> - Lwt_unix.mkdir dir perm + mkdir_p (Filename.dirname dir) >>= fun () -> Lwt_unix.mkdir dir perm let copy_file src dst = - Lwt.catch (fun () -> - let cmd = [|"cp";src;dst|]in - Lwt_process.exec ("", cmd) >>= fun r -> + Lwt.catch + (fun () -> + let cmd = [|"cp"; src; dst|] in + Lwt_process.exec ("", cmd) + >>= fun r -> if r <> Unix.WEXITED 0 then Lwt.fail_with "copy_file" - else Lwt.return_unit) + else Lwt.return_unit ) (function - | Sys_error _ | Unix.Unix_error _ -> Lwt.fail_with "copy_file" - | e -> raise e) + | Sys_error _ | Unix.Unix_error _ -> Lwt.fail_with "copy_file" + | e -> raise e) let copy_tree src dst = let files = Sys.readdir src in if Array.length files = 0 then Lwt.return_unit else - Lwt.catch (fun () -> - mkdir_p dst >>= fun () -> + Lwt.catch + (fun () -> + mkdir_p dst + >>= fun () -> let cmd = Array.concat - [[|"cp"; "-PR"|]; - Array.map (Filename.concat src) files; - [|dst|]] + [[|"cp"; "-PR"|]; Array.map (Filename.concat src) files; [|dst|]] in - Lwt_process.exec ("", cmd) >>= fun r -> + Lwt_process.exec ("", cmd) + >>= fun r -> if r <> Unix.WEXITED 0 then Lwt.fail_with "copy_tree" - else Lwt.return_unit) + else Lwt.return_unit ) (function | Sys_error _ | Unix.Unix_error _ -> Lwt.fail_with "copy_tree" | e -> raise e) -type 'a with_lock = { with_lock: 'b. 'a -> (unit -> 'b Lwt.t) -> 'b Lwt.t } +type 'a with_lock = {with_lock : 'b. 'a -> (unit -> 'b Lwt.t) -> 'b Lwt.t} -let gen_mutex_table: type t. unit -> t with_lock = fun () -> +let gen_mutex_table : type t. unit -> t with_lock = + fun () -> let table = Hashtbl.create 223 in let get_mutex key = try Hashtbl.find table key with Not_found -> @@ -61,11 +64,13 @@ let gen_mutex_table: type t. unit -> t with_lock = fun () -> in let with_lock key f = let mutex = get_mutex key in - Lwt_mutex.with_lock mutex @@ fun () -> - Lwt.finalize f @@ fun () -> + Lwt_mutex.with_lock mutex + @@ fun () -> + Lwt.finalize f + @@ fun () -> if Lwt_mutex.is_empty mutex then (* we still hold the mutex, nobody else is waiting: drop it *) Hashtbl.remove table key; Lwt.return_unit in - { with_lock } + {with_lock} diff --git a/src/utils/lwt_utils.mli b/src/utils/lwt_utils.mli index 11eecd091..ff86d20ab 100644 --- a/src/utils/lwt_utils.mli +++ b/src/utils/lwt_utils.mli @@ -6,17 +6,17 @@ * Learn-OCaml is distributed under the terms of the MIT license. See the * included LICENSE file for details. *) -val mkdir_p: ?perm:int -> string -> unit Lwt.t +val mkdir_p : ?perm:int -> string -> unit Lwt.t +val copy_file : string -> string -> unit Lwt.t (** [copy_tree src dst] copies the file [src] into file [dst] *) -val copy_file: string -> string -> unit Lwt.t +val copy_tree : string -> string -> unit Lwt.t (** [copy_tree src dst] copies the contents of directory [src] into directory [dst] *) -val copy_tree: string -> string -> unit Lwt.t -type 'a with_lock = { with_lock: 'b. 'a -> (unit -> 'b Lwt.t) -> 'b Lwt.t } +type 'a with_lock = {with_lock : 'b. 'a -> (unit -> 'b Lwt.t) -> 'b Lwt.t} +val gen_mutex_table : unit -> 'a with_lock (** Creates a mutex hashtable when applied to [()], and returns a `with_mutex` function that can lock with a given mutex identifier *) -val gen_mutex_table: unit -> 'a with_lock diff --git a/src/utils/ocplib_i18n.ml b/src/utils/ocplib_i18n.ml index dbc2634c0..84276e344 100644 --- a/src/utils/ocplib_i18n.ml +++ b/src/utils/ocplib_i18n.ml @@ -19,8 +19,6 @@ let s_ arr = arr.(!lang_id) let set_lang lang = let lang = List.hd (String.split_on_char '-' lang) in let rec aux i = - if i <= 0 then 0 - else if known_lang_ids.(i) = lang then i - else aux (i-1) + if i <= 0 then 0 else if known_lang_ids.(i) = lang then i else aux (i - 1) in lang_id := aux (Array.length known_lang_ids - 1) diff --git a/src/utils/ocplib_i18n.mli b/src/utils/ocplib_i18n.mli index 73b643207..465b95a42 100644 --- a/src/utils/ocplib_i18n.mli +++ b/src/utils/ocplib_i18n.mli @@ -10,13 +10,13 @@ * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. *) -(** Lists all the supported languages *) val known_lang_ids : string array +(** Lists all the supported languages *) +val s_ : 'a array -> 'a (** Gets the right index from the given array for the currently selected language (should be used by the preprocessor only) *) -val s_ : 'a array -> 'a +val set_lang : string -> unit (** Select the currently active language (two-letter code, possibly with country suffix e.g. [fr-FR]. *) -val set_lang : string -> unit diff --git a/src/utils/ppx_ocplib_i18n.ml b/src/utils/ppx_ocplib_i18n.ml index 6a235b7ce..349503d24 100644 --- a/src/utils/ppx_ocplib_i18n.ml +++ b/src/utils/ppx_ocplib_i18n.ml @@ -12,7 +12,6 @@ open Migrate_parsetree open OCaml_405.Ast - open Ast_mapper open Ast_helper open Asttypes @@ -24,158 +23,169 @@ let read_translations f = let t = Hashtbl.create 427 in let id = ref None in let in_id = ref false in - try while true do + try + while true do let l = String.trim (input_line ic) in if String.length l = 0 then id := None else if l.[0] = '#' then () else - try Scanf.sscanf l "msgid %S" (fun s -> in_id := true; id := Some s) - with Scanf.Scan_failure _ -> - try Scanf.sscanf l "msgstr %S" (fun s -> - match !id with - | None -> - Printf.ksprintf failwith - "Missing 'msgid' before 'msgstr' in %s" f - | Some id -> - if Hashtbl.mem t id then - Printf.ksprintf failwith - "Duplicate definition for msgid '%s' in %s" id f; - in_id := false; - Hashtbl.add t id s) - with Scanf.Scan_failure _ -> - try Scanf.sscanf l "%S" (fun s -> - match !id with - | None -> - Printf.ksprintf failwith - "Missing 'msgid' before string in %s" f - | Some sid -> - if !in_id then id := Some (sid ^ s) - else - let s0 = Hashtbl.find t sid in - Hashtbl.replace t sid (s0 ^ s)) - with Scanf.Scan_failure _ -> - Printf.ksprintf failwith "Error in translation file %s at %S" f l - done; assert false - with End_of_file -> - close_in ic; - t + try + Scanf.sscanf l "msgid %S" (fun s -> + in_id := true; + id := Some s ) + with Scanf.Scan_failure _ -> ( + try + Scanf.sscanf l "msgstr %S" (fun s -> + match !id with + | None -> + Printf.ksprintf failwith + "Missing 'msgid' before 'msgstr' in %s" f + | Some id -> + if Hashtbl.mem t id then + Printf.ksprintf failwith + "Duplicate definition for msgid '%s' in %s" id f; + in_id := false; + Hashtbl.add t id s ) + with Scanf.Scan_failure _ -> ( + try + Scanf.sscanf l "%S" (fun s -> + match !id with + | None -> + Printf.ksprintf failwith + "Missing 'msgid' before string in %s" f + | Some sid -> + if !in_id then id := Some (sid ^ s) + else + let s0 = Hashtbl.find t sid in + Hashtbl.replace t sid (s0 ^ s) ) + with Scanf.Scan_failure _ -> + Printf.ksprintf failwith "Error in translation file %s at %S" f l ) ) + done; + assert false + with End_of_file -> close_in ic; t let translations_dir = "../../translations" + let transl_file_suffix = ".po" + let dump_pot_file = Sys.getenv_opt "DUMP_POT" <> None let all_ids = Hashtbl.create 23 let htbl_update ht key f create = - try Hashtbl.replace ht key (f (Hashtbl.find ht key)) - with Not_found -> Hashtbl.add ht key (f (create ())) + try Hashtbl.replace ht key (f (Hashtbl.find ht key)) with Not_found -> + Hashtbl.add ht key (f (create ())) let find_translation lang ht ~loc s = if dump_pot_file then htbl_update all_ids lang (fun t -> Hashtbl.add t s loc; t) (fun () -> Hashtbl.create 143); - try Hashtbl.find ht s - with Not_found -> - Location.print_warning loc !Location.formatter_for_warnings @@ - Warnings.Preprocessor - (Printf.sprintf "%s translation not found for %S" lang s); + try Hashtbl.find ht s with Not_found -> + Location.print_warning loc !Location.formatter_for_warnings + @@ Warnings.Preprocessor + (Printf.sprintf "%s translation not found for %S" lang s); s let dump_pot () = - Hashtbl.iter (fun lang strs -> + Hashtbl.iter + (fun lang strs -> let file = Filename.concat translations_dir (lang ^ ".pot") in let oc = open_out_gen [Open_append; Open_creat] 0o644 file in let misses = - Hashtbl.fold (fun str loc acc -> match acc with - | (locs, s)::acc when s = str -> (loc::locs, str)::acc - | acc -> ([loc], str)::acc) + Hashtbl.fold + (fun str loc acc -> + match acc with + | (locs, s) :: acc when s = str -> (loc :: locs, str) :: acc + | acc -> ([loc], str) :: acc ) strs [] in let misses = List.sort compare - (List.map (fun (locs, s) -> List.sort compare locs, s) misses) + (List.map (fun (locs, s) -> (List.sort compare locs, s)) misses) in let fmt = Format.formatter_of_out_channel oc in - List.iter (fun (locs, s) -> + List.iter + (fun (locs, s) -> Format.pp_print_string fmt "#:"; - List.iter (fun l -> + List.iter + (fun l -> Format.pp_print_char fmt ' '; - Location.print_compact fmt l) + Location.print_compact fmt l ) locs; Format.fprintf fmt "\nmsgid %S\n" s; - Format.fprintf fmt "msgstr \"\"\n\n"; - ) misses; - close_out oc - ) + Format.fprintf fmt "msgstr \"\"\n\n" ) + misses; + close_out oc ) all_ids let translations = (* default language should be first *) - ("en", (fun ~loc:_ s -> s)) :: - let langs = Hashtbl.create 7 in - Array.iter (fun f -> - if Filename.check_suffix f transl_file_suffix then - Hashtbl.add langs - (Filename.chop_suffix f transl_file_suffix) - (read_translations (Filename.concat translations_dir f)) - ) - (Sys.readdir translations_dir); - List.sort compare @@ - Hashtbl.fold (fun lang h acc -> - (lang, find_translation lang h) :: acc) langs [] + ("en", fun ~loc:_ s -> s) + :: + (let langs = Hashtbl.create 7 in + Array.iter + (fun f -> + if Filename.check_suffix f transl_file_suffix then + Hashtbl.add langs + (Filename.chop_suffix f transl_file_suffix) + (read_translations (Filename.concat translations_dir f)) ) + (Sys.readdir translations_dir); + List.sort compare + @@ Hashtbl.fold + (fun lang h acc -> (lang, find_translation lang h) :: acc) + langs []) let get_lang_expr ~loc transl_expr = Exp.apply ~loc - (Exp.ident { txt = Ldot (Lident "Ocplib_i18n", "s_"); loc }) - [Nolabel, transl_expr] + (Exp.ident {txt = Ldot (Lident "Ocplib_i18n", "s_"); loc}) + [(Nolabel, transl_expr)] let transl_mapper _config _cookies = { default_mapper with - expr = fun mapper expr -> - match expr with - | { pexp_desc = - Pexp_extension ({ txt = "lang_ids_array"; loc }, _); - _ } -> - Exp.array ~loc - (List.map (fun (lang, _) -> - Exp.constant ~loc (Pconst_string (lang,None))) - translations) - | { pexp_desc = - Pexp_extension ({ txt = ("i"|"if" as tag); loc }, pstr); - _ } -> - (match pstr with - | PStr [{ - pstr_desc = - Pstr_eval ({ - pexp_loc = loc; - pexp_desc = Pexp_constant (Pconst_string (s, _)); - _ - }, _); - _ - }] -> - let is_format = tag = "if" in - let translations = - List.map (fun (_lang, f) -> f ~loc s) translations - in - let translations_expr = - Exp.array ~loc - (List.map (fun s -> - let e = Exp.constant ~loc (Pconst_string (s,None)) in - if is_format then - Exp.apply ~loc - (Exp.ident { txt = Lident "format_of_string"; loc }) - [Nolabel, e] - else e) - translations) - in - get_lang_expr ~loc translations_expr - | _ -> - raise (Location.Error ( - Location.error ~loc "[%i] requires a constant string, e.g. [%i \"text\"]"))) - | x -> default_mapper.expr mapper x; - } + expr = + (fun mapper expr -> + match expr with + | {pexp_desc = Pexp_extension ({txt = "lang_ids_array"; loc}, _); _} -> + Exp.array ~loc + (List.map + (fun (lang, _) -> + Exp.constant ~loc (Pconst_string (lang, None)) ) + translations) + | { pexp_desc = Pexp_extension ({txt = ("i" | "if") as tag; loc}, pstr); _ + } -> ( + match pstr with + | PStr + [ { pstr_desc = + Pstr_eval + ( { pexp_loc = loc + ; pexp_desc = Pexp_constant (Pconst_string (s, _)); _ + } + , _ ); _ } ] -> + let is_format = tag = "if" in + let translations = + List.map (fun (_lang, f) -> f ~loc s) translations + in + let translations_expr = + Exp.array ~loc + (List.map + (fun s -> + let e = Exp.constant ~loc (Pconst_string (s, None)) in + if is_format then + Exp.apply ~loc + (Exp.ident {txt = Lident "format_of_string"; loc}) + [(Nolabel, e)] + else e ) + translations) + in + get_lang_expr ~loc translations_expr + | _ -> + raise + (Location.Error + (Location.error ~loc + "[%i] requires a constant string, e.g. [%i \"text\"]")) ) + | x -> default_mapper.expr mapper x ) } let () = Driver.register ~name:"i18n" (module OCaml_405) transl_mapper -let () = - if Sys.getenv_opt "DUMP_POT" <> None then at_exit dump_pot + +let () = if Sys.getenv_opt "DUMP_POT" <> None then at_exit dump_pot diff --git a/src/utils/sha.mli b/src/utils/sha.mli index da2e7cd2b..890cbc715 100644 --- a/src/utils/sha.mli +++ b/src/utils/sha.mli @@ -1 +1 @@ -val sha512: string -> string +val sha512 : string -> string