diff --git a/learn-ocaml.opam b/learn-ocaml.opam index a89a43dea..e088d87dd 100644 --- a/learn-ocaml.opam +++ b/learn-ocaml.opam @@ -45,7 +45,7 @@ depends: [ "ocp-indent-nlfork" "ocp-ocamlres" {>= "0.4"} "ocplib-json-typed" {= "0.6"} - "odoc" {build & >= "1.3.0"} + "odoc" {build & >= "1.4.0"} "omd" {<= "1.3.1"} "pprint" "ppx_cstruct" diff --git a/src/ace-lib/ace.ml b/src/ace-lib/ace.ml index 58f55feca..a8d23c07d 100644 --- a/src/ace-lib/ace.ml +++ b/src/ace-lib/ace.ml @@ -43,6 +43,8 @@ let create_range s e = let read_position pos = (pos##.row, pos##.column) +let get_cursor_position {editor} = editor##(getCursorPosition) + let read_range range = ((range##.start##.row, range##.start##.column), (range##.end_##.row, range##.end_##.column)) @@ -92,6 +94,12 @@ let set_custom_data { editor } data = let set_mode {editor} name = editor##getSession##(setMode (Js.string name)) +let on {editor} event callback = + editor##getSession##(on (Js.string event) (Js.Unsafe.meth_callback callback)) + +let insert {editor} position content = + editor##getSession##(insert position (Js.string content)) + type mark_type = Error | Warning | Message let string_of_make_type: mark_type -> string = function diff --git a/src/ace-lib/ace.mli b/src/ace-lib/ace.mli index 21be73d09..c7b16119a 100644 --- a/src/ace-lib/ace.mli +++ b/src/ace-lib/ace.mli @@ -20,6 +20,9 @@ type loc = { val create_editor: Dom_html.divElement Js.t -> 'a editor val set_mode: 'a editor -> string -> unit +val on: 'b editor -> string -> (Dom_html.event Js.t -> unit) -> unit +val insert: 'a editor -> Ace_types.position Js.t -> string -> unit + val read_range: Ace_types.range Js.t -> (int * int) * (int * int) val create_range: @@ -28,6 +31,7 @@ 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_cursor_position: 'a editor -> Ace_types.position Js.t val get_contents: ?range:Ace_types.range Js.t -> 'a editor -> string val get_line: 'a editor -> int -> string diff --git a/src/ace-lib/ace_types.mli b/src/ace-lib/ace_types.mli index 1867a3128..a4b533444 100644 --- a/src/ace-lib/ace_types.mli +++ b/src/ace-lib/ace_types.mli @@ -60,6 +60,10 @@ class type editSession = object 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 on : Js.js_string Js.t -> + ((Dom_html.event Js.t , unit) Js.meth_callback)-> + unit Js.meth + method insert : position Js.t -> 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 diff --git a/src/app/dune b/src/app/dune index e48a6d160..101753c1b 100644 --- a/src/app/dune +++ b/src/app/dune @@ -24,6 +24,7 @@ learnocaml_data learnocaml_api sha + grading_jsoo ocplib_i18n) ) @@ -38,13 +39,14 @@ learnocaml_app_common learnocaml_toplevel js_of_ocaml.ppx - ocplib_i18n) + ocplib_i18n + learnocaml_editor_tab) (modules Learnocaml_teacher_tab Learnocaml_index_main) (preprocess (pps ppx_ocplib_i18n js_of_ocaml.ppx)) (js_of_ocaml (flags :standard +cstruct/cstruct.js) - (javascript_files ../ace-lib/ace_bindings.js)) + (javascript_files ../ace-lib/ace_bindings.js ../../static/js/jszip/learnocaml_jszip_wrapper.js)) ) (executable @@ -59,7 +61,8 @@ learnocaml_app_common learnocaml_toplevel js_of_ocaml.ppx - ocplib_i18n) + editor_lib + ocplib_i18n) (modules Learnocaml_exercise_main) (preprocess (pps ppx_ocplib_i18n js_of_ocaml.ppx)) (js_of_ocaml diff --git a/src/app/learnocaml_common.ml b/src/app/learnocaml_common.ml index bd9c10aff..2f4b15652 100644 --- a/src/app/learnocaml_common.ml +++ b/src/app/learnocaml_common.ml @@ -113,12 +113,12 @@ let ext_alert ~title ?(buttons = [close_button [%i"OK"]]) message = 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; + H.div [ + H.h3 [ H.pcdata title ]; + H.div message; + H.div ~a:[ H.a_class ["buttons"] ] buttons; + ] ] - ] let lwt_alert ~title ~buttons message = let waiter, wakener = Lwt.task () in @@ -1230,3 +1230,43 @@ module Display_exercise = [ focus ; requirements ; backward ; forward ] ]) end + +module Grade_exercise = struct + +let get_grade = + let get_worker = get_worker_code "learnocaml-grader-worker.js" in + fun ?callback ?timeout exercise -> + 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" ; + 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.[ pcdata [%i"Report"] ] + end else if grade = 0 then begin + Manip.addClass report_button "failure" ; + Manip.replaceChildren report_button + Tyxml_js.Html5.[ pcdata [%i"Report"] ] + end else begin + Manip.addClass report_button "partial" ; + let pct = Format.asprintf "%2d%%" grade in + Manip.replaceChildren report_button + Tyxml_js.Html5.[ pcdata [%i"Report"] ; + span ~a: [ a_class [ "score" ] ] [ pcdata pct ]] + end ; + let report_container = find_component "learnocaml-exo-tab-report" in + Manip.setInnerHtml report_container + (Format.asprintf "%a" Report.(output_html ~bare: true) report) ; + grade + +end diff --git a/src/app/learnocaml_common.mli b/src/app/learnocaml_common.mli index 275f2c8cf..c2d959558 100644 --- a/src/app/learnocaml_common.mli +++ b/src/app/learnocaml_common.mli @@ -23,6 +23,14 @@ val fatal : ?title: string -> string -> unit val alert : ?title: string -> ?buttons: Html_types.div_content Tyxml_js.Html.elt list -> string -> unit +val box_button : + string Tyxml_js.Html5.wrap -> (unit -> 'a) -> + [> Html_types.button ] Tyxml_js.Html5.elt + +val close_button : + string Tyxml_js.Html5.wrap -> + [> Html_types.button ] Tyxml_js.Html5.elt + val ext_alert : title: string -> ?buttons: Html_types.div_content_fun Tyxml_js.Html.elt list -> @@ -277,3 +285,13 @@ module Display_exercise :functor 'a Learnocaml_data.token option -> Learnocaml_data.Exercise.Meta.t -> string -> unit Lwt.t end + +module Grade_exercise : sig + val get_grade : + ?callback:(string -> unit) -> + ?timeout:float -> + Learnocaml_exercise.t -> + (string -> (Learnocaml_report.t * string * string * string) Lwt.t) Lwt.t + val display_report : + Learnocaml_exercise.t -> Learnocaml_data.Report.t -> int +end diff --git a/src/app/learnocaml_config.ml b/src/app/learnocaml_config.ml index f99afef34..335971396 100644 --- a/src/app/learnocaml_config.ml +++ b/src/app/learnocaml_config.ml @@ -9,6 +9,7 @@ class type learnocaml_config = object method enableTryocaml: bool Js.optdef_prop method enableLessons: bool Js.optdef_prop method enableExercises: bool Js.optdef_prop + method enableEditor: 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 diff --git a/src/app/learnocaml_config.mli b/src/app/learnocaml_config.mli index ba20ae535..5d2c4c31c 100644 --- a/src/app/learnocaml_config.mli +++ b/src/app/learnocaml_config.mli @@ -13,6 +13,7 @@ class type learnocaml_config = object method enableTryocaml: bool Js.optdef_prop method enableLessons: bool Js.optdef_prop method enableExercises: bool Js.optdef_prop + method enableEditor: 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 diff --git a/src/app/learnocaml_exercise_main.ml b/src/app/learnocaml_exercise_main.ml index db13a77a1..1346c0926 100644 --- a/src/app/learnocaml_exercise_main.ml +++ b/src/app/learnocaml_exercise_main.ml @@ -12,6 +12,7 @@ open Lwt.Infix open Learnocaml_common open Learnocaml_data open Learnocaml_config +open Grade_exercise module H = Tyxml_js.Html @@ -34,42 +35,6 @@ let check_if_need_refresh has_server = 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 -> - 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" ; - 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" ; - 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 ; - let report_container = find_component "learnocaml-exo-tab-report" in - Manip.setInnerHtml report_container - (Format.asprintf "%a" Report.(output_html ~bare: true) report) ; - grade - module Exercise_link = struct let exercise_link ?(cl = []) id content = @@ -79,9 +44,9 @@ module Exercise_link = ] content end - -module Display = Display_exercise(Exercise_link) -open Display + +module Display = Display_exercise(Exercise_link) +open Display let is_readonly = ref false @@ -91,6 +56,13 @@ let make_readonly () = [%i"The deadline for this exercise has expired. Any changes you make \ from now on will remain local only."] +(* experiment button of editor.html redirects to the html associated to this ml + to know if we are in this page because of that we decide + to put a '.' before the id + therefore idEditor looks for a '.' before the id *) + +let idEditor s = not ((Regexp.string_match (Regexp.regexp "^[.]+") s 0) = None) + let () = run_async_with_log @@ fun () -> set_string_translations_exercises (); @@ -115,12 +87,27 @@ let () = in Dom_html.document##.title := Js.string (id ^ " - " ^ "Learn OCaml" ^" v."^ Learnocaml_api.version); - let exercise_fetch = - token >>= fun token -> - retrieve (Learnocaml_api.Exercise (token, id)) + + (* if we came from a true exercise we search in the server. + In the other case we get the exercise information from the Local storage *) + (* FIXME: for debug purposes; to be removed: + let ok str = lwt_alert ~title:"TEST1" ~buttons:["OK", (fun () -> Lwt.return_unit)] + [ H.p [H.txt (String.trim str)] ] in *) + let exercise_fetch = match idEditor id with + | false -> token >>= fun token -> + retrieve (Learnocaml_api.Exercise (token, id)) + + | true -> let proper_id = String.sub id 1 ((String.length id)-1) in + let state = Editor_lib.get_editor_state proper_id in + (* FIXME: for debug purposes; to be removed: + ok state.Editor.metadata.title >>= fun () -> *) + let exo = Editor_lib.exo_creator proper_id in + Lwt.return (state.Editor.metadata, exo, None) in + let after_init top = exercise_fetch >>= fun (_meta, exo, _deadline) -> + begin match Learnocaml_exercise.(decipher File.prelude exo) with | "" -> Lwt.return true | prelude -> @@ -196,6 +183,18 @@ let () = (* ---- main toolbar -------------------------------------------------- *) let exo_toolbar = find_component "learnocaml-exo-toolbar" in let toolbar_button = button ~container: exo_toolbar ~theme: "light" in + let () = + if idEditor id then + begin + let id = String.sub id 1 ((String.length id)-1) in + begin toolbar_button + ~icon: "upload" [%i"Edit"] @@ fun ()-> + Dom_html.window##.location##assign + (Js.string (api_server ^ "/editor.html#id=" ^ id ^ "&action=open")); + Lwt.return_unit + end; + end + in begin toolbar_button ~icon: "list" [%i"Exercises"] @@ fun () -> Dom_html.window##.location##assign @@ -208,6 +207,7 @@ let () = let worker = ref (get_grade ~callback exo) in + begin toolbar_button ~icon: "typecheck" [%i"Compile"] @@ fun () -> typecheck true diff --git a/src/app/learnocaml_index_main.ml b/src/app/learnocaml_index_main.ml index cf67f896c..b1a41d55b 100644 --- a/src/app/learnocaml_index_main.ml +++ b/src/app/learnocaml_index_main.ml @@ -512,6 +512,11 @@ let teacher_tab token a b () = Learnocaml_teacher_tab.teacher_tab token a b () >>= fun div -> Lwt.return div +let editor_tab a b () = + show_loading[%i"Loading Editor"] @@ fun () -> + Learnocaml_editor_tab.editor_tab a b () >>= fun div -> + Lwt.return div + let get_stored_token () = Learnocaml_local_storage.(retrieve sync_token) @@ -689,7 +694,12 @@ let () = then [ "playground", ([%i"Playground"], playground_tab token) ] else []) @ (match token with | Some t when Token.is_teacher t -> - [ "teacher", ([%i"Teach"], teacher_tab t) ] + [ "teacher", ([%i"Teach"], teacher_tab t) ] @ + if get_opt config##.enableEditor then + [ "editor", ([%i"Editor"], editor_tab) ] + else [] + | None when get_opt config##.enableEditor -> + [ "editor", ([%i"Editor"], editor_tab) ] | _ -> []) in let container = El.tab_buttons_container in @@ -798,8 +808,10 @@ let () = H.div ~a:[H.a_style "text-align: center;"] [token_disp_div (get_stored_token ())]] (fun () -> - Lwt.async @@ fun () -> + Lwt.async @@ fun () -> + let index = Learnocaml_local_storage.(retrieve editor_index) in Learnocaml_local_storage.clear (); + Learnocaml_local_storage.(store editor_index index); reload (); Lwt.return_unit) in diff --git a/src/app/learnocaml_local_storage.ml b/src/app/learnocaml_local_storage.ml index d2e5e0d66..15c873881 100644 --- a/src/app/learnocaml_local_storage.ml +++ b/src/app/learnocaml_local_storage.ml @@ -1,3 +1,4 @@ + (* This file is part of Learn-OCaml. * * Copyright (C) 2019 OCaml Software Foundation. @@ -160,7 +161,7 @@ let nickname = and delete () = delete_single key enc () in { key = Some key ; dependent_keys = (=) key ; store ; retrieve ; delete ; listeners = [] } - + let cached_exercise name = let key = mangle [ "cached-exercise" ; name ] in let enc = Learnocaml_exercise.enc in @@ -253,3 +254,26 @@ let exercise_toplevel_history_list, [ "exercise-toplevel-history" ] ~default: Learnocaml_toplevel_history.empty_snapshot Learnocaml_toplevel_history.snapshot_enc + +(* Editor storage *) +let editor_index= + let key = mangle [ "editor-index" ] in + let enc = SMap.enc Editor.editor_state_enc in + let store value = store_single key enc value + and retrieve () = + try retrieve_single key enc () with Not_found -> SMap.empty + and delete () = delete_single key enc () in + { key = Some key ; dependent_keys = (=) key ; + store ; retrieve ; delete ; listeners = [] } + +let editor_templates = + let key = mangle [ "editor-templates" ] in + let enc = Json_encoding.list Editor.editor_template_enc in + let store value = store_single key enc value + 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 = [] } + + diff --git a/src/app/learnocaml_local_storage.mli b/src/app/learnocaml_local_storage.mli index 1755e6342..18f3fb65a 100644 --- a/src/app/learnocaml_local_storage.mli +++ b/src/app/learnocaml_local_storage.mli @@ -47,3 +47,7 @@ val server_id : int storage_key val sync_token : Token.t storage_key val nickname : string storage_key + +val editor_index : Editor.editor_state SMap.t storage_key + +val editor_templates : Editor.editor_template list storage_key diff --git a/src/editor/build.ocp b/src/editor/build.ocp new file mode 100644 index 000000000..c02e14431 --- /dev/null +++ b/src/editor/build.ocp @@ -0,0 +1,157 @@ +begin program "new_exercise" + comp_requires = "ppx_ocplib_i18n:asm" + requires = [ + "ezjsonm" + "grading-jsoo" + "ace" + "learnocaml-state" + "learnocaml-repository" + "learnocaml-app-common" + "learnocaml-toplevel" + "jsutils" + "ppx_metaquot_lib" + "js_of_ocaml.ppx" + "ocplib_i18n" + "ocplib-json-typed.browser" + "translate" + "editor_lib" + ] + files = [ + "new_exercise.ml" ( comp = [ ppx_js ppx_ocplib_i18n] ) + ] + build_rules = [ + "%{new_exercise_FULL_DST_DIR}%/new-exercise.js" ( + build_target = true + sources = %byte_exe( p = "new_exercise" ) + commands = [ { + "js_of_ocaml" + "+cstruct/cstruct.js" + "%{ace_FULL_SRC_DIR}%/ace_bindings.js" + %byte_exe( p = "new_exercise" ) + } ] + ) + ] +end + +begin program "testhaut" + comp_requires = "ppx_ocplib_i18n:asm" + requires = [ + "ezjsonm" + "grading-jsoo" + "ace" + "learnocaml-state" + "learnocaml-repository" + "learnocaml-app-common" + "learnocaml-toplevel" + "jsutils" + "jsutils" + "ppx_metaquot_lib" + "js_of_ocaml.ppx" + "ocplib_i18n" + "ocplib-json-typed.browser" + "editor_lib" + "translate" + "test-spec" + ] + files = [ + "testhaut.ml" ( comp = [ ppx_js ppx_ocplib_i18n] ) + ] + build_rules = [ + "%{testhaut_FULL_DST_DIR}%/testhaut.js" ( + build_target = true + sources = %byte_exe( p = "testhaut" ) + commands = [ { + "js_of_ocaml" + "+cstruct/cstruct.js" + "+dynlink.js" + "+toplevel.js" + "--toplevel" + "--nocmis" + "%{ace_FULL_SRC_DIR}%/ace_bindings.js" + %byte_exe( p = "testhaut" ) + } ] + ) + ] +end + +begin library "editor_lib" + comp_requires = [ "ppx_ocplib_i18n:asm" "ppx_metaquot:asm" ] + + files = [ + "editor_lib.ml" ( comp = [ ppx_js ppx_ocplib_i18n "-ppx" %asm_exe( p = "ppx_metaquot") ] ) + ] + requires = [ + "ace" + "learnocaml-repository" + "ocplib_i18n" + "js_of_ocaml.ppx" + "learnocaml-app-common" + "translate" + "omd" + ] + +end + + +begin library "test-spec" + comp_requires = "ppx_metaquot:asm" "ppx_ocplib_i18n:asm" + link += [ "-linkall" ] + requires = [ + "ty" + "toploop" + "ppx_metaquot" + "ppx_metaquot_lib" + "ocplib-json-typed" + "learnocaml-state" + "learnocaml-repository" + "testing" + "ocplib_i18n" + "editor_lib" + "translate" + ] + files = [ + "test_spec.ml" ( comp += [ ppx_js "-ppx" %asm_exe( p = "ppx_metaquot") ppx_ocplib_i18n ] ) + ] +end + + +begin program "editor" + comp_requires = "ppx_ocplib_i18n:asm" + + requires = [ + "ezjsonm" + "grading-jsoo" + "ace" + "learnocaml-repository" + "learnocaml-app-common" + "learnocaml-toplevel" + "ocplib_i18n" + "js_of_ocaml.ppx" + "ocplib-json-typed.browser" + "omd" + "testing" + "grading" + "editor_lib" + "test-spec" + "translate" + ] + files = [ + "editor.ml" ( comp = [ ppx_js ppx_ocplib_i18n] ) + ] + build_rules = [ + "%{editor_FULL_DST_DIR}%/editor.js" ( + build_target = true + sources = %byte_exe( p = "editor" ) + commands = [ { + "js_of_ocaml" + "+cstruct/cstruct.js" + "+dynlink.js" + "+toplevel.js" + "--toplevel" + "--nocmis" + "%{ace_FULL_SRC_DIR}%/ace_bindings.js" + %byte_exe( p = "editor" ) + } ] + ) + ] +end diff --git a/src/editor/dune b/src/editor/dune new file mode 100644 index 000000000..06bec26ac --- /dev/null +++ b/src/editor/dune @@ -0,0 +1,132 @@ +(library + (name jszip) + (wrapped false) + (flags :standard -w -9 -warn-error -27) + (modules_without_implementation Jszip) + (modules Jszip) + (libraries jsutils + js_of_ocaml + js_of_ocaml-lwt + lwt) + (preprocess (pps js_of_ocaml.ppx)) + ) + +(library + (name editor_lib) + (wrapped false) + (modes byte) + (flags :standard -warn-error -4-42-44-45-48-33-27-32-34) + (libraries + ace + learnocaml_repository + ocplib_i18n + js_of_ocaml.ppx + learnocaml_app_common + learnocaml_ppx_metaquot + learnocaml_ppx_metaquot_lib + omd) + (modules Editor_lib) + (preprocess (pps learnocaml_ppx_metaquot ppx_ocplib_i18n js_of_ocaml.ppx )) +) + + +(library + (name test_spec) + (wrapped false) + (modes byte) + (flags :standard -warn-error -4-42-44-45-48-33-27-32-34) + (libraries + ty + toploop + learnocaml_ppx_metaquot + learnocaml_ppx_metaquot_lib + ocplib-json-typed + learnocaml_data + learnocaml_repository + testing + ocplib_i18n + editor_lib + grading + ) + (modules Test_spec) + (preprocess (pps ppx_ocplib_i18n learnocaml_ppx_metaquot)) + ) + + +(library + (name learnocaml_editor_tab) + (modes byte) + (flags :standard -warn-error -6-9-27-33-39) + (libraries ezjsonm + ace + sha + learnocaml_repository + learnocaml_app_common + learnocaml_toplevel + js_of_ocaml.ppx + ocplib_i18n + editor_lib) + (modules Learnocaml_editor_tab) + (preprocess (pps ppx_ocplib_i18n js_of_ocaml.ppx)) +) + + +(executable + (name new_exercise) + (flags :standard -warn-error -6-9-27-33-39) + (modes byte) + (libraries + ezjsonm + grading_jsoo + ace + sha + learnocaml_repository + learnocaml_app_common + learnocaml_toplevel + learnocaml_data + jsutils + learnocaml_ppx_metaquot_lib + js_of_ocaml.ppx + ocplib_i18n + ocplib-json-typed.browser + editor_lib + ) + (modules New_exercise) + (preprocess (pps ppx_ocplib_i18n js_of_ocaml.ppx )) + (js_of_ocaml + (flags :standard +cstruct/cstruct.js) + (javascript_files ../ace-lib/ace_bindings.js))) + +(executable + (name editor) + (flags :standard -warn-error -6-9-27-33-39) + (libraries + ezjsonm + grading_jsoo + ace + sha + learnocaml_repository + learnocaml_app_common + learnocaml_toplevel + js_of_ocaml.ppx + ocplib-json-typed.browser + ocplib_i18n + omd + testing + grading + editor_lib + jszip + test_spec) + (modules Editor) + (preprocess (pps ppx_ocplib_i18n js_of_ocaml.ppx)) + (js_of_ocaml (flags :standard --toplevel --nocmis + +cstruct/cstruct.js +dynlink.js +toplevel.js) + (javascript_files ../ace-lib/ace_bindings.js)) +) + + +(install + (section share) + (package learn-ocaml) + (files (editor.bc.js as editor/js/editor.js) + (new_exercise.bc.js as editor/js/new-exercise.js))) diff --git a/src/editor/editor.ml b/src/editor/editor.ml new file mode 100644 index 000000000..6ff5da802 --- /dev/null +++ b/src/editor/editor.ml @@ -0,0 +1,635 @@ +(* This file is part of Learn-OCaml. + * + * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2016-2018 OCamlPro. + * + * The main authors of the editor part is the pfitaxel team see + * https://github.com/pfitaxel/learn-ocaml-editor for more information + * + * Learn-OCaml is distributed under the terms of the MIT license. See the + * included LICENSE file for details. *) + +open Js_utils +open Lwt.Infix +open Learnocaml_common +open Learnocaml_config +open Learnocaml_data +open Js_of_ocaml +open Editor_lib +open Dom_html +open Test_spec +open Editor_components +open Grade_exercise + +module H = Tyxml_js.Html + +(*----------------------------------------------------------------------*) + +let init_tabs, select_tab = + mk_tab_handlers "question" [ "toplevel" ; "report" ; "editor" ; "template" ; "test" ; + "prelude" ; "prepare" ] + +let set_string_translations () = + let translations = [ + "txt_preparing", [%i"Preparing the environment"]; + "learnocaml-exo-button-editor", [%i"Solution"]; + "learnocaml-exo-button-template", [%i"Template"]; + "learnocaml-exo-button-prelude", [%i"Prelude"]; + "learnocaml-exo-button-prepare", [%i"Prepare"]; + "learnocaml-exo-button-toplevel", [%i"Toplevel"]; + "learnocaml-exo-button-question", [%i"Question"]; + "learnocaml-exo-button-test", [%i"Test"]; + "learnocaml-exo-button-report", [%i"Report"]; + "learnocaml-exo-editor-pane", [%i"Editor"]; + "txt_grade_report", [%i"Click the Grade! button to test your solution"]; + "learnocaml-exo-test-pane", [%i"Editor"]; + "learnocaml-exo-button-editor", + [%i"Type here the solution of the exercise"]; + "learnocaml-exo-button-template", + [%i"Type here or generate the template \ + the student will complete or correct"]; + "learnocaml-exo-button-prelude", + [%i"Type here the definitions of types and \ + functions given to the student"]; + "learnocaml-exo-button-prepare", + [%i"Type here hidden definitions given to the student"]; + "learnocaml-exo-button-question", + [%i"Type here the wording of the exercise in Markdown"]; + "learnocaml-exo-button-test", + [%i"Type here the tests code"]; + ] in + List.iter + (fun (id, text) -> + Manip.setInnerHtml (find_component id) text) + translations + +let changed = ref false + +let activate_before_unload () :unit = + if not !changed then + begin + changed := true; + Js.Unsafe.js_expr + "window.onbeforeunload = function() {return 'You have unsaved changes!';}" + end +let unable_before_unload () :unit = + if !changed then + begin + changed := false; + Js.Unsafe.js_expr "window.onbeforeunload = null" + end + +let onchange ace_list = + let add_change_listener ace = + Ace.on + ace + "change" + (fun _ -> activate_before_unload ();) in + List.iter (fun ace -> add_change_listener ace) ace_list + +let recovering_callback = ref (fun () -> ()) + +let () = + run_async_with_log @@ fun () -> + (*set_string_translations ();*) + Learnocaml_local_storage.init () ; + + (* ---- launch everything --------------------------------------------- *) + let toplevel_buttons_group = button_group () in + 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 template_toolbar = find_component "learnocaml-exo-template-toolbar" in + let prelude_toolbar = find_component "learnocaml-exo-prelude-toolbar" in + let prepare_toolbar = find_component "learnocaml-exo-prepare-toolbar" in + let test_toolbar = find_component "learnocaml-exo-test-toolbar" in + let toplevel_button = button ~container: toplevel_toolbar ~theme: "dark" in + let editor_button = button ~container: editor_toolbar ~theme: "light" in + let test_button = button ~container: test_toolbar ~theme: "light" in + let template_button = button ~container: template_toolbar ~theme: "light" in + let prelude_button = button ~container: prelude_toolbar ~theme: "light" in + let prepare_button = button ~container: prepare_toolbar ~theme: "light" in + let id = match Url.Current.path with + | "" :: "exercises" :: p | "exercises" :: 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); + + (* TODO/FIXME: Extend and Use Learnocaml_common.toplevel_launch *) + let after_init top = + begin + Lwt.return true + end >>= fun r1 -> + Learnocaml_toplevel.load ~print_outcome:false top + "" >>= fun r2 -> + if not r1 || not r2 then failwith [%i"unexpected error"]; + Learnocaml_toplevel.set_checking_environment top >>= fun () -> + Lwt.return () in + + let timeout_prompt = + Learnocaml_toplevel.make_timeout_popup + ~on_show: (fun () -> select_tab "toplevel") + () in + let flood_prompt = + Learnocaml_toplevel.make_flood_popup + ~on_show: (fun () -> select_tab "toplevel") + () in + let history = + let storage_key = + Learnocaml_local_storage.exercise_toplevel_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 -> + let toplevel_launch = + Learnocaml_toplevel.create ~worker_js_file + ~after_init ~timeout_prompt ~flood_prompt + ~on_disable_input: (fun _ -> disable_button_group toplevel_buttons_group) + ~on_enable_input: (fun _ -> enable_button_group toplevel_buttons_group) + ~container:(find_component "learnocaml-exo-toplevel-pane") + ~history () in + init_tabs () ; + toplevel_launch >>= fun top -> + + (* ---- toplevel pane ------------------------------------------------- *) + + begin toplevel_button + ~group: toplevel_buttons_group + ~icon: "cleanup" [%i"Clear"] @@ fun () -> + Learnocaml_toplevel.clear top ; + Lwt.return () + end ; + begin toplevel_button + ~icon: "reload" [%i"Reset"] @@ fun () -> + (* toplevel_launch >>= fun top -> SHOULD BE UNNECESSARY *) + disabling_button_group toplevel_buttons_group + (fun () -> Learnocaml_toplevel.reset top) + end ; + begin toplevel_button + ~group: toplevel_buttons_group + ~icon: "run" [%i"Eval phrase"] @@ fun () -> + Learnocaml_toplevel.execute top ; + Lwt.return () + end ; + + (* ---- prelude pane --------------------------------------------------- *) + + let editor_prelude = find_component "learnocaml-exo-prelude-pane" in + let editor_prel = Ocaml_mode.create_ocaml_editor + (Tyxml_js.To_dom.of_div editor_prelude) in + let ace_prel = Ocaml_mode.get_editor editor_prel in + let contents= get_prelude id in + Ace.set_contents ace_prel contents ; + Ace.set_font_size ace_prel 18; + + let typecheck_prelude () = + Editor_lib.typecheck true ace_prel editor_prel top "" (Ace.get_contents ace_prel) in + begin prelude_button + ~group: toplevel_buttons_group + ~icon: "typecheck" [%i"Check"] @@ typecheck_prelude + end; + + (* ---- prepare pane --------------------------------------------------- *) + + let editor_prepare = find_component "learnocaml-exo-prepare-pane" in + let editor_prep = Ocaml_mode.create_ocaml_editor + (Tyxml_js.To_dom.of_div editor_prepare) in + let ace_prep = Ocaml_mode.get_editor editor_prep in + let contents= get_prepare id in + Ace.set_contents ace_prep contents ; + Ace.set_font_size ace_prep 18; + + let typecheck_prepare () = + let prel = Ace.get_contents ace_prel ^ "\n" in + Editor_lib.typecheck true ace_prep editor_prep top prel + ~onpasterr:(fun () -> select_tab "prelude"; typecheck_prelude ()) + (Ace.get_contents ace_prep) in + begin prepare_button + ~group: toplevel_buttons_group + ~icon: "typecheck" [%i"Check"] @@ typecheck_prepare + end; + + (*-------question pane -------------------------------------------------*) + 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 editor_question = find_component "learnocaml-exo-question-mark" in + let ace_quest = Ace.create_editor (Tyxml_js.To_dom.of_div editor_question ) in + let question = + let a = get_question id in + if a = "" then {|# Exercise Title + +1. You can write here your questions using the + [**Markdown**](https://commonmark.org/) lightweight markup language. + +1. For details, see the [CommonMark spec](https://spec.commonmark.org/).|} + else a in + + Ace.set_contents ace_quest question ; + Ace.set_font_size ace_quest 18; + + let question = get_question id in + let question = Omd.to_html ~override:override_url (Omd.of_string question) in + + let text_container = find_component "learnocaml-exo-question-html" in + let text_div = Dom_html.createDiv Dom_html.document in + Manip.replaceChildren text_container + Tyxml_js.Html5.[ Tyxml_js.Of_dom.of_div text_div ] ; + text_div##.innerHTML := (Js.string question); + + let old_text = ref "" in + + let onload () = + let dyn_preview = + let text = Ace.get_contents ace_quest in + let question = Omd.to_html ~override:override_url (Omd.of_string text) in + if text <> !old_text then begin + text_div##.innerHTML := (Js.string question); + old_text := text + end in + dyn_preview; () in + + (* ---- editor pane --------------------------------------------------- *) + + 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 ace = Ocaml_mode.get_editor editor in + + let contents = + let a= get_solution id in + if a = "" then + [%i"(* Your solution *)\n"] + else + a in + Ace.set_contents ace contents; + Ace.set_font_size ace 18; + + + (* ---- test pane --------------------------------------------------- *) + + let editor_test = find_component "learnocaml-exo-test-pane" in + let editor_t = Ocaml_mode.create_ocaml_editor + (Tyxml_js.To_dom.of_div editor_test) in + let ace_t = Ocaml_mode.get_editor editor_t in + let contents= + let a = get_testml id in + if a = "" then + [%i"(* Grader and tests code *)\n"] + else + a in + + Ace.set_contents ace_t contents; + Ace.set_font_size ace_t 18; + + begin test_button + ~group: toplevel_buttons_group + ~icon: "sync" [%i"Generate"] @@ fun () -> + disabling_button_group toplevel_buttons_group + (fun () -> Learnocaml_toplevel.reset top) >>= fun () -> + Learnocaml_toplevel.execute_phrase top (Ace.get_contents ace) >>= + fun ok -> + if ok then + begin + let questions = monomorph_generator (extract_functions (get_answer top)) in + let string = compile questions in + Ace.set_contents ace_t string; + Lwt.return_unit + end + else (select_tab "toplevel" ; Lwt.return ()) + end; + + (* templates *) + let default_templates = + Templates.init(); + Templates.give_first_templates () + |> List.map (Templates.template_to_a_elt ace_t) + in + + let config_editor_div = H.(div ~a: [ a_class ["config-editor"]] []) in + + + let config_editor_ace = config_editor_div + |> Tyxml_js.To_dom.of_div + |> Ace.create_editor + in + Ace.set_font_size config_editor_ace 18; + Ace.set_mode config_editor_ace "ace/mode/ocaml"; + + let configuration = + let save () = + run_async_with_log @@ + fun () -> + Ace.get_contents config_editor_ace + |> Templates.from_string + |> Templates.save + |> !recovering_callback + |> Lwt.return + in + H.(a ~a: [ a_onclick (fun _ -> + let div = + ace_editor_container + ~box_title: "Template Configuration" + ~box_header: "The three first templates will be visible in the menu" + ~size: ("90%","80%") + ~save + ~editor: config_editor_div + in + Manip.appendToBody div; + Templates.give_templates () + |> Templates.to_string + |> Ace.set_contents config_editor_ace; + true )] + [pcdata "Configuration"]) + in + + let all_templates = + H.(a ~a:[a_class [ "editor-template"]; + a_onclick (fun _ -> + let content = + Templates.give_templates () + |> List.map (Templates.template_to_a_elt ace_t) + in + + let input_elt = + (H.input ()) + in + + + let div = + all_templates_container + ~box_title: "All templates" + ~size: ("90%","80%") + ~elements: content + ~box_header: input_elt + in + Manip.appendToBody div; + let to_change = + match Manip.by_classname "templates-to-change" with + | [] -> H.div [] + | div :: _ -> div + in + Manip.Ev.oninput input_elt + (fun _ -> let value = Manip.value input_elt in + let content = + Templates.give_templates () + |> List.filter ( fun Editor.{name; _ } -> + let reg_exp = Regexp.regexp value in + match Regexp.string_match reg_exp name 0 with + | None -> false + | Some _ -> true) + |> List.map (Templates.template_to_a_elt ace_t) + in + Manip.removeChild Manip.Elt.body div; + Manip.replaceChildren to_change content;true); + + true)] + [pcdata "All templates"]) + in + + let templates = + dropup + ~icon:"sync" + ~theme:"light" + "Templates" + (configuration :: all_templates :: default_templates) + in + Manip.appendChild test_toolbar templates; +(* end templates *) + + let typecheck_testml () = + let prelprep = (Ace.get_contents ace_prel ^ "\n" + ^ Ace.get_contents ace_prep ^ "\n") in + Editor_lib.typecheck true ace_t editor_t top prelprep ~mock:true + ~onpasterr:(fun () -> select_tab "prepare"; typecheck_prepare ()) + (Ace.get_contents ace_t) in + begin test_button + ~group: toplevel_buttons_group + ~icon: "typecheck" [%i"Check"] @@ typecheck_testml + end; + + (* ---- template pane --------------------------------------------------- *) + + let editor_template = find_component "learnocaml-exo-template-pane" in + let editor_temp = Ocaml_mode.create_ocaml_editor + (Tyxml_js.To_dom.of_div editor_template) in + let ace_temp = Ocaml_mode.get_editor editor_temp in + let contents= get_template id in + Ace.set_contents ace_temp contents ; + Ace.set_font_size ace_temp 18; + + let set_temp_tab () = + genTemplate top ~on_err:(fun () -> select_tab "toplevel") + (Ace.get_contents ace) >>= fun template -> + (Ace.set_contents ace_temp template; Lwt.return ()) in + begin template_button + ~icon: "sync" [%i"Gen. template"] @@ fun () -> + if (Ace.get_contents ace_temp) = "" then + set_temp_tab () + else + begin + Learnocaml_common.confirm + ~title:"Confirmation" + ~ok_label:"Yes" + [ H.p [H.pcdata "Do you want to crush the template?\n"] ] + (fun () -> Lwt.async set_temp_tab); + Lwt.return () + end; + end; + + let typecheck_template () = + let prelprep = (Ace.get_contents ace_prel ^ "\n" + ^ Ace.get_contents ace_prep ^ "\n") in + Editor_lib.typecheck true ace_temp editor_temp top prelprep + ~onpasterr:(fun () -> select_tab "prepare"; typecheck_prepare ()) + (Ace.get_contents ace_temp) in + begin template_button + ~group: toplevel_buttons_group + ~icon: "typecheck" [%i"Check"] @@ typecheck_template + end; + + let recovering () = + unable_before_unload (); + let solution = Ace.get_contents ace in + let descr = Ace.get_contents ace_quest in + let template = Ace.get_contents ace_temp in + let test = Ace.get_contents ace_t in + let prepare= Ace.get_contents ace_prep in + let prelude = Ace.get_contents ace_prel in + let open Learnocaml_data.Editor in + let exercise = + {id;prelude;template;descr;prepare;test;solution;max_score=0} + in + let old_state = get_editor_state id in + let new_state = {metadata=old_state.metadata;exercise} in + update_index new_state in + recovering_callback:= recovering; + begin editor_button + ~icon: "save" [%i"Save"] @@ fun () -> + recovering (); + Lwt.return () + end ; + begin editor_button + ~icon: "download" [%i"Save & Download"] @@ fun () -> + recovering () ; + Editor_io.download id; + Lwt.return () + end ; + + let typecheck_editor () = + let prelprep = (Ace.get_contents ace_prel ^ "\n" + ^ Ace.get_contents ace_prep ^ "\n") in + Editor_lib.typecheck true ace editor top prelprep + ~onpasterr:(fun () -> select_tab "prepare"; typecheck_prepare ()) + (Ace.get_contents ace) in + begin editor_button + ~group: toplevel_buttons_group + ~icon: "typecheck" [%i"Check"] @@ typecheck_editor + end; + begin toplevel_button + ~group: toplevel_buttons_group + ~icon: "run" [%i"Eval code"] @@ fun () -> + let prelprep = (Ace.get_contents ace_prel ^ "\n" + ^ Ace.get_contents ace_prep ^ "\n") in + Learnocaml_toplevel.execute_phrase top (prelprep ^ Ace.get_contents ace) + >>= fun _ -> Lwt.return () + end; + + (* ---- main toolbar -------------------------------------------------- *) + + let exo_toolbar = find_component "learnocaml-exo-toolbar" in + let toolbar_button = button ~container: exo_toolbar ~theme: "light" in + (*let toolbar_button2 = button2 ~container: exo_toolbar ~theme: "light" in*) + begin toolbar_button + ~icon: "left" [%i"Metadata"] @@ fun () -> + Dom_html.window##.location##assign + (Js.string (api_server ^ "/new-exercise.html#id=" ^ id ^ "&action=open")); + Lwt.return () + end; + begin toolbar_button + ~icon: "list" [%i"Exercises"] @@ fun () -> + Dom_html.window##.location##assign + (Js.string (api_server ^ "/index.html#activity=editor")); + Lwt.return () + end; + begin toolbar_button + ~icon: "upload" [%i"Experiment"] @@ + fun ()-> + Dom_html.window##.location##assign + (Js.string (api_server ^ "/exercise.html#id=." ^ id)); + Lwt.return_unit + end; + + (* TODO : factorize somehow this with + src/app/learnocaml_exercise_main grade to learnocaml_common *) + let messages = Tyxml_js.Html5.ul [] in + let callback text = + Manip.appendChild messages Tyxml_js.Html5.(li [ pcdata text ]) in + + let reset_worker () : + (string -> + (Learnocaml_report.item list * string * string * string) Lwt.t) Lwt.t = + Lwt.return (fun _s -> failwith "no worker") in + let gen_worker () = get_grade ~callback (exo_creator id) in + let worker = ref (reset_worker ()) in + let grade () = + let aborted, abort_message = + let t, u = Lwt.task () in + let btn = Tyxml_js.Html5.(button [ pcdata [%i "abort" ]]) in + Manip.Ev.onclick btn (fun _ -> Lwt.wakeup u () ; true) ; + let div = + Tyxml_js.Html5.(div ~a: [ a_class [ "dialog" ] ] + [ pcdata [%i"Grading is taking a lot of time, "] ; + btn ; + pcdata "?" ]) in + Manip.SetCss.opacity div (Some "0") ; + t, div in + Manip.replaceChildren messages + Tyxml_js.Html5.[ li [ pcdata [%i"Launching the grader"] ] ] ; + show_load "learnocaml-exo-loading" [ messages ; abort_message ]; + worker := gen_worker () ; + Lwt_js.sleep 1. >>= fun () -> + let prelprep = (Ace.get_contents ace_prel ^ "\n" ^ Ace.get_contents ace_prep ^ "\n") in + let solution = Ace.get_contents ace in + Learnocaml_toplevel.check top (prelprep ^ 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 := reset_worker () ; + 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_creator id) report in + select_tab "report" ; + Lwt_js.yield () >>= fun () -> + hide_loading ~id:"learnocaml-exo-loading" () ; + Lwt.return () + | Toploop_results.Error _ -> + select_tab "report" ; + worker := reset_worker () ; + Lwt_js.yield () >>= fun () -> + hide_loading ~id:"learnocaml-exo-loading" () ; + typecheck_editor () in + begin toolbar_button + ~group: toplevel_buttons_group + (* so "Check" disables "Save&Grade!" and conversely *) + ~icon: "reload" [%i"Save&Grade!"] @@ fun () -> + recovering (); + grade (); + end ; + onchange [ace_temp; ace_t; ace_prep; ace_prel; ace_quest; ace ]; + + (* ---- return -------------------------------------------------------- *) + (* toplevel_launch >>= fun _ -> should be unnecessary? *) + (* typecheck false >>= fun () -> *) + hide_loading ~id:"learnocaml-exo-loading" () ; + let () = Lwt.async @@ fun () -> + let _ = Dom_html.window##setInterval + (Js.wrap_callback (fun () -> onload ())) 200. in + Lwt.return () in + + Lwt.return ();; + +(* Temporary workaround; could be done without the sleep *) +let () = Lwt.async @@ + fun () -> + Lwt_js.sleep 5. >>= + fun () -> + changed:=true; + unable_before_unload (); + Lwt.return (); diff --git a/src/editor/editor_lib.ml b/src/editor/editor_lib.ml new file mode 100644 index 000000000..434d4bdd4 --- /dev/null +++ b/src/editor/editor_lib.ml @@ -0,0 +1,655 @@ +(* This file is part of Learn-OCaml. + * + * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2016-2018 OCamlPro. + * + * The main authors of the editor part is the pfitaxel team see + * https://github.com/pfitaxel/learn-ocaml-editor for more information + * + * Learn-OCaml is distributed under the terms of the MIT license. See the + * included LICENSE file for details. *) + +open Learnocaml_data +open Learnocaml_common +open Learnocaml_config +open Learnocaml_index +open Lwt.Infix +open Js_utils +open Dom_html +open Editor +open Exercise.Meta + +module H = Tyxml_js.Html + +(* Internationalization *) + +let get_editor_state id= + (SMap.find id Learnocaml_local_storage.(retrieve editor_index)) +let update_index editor_state = + let old_index= + Learnocaml_local_storage.(retrieve editor_index) + in + let new_index = + SMap.add editor_state.exercise.id editor_state old_index + in + Learnocaml_local_storage.(store editor_index) new_index + +let get_titre id = + (get_editor_state id).metadata.title +let get_description id = + match (get_editor_state id).metadata.short_description with + | None -> "" + | Some d -> d +let get_diff id = + (get_editor_state id).metadata.stars +let get_solution id = + (get_editor_state id).exercise.solution +let get_question id = + (get_editor_state id).exercise.descr +let get_template id = + (get_editor_state id).exercise.template +let get_testml id = + (get_editor_state id).exercise.test +let get_prelude id = + (get_editor_state id).exercise.prelude +let get_prepare id = + (get_editor_state id).exercise.prepare + + +let remove_exo exercise_id = + let old_index= Learnocaml_local_storage.(retrieve editor_index) in + let new_index= SMap.remove exercise_id old_index in + Learnocaml_local_storage.(store editor_index) new_index + +let titleUnique title = + let exos=Learnocaml_local_storage.(retrieve editor_index) in + match SMap.find_first_opt + (fun key -> (SMap.find key exos).metadata.title = title) + exos with + | None -> true + | _ -> false + +let idUnique id = + match SMap.find id Learnocaml_local_storage.(retrieve editor_index ) with + | exception Not_found -> true + | _ -> false + +let new_state (metadata:Exercise.Meta.t) = + let id= match metadata.id with + None -> "" + | Some i -> i + in + let exercise = {id;prelude=""; + template="";descr="";prepare=""; + test="";solution="";max_score=0} + in + {exercise;metadata} + +let setInnerHtml elt s = + elt##.innerHTML := Js.string s + +let show_load id contents = + let elt = match Manip.by_id id with + None -> failwith "Element not found" + | Some e -> e + in + 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 8 + 1) ^ ".gif" in + Manip.replaceChildren elt + Tyxml_js.Html.[ + div ~a: [ a_id "chamo" ] [ img ~alt: "loading" ~src: chamo_src () ] ; + div ~a: [ a_class [ "messages" ] ] contents + ] + + +let test_lib_prepare = + {|module Wrapper (Introspection : Introspection_intf.INTROSPECTION) = + struct + module Mock = struct + let results = ref None + let set_progress _ = () + let timeout = None + module Introspection = Introspection + end + module Test_lib = Test_lib.Make(Mock) + module Report = Learnocaml_report;; + open Mock + let code_ast = (failwith "WIP" : Parsetree.structure);; + |} + +let rec num_occs s i c num_acc = + if i > String.length s then num_acc + else match String.index_from_opt s (i + 1) c with + | None -> num_acc + | Some i -> num_occs s i c (num_acc + 1) +let offset_test_lib_prepare = + num_occs test_lib_prepare (-1) '\n' 0 + +let with_test_lib_prepare string = test_lib_prepare ^ string ^ " end";; +let typecheck set_class ace_t editor_t top prelprep ?(mock = false) ?onpasterr string = + let offset_prelprep = num_occs prelprep (-1) '\n' 0 in + let code = prelprep ^ if mock then with_test_lib_prepare string + else string + and grading = mock in + Learnocaml_toplevel.check ~grading top code >>= fun res -> + let error, warnings = + match res with + | Toploop_results.Ok ((), warnings) -> None, warnings + | Toploop_results.Error (err, warnings) -> Some err, warnings in + let shift_loc (l, c) = + (l - (if mock then offset_test_lib_prepare else 0) + - offset_prelprep, c) in + let transl_loc { Toploop_results.loc_start ; loc_end } = + { Ocaml_mode.loc_start = shift_loc loc_start ; + Ocaml_mode.loc_end = shift_loc 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 + 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 + let pasterr = + match error with + | None -> false + | Some {Ocaml_mode.locs; _} -> + List.exists (fun { Ocaml_mode.loc_start = (m, _); + Ocaml_mode.loc_end = (n, _) } -> (m <= 0 || n <= 0)) + locs in + match onpasterr, pasterr with + | Some onpasterr, true -> onpasterr () + | None, _ | _, false -> + Ocaml_mode.report_error ~set_class editor_t error warnings >>= fun () -> + Ace.focus ace_t; + Lwt.return () + +(* ---------- Functions for generate test -> Compile ---------- *) + +(* Unused: + +let rec redondance liste = match liste with + | [] -> [] + | e :: s -> e :: redondance (List.filter ((<>) e) s) + *) + +let init = "let () = + set_result @@ + ast_sanity_check code_ast @@ fun () ->\n" + +let section name report = {|Section ([ Text "Function:" ; Code "|} + ^ name ^ {|" ], |} ^ report ^ " );\n" + + +(*_____________________Functions for the Generate button_____________________*) + +(* Remove duplicates; keep the latest occurence *) +let rec undup_assoc = function + | [] -> [] + | (f, ty) :: l -> if List.mem_assoc f l then undup_assoc l + else (f, ty) :: undup_assoc l + +(* Minor bug: if the file contains 2 definitions of a same identifier + and only one of them is a function, this function will be selected + even if it is defined before the non-function expression. *) +let extract_functions s = + (* Remove module/module_types as their signature could contain val items *) + let s = Regexp.(global_replace (regexp "module type\\s\\w+\\s=\\ssig\\s[^]+?\\send\\s*") s "") in + let s = Regexp.(global_replace (regexp "module type\\s\\w+\\s=\\s\\w+\\s*") s "") in + let s = Regexp.(global_replace (regexp "module\\s\\w+\\s:\\ssig\\s[^]+?\\send\\s*") s "") in + let s = Regexp.(global_replace (regexp "module\\s\\w+\\s:\\s\\w+\\s*") s "") in + let rec process i acci = + match Regexp.(search (regexp "val\\s(\\S+)\\s:\\s([^:]+?)\\s+=\\s+") s i) with + | None -> List.rev acci + | Some (i, result) -> + match Regexp.(matched_group result 1, matched_group result 2) with + | Some func, Some ty -> process (i + 1) ((func, ty) :: acci) + | _ -> process (i + 1) acci + in undup_assoc (process 0 []) + +let find_all r s = + let rec process i acci = + match Regexp.(search r s i) with + | None -> List.rev acci + | Some (i, result) -> + match Regexp.(matched_group result 0) with + | Some s -> process (i + 1) (s :: acci) + | _ -> Dom_html.window##alert (Js.string "Error in editor_lib.ml: this should not occur"); + List.rev acci + in process 0 [] + +let replace_all map s = + List.fold_left (fun res (e, by) -> + Regexp.(global_replace (regexp_string e) res by)) + s map + +(* [gen1] and [gen2] are helper functions devised to generate for each + polymorphic function, two monomorphic testcases. + + Properties: + * forall i, gen1 i \in base + * forall i, gen1 i <> gen2 i + * forall i, j \in [[0, 11]], i <> j -> (gen1 i, gen2 i) <> (gen1 j, gen2 j) + + List.map gen1 [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11] + = ["int"; "bool"; "char"; "string"; "int"; "bool"; "char"; "string"; + "int"; "bool"; "char"; "string"] + + List.map gen2 [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11] + = ["bool"; "int"; "bool"; "bool"; "char"; "char"; "int"; "char"; + "string"; "string"; "string"; "int"] + *) +let base = ["int"; "bool"; "char"; "string"] +let gen1 i = List.nth base (i mod 4) +let gen2 i = + let q, r = (i / 4) mod 3, i mod 4 in + if q + 1 = r then List.nth base 0 + else List.nth base (q + 1) + +(* TODO/FIXME: when the considered function is higher-order, + we should insert a comment and disable automatic testcases (~gen:0) + in order to avoid the Exception: Failure "unsamplable type". *) +let monomorph_generator l = + let f ty = + let vars = + List.sort_uniq compare + @@ find_all (Regexp.regexp "'[A-Za-z](?:\\w[A-Za-z0-9_']*)?") ty + in + if vars = [] then [(10, ty)] + else let t1 = replace_all (List.mapi (fun i e -> (e, gen1 i)) vars) ty + and t2 = replace_all (List.mapi (fun i e -> (e, gen2 i)) vars) ty + in [(5, t1); (5, t2)] + in + List.map (fun (name, ty) -> + let list_mono = f ty in + let list_qst = List.map (fun (gen, mono_ty) -> + TestAgainstSol {name; ty = mono_ty; suite = "[]"; gen; + tester = ""; sampler = ""}) + list_mono + in (name, list_qst)) l + +(* ____Functions for "GenTemplate" feature__________________________________ *) + +let get_answer top = + Learnocaml_toplevel.execute_test top + +let genTemplate top ?(on_err = fun () -> ()) sol = + Learnocaml_toplevel.reset top >>= fun () -> + Learnocaml_toplevel.execute_phrase top sol >>= + fun ok -> + if ok then + let list_f_ty = extract_functions (get_answer top) in + let result = + List.fold_right (fun (f, _ty) r -> + Format.sprintf {|let %s =@. "Replace this string with you code."@.@.|} f ^ r) + list_f_ty "" + in Lwt.return result + else Lwt.return (let () = on_err () in "") + +(* ---- create an exo ------------------------------------------------------- *) + +let exo_creator proper_id = + let exercise = (get_editor_state proper_id).exercise in + let read_field field = + match field with + | "id" -> Some exercise.id (* XXX = proper_id *) + | "prelude.ml" -> Some exercise.prelude + | "template.ml" -> Some exercise.template + | "descr.md" -> (* FIXME: SHOULD BE 'Some exercise.descr' *) + Some "

Note: question export disabled for the time being; otherwise descrs_from_string raises a Stack_overflow on strings of size 14000.

" + | "prepare.ml" -> Some exercise.prepare + | "solution.ml" -> Some exercise.solution + | "test.ml" -> Some exercise.test + | "max_score.txt" -> Some (string_of_int exercise.max_score) + | "depend.txt" -> None (* TODO: Add support *) + | _ -> None + in + Learnocaml_exercise.read + ~read_field + ~id:proper_id + ~decipher:false + () + +(* TODO look for the record type of res to make the message more understandable *) +let typecheck_dialog_box div_id res = + let result,ok = + match res with + | Toploop_results.Ok _ -> [%i"Your question does typecheck. "],true + | Toploop_results.Error (err,_) -> + [%i"Your question does not typecheck. "] + ^ err.Toploop_results.msg ,false in + if ok then + begin + let messages = Tyxml_js.Html5.ul [] in + let _checked, check_message = + let t, _u = Lwt.task () in + let btn_ok = Tyxml_js.Html5.(button [ pcdata [%i"OK"] ]) in + Manip.Ev.onclick btn_ok ( fun _ -> + hide_loading ~id:div_id () ; true) ; + let div = + Tyxml_js.Html5.(div ~a: [ a_class [ "dialog" ] ] + [ pcdata result ; + btn_ok ; + ]) in + Manip.SetCss.opacity div (Some "0") ; + t, div in + Manip.replaceChildren messages + Tyxml_js.Html5.[ li [ pcdata "" ] ] ; + show_load div_id [ check_message ] ; + Manip.SetCss.opacity check_message (Some "1"); + Lwt.return (); + end + else + begin + hide_loading ~id:div_id (); + Dom_html.window##alert (Js.string result); + Lwt.return (); + end + + +let put_exercise_id id (old_state:editor_state) = + { + exercise = {old_state.exercise with id} ; + metadata = {old_state.metadata with id =Some id} + } + ;; + +module Editor_io = struct + + let download_file name contents = + let url = + Js.Unsafe.meth_call (Js.Unsafe.global##._URL) "createObjectURL" [| Js.Unsafe.inject contents |] 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)))) + + let download id = + let name = id ^ ".zip" in + let json = (get_editor_state id) + |> Json_repr_browser.Json_encoding.construct + Editor.editor_state_enc + in + let contents = Js._JSON##(stringify json) in + let editor_download = Js.Unsafe.eval_string "editor_download" in + let callback = download_file name in + let _ = + Js.Unsafe.fun_call editor_download + [|Js.Unsafe.inject contents; + Js.Unsafe.inject (Js.wrap_callback callback) |] in () + + let download_all () = + let name = "exercises.zip" in + let editor_index= Learnocaml_local_storage.(retrieve editor_index) in + let json = Json_repr_browser.Json_encoding.construct + (SMap.enc editor_state_enc) + editor_index + in + let exercises = Js._JSON##(stringify json) in + let index = SMap.fold + (fun k editor_state acc -> + (k, Some editor_state.metadata ) :: acc) editor_index [] + in + let index = Learnocaml_data.Exercise.Index.Exercises index + |> Json_repr_browser.Json_encoding.construct + Exercise.Index.enc + in + let index = Js._JSON##(stringify index) in + let editor_download = Js.Unsafe.eval_string "editor_download_all" in + let callback = download_file name in + let _ = + Js.Unsafe.fun_call editor_download + [|Js.Unsafe.inject exercises; + Js.Unsafe.inject index; + Js.Unsafe.inject (Js.wrap_callback callback) |] in () + + let upload_file () = + let input_files_load = + 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 -> + Lwt.wakeup result_wakener file; Js._true); + ignore (Js.Unsafe.meth_call input_files_load "click" [||]) ; + result_t + + + let upload () = + run_async_with_log + (fun () -> + upload_file () >>= + fun file -> + let (f:Js.js_string Js.t ->(Js.js_string Js.t -> unit)->unit) = Js.Unsafe.eval_string "editor_import" in + let override_all = Js_utils.confirm "Do you want to override all?" in + let callback = + (fun text -> + SMap.iter + (fun id editor_state -> + let editor_state = put_exercise_id id editor_state in (* update metadata.id *) + if (not (idUnique id && titleUnique id)) && not override_all then + let override = Js_utils.confirm + ([%i"Identifier and/or title not unique\n"] ^ + "id:" ^ id ^ [%i" title:"] ^ editor_state.metadata.title ^ + "\n Do you want to override?") + in + if override then + update_index editor_state; + else + update_index editor_state + ) + (Json_repr_browser.Json_encoding.destruct + (SMap.enc editor_state_enc) + (Js._JSON##(parse text))); + Dom_html.window##.location##reload) + in + let _ = + Js.Unsafe.fun_call f + [| Js.Unsafe.inject file ; + Js.Unsafe.inject callback|] + in Lwt.return_unit) +end + +module Templates = struct + + let give_templates () = + Learnocaml_local_storage.(retrieve editor_templates) + + (*gives the first 3 templates to show *) + let give_first_templates () = + let templates = + give_templates () + in + match templates with + | [] -> [] + | hd :: [] -> [hd] + | hd :: snd :: [] -> [hd; snd] + | hd :: snd :: thrd :: _ -> [ hd; snd; thrd] + + (* WARNING very important that |} is without indenitng and in a new line + if not there will be a bug for the first edition of the templates in the editor: + add templates after the last template is not possible if you don't know the trick. + The trick is to remove the new line of the last template and then manually type return in the keyboard *) + let against_solution_template = + { name = "Against solution"; + template = {| + let q_plus = + let prot = arg_ty [%ty:int] (last_ty [%ty: int] [%ty: int ]) in (* type: int-> int -> int *) + test_function_against_solution ~gen:(10) prot (*10 random tests *) + "plus" (* function name = plus *) + [1 @:!! 4 ; 3 @:!! 3 ];; (* compare (plus 1 4) and + (plus 3 3) against professor\'s solution *) +|} + } + + let test_suite_template = + { name = "Test Suite"; + template = {| + let q_plus2 = + let prot = arg_ty [%ty:int] (last_ty [%ty: int] [%ty: int ]) in (*type : int -> int ->int *) + test_function prot + (lookup_student (ty_of_prot prot) "plus") (*function name :"plus" *) + [5 @:!! 4 ==> 9; (* plus 5 4 = 9 *) + 5 @:!! 5 ==> 10; + 1 @:!! 1 ==> 2; + 0 @:!! 0 ==> 0];; +|} + } + + let save templates = + Learnocaml_local_storage.(store editor_templates templates) + + (* adding default templates if empty *) + let init () = let templates = give_templates () in + if templates = [] then + [against_solution_template; + test_suite_template] + |> save + + let to_string templates = + let rec aux acc = function + | [] -> acc + | Editor.{name; template} :: l -> + let new_acc = acc ^ "#" + ^ name ^ "\n" ^ template + in + aux new_acc l + in + aux "" templates + + let from_string string = + let extract = + Regexp.(split (regexp_with_flag "^#+\\s*(.*)\n" "m")) string + in + + let rec aux acc = function + | name :: template :: l -> aux ({name;template}:: acc) l + | _ -> acc + in + match extract with + | [] -> [] + | _ ::l -> List.rev (aux [] l) + + let template_to_a_elt ace_t Editor.{name; template = templ} = + H.(a ~a:[ a_onclick (fun _ -> + let position = Ace.get_cursor_position ace_t in + Ace.insert ace_t position templ; true); + a_class ["editor-template"]] + [pcdata name]) +end + +module Editor_components = struct + let dropup ~icon ~theme name items = + let dropup_content = + H.(div ~a:[a_class ["dropup-content"]] items) + in + let drop_button = + H.(button ~a:[a_class ["dropbtn"]] [ + img ~alt:"" ~src:(api_server ^ "/icons/icon_" ^ icon ^ "_" ^ theme ^ ".svg") () ; + pcdata " " ; + span ~a:[ a_class [ "label" ] ] [ pcdata name ] + ]) + in + Manip.Ev.onclick drop_button + (fun _ -> Manip.toggleClass dropup_content "show"); + (* TODO translate it to js_of_ocaml *) + let _ = + Js.Unsafe.js_expr " //Close the dropdown menu if the user clicks outside of it\nwindow.onclick = function(event) {\n if (!event.target.matches(\'.dropbtn\')) {\n var dropdowns = document.getElementsByClassName(\"dropup-content\");\n var i;\n for (i = 0; i < dropdowns.length; i++) {\n var openDropdown = dropdowns[i];\n if (openDropdown.classList.contains(\'show\')) {\n openDropdown.classList.remove(\'show\');\n }\n }\n }\n} " in (); + H.(div ~a:[a_class ["dropup"]] [drop_button; dropup_content]) + + let editor_overlay () = + H.(div ~a:[a_class ["learnocaml-dialog-overlay"; "config-editor-overlay"] ] + []) + + + let editor_container ~size ~contents ~buttons ~box_title ~box_header = + let container = + H.(div + [ + h3 [pcdata box_title]; + div [box_header]; + contents; + div ~a:[a_class ["buttons"] ] buttons + ] + ) + in + let (width, height) = size in + Manip.SetCss.width container width; + Manip.SetCss.height container height; + container + + + let ace_editor_container ~save ~size ~editor ~box_title ~box_header = + let overlay = editor_overlay () in + let close_btn = + H.(button ~a:[ a_onclick (fun _ -> + Manip.removeChild Manip.Elt.body overlay;false + )] [pcdata "Cancel"]) + in + let save_btn = + H.(button ~a:[ a_onclick (fun _ -> + save(); + reload(); false + )] [pcdata "Save"]) + in + let container = editor_container + ~size + ~contents: editor + ~buttons: [close_btn;save_btn] + ~box_title + ~box_header: (H.pcdata box_header) + in + Manip.replaceChildren overlay [container]; + overlay + + let all_templates_container ~size ~elements ~box_title ~box_header = + let overlay = editor_overlay () in + let close () = Manip.removeChild Manip.Elt.body overlay in + let ok_btn = + H.(button ~a:[ a_onclick (fun _ -> + close ();false + )] [pcdata "Ok"]) + in + + List.iter + (fun elt -> + let dom_elt = Tyxml_js.To_dom.of_a elt in + Dom_html.addEventListener dom_elt Dom_html.Event.click + (Dom_html.handler ( fun _ -> close ();Js._true )) + Js._true + |> ignore) + elements; + let contents = H.(div ~a: [a_style "overflow:auto"; + a_class["templates-to-change"]] elements) + in + let container = editor_container + ~size + ~contents + ~buttons: [ok_btn] + ~box_title + ~box_header + in + Manip.replaceChildren overlay [container]; + overlay +end diff --git a/src/editor/editor_lib.mli b/src/editor/editor_lib.mli new file mode 100644 index 000000000..cd744b7a0 --- /dev/null +++ b/src/editor/editor_lib.mli @@ -0,0 +1,163 @@ +(* This file is part of Learn-OCaml. + * + * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2016-2018 OCamlPro. + * + * The main authors of the editor part is the pfitaxel team see + * https://github.com/pfitaxel/learn-ocaml-editor for more information + * + * Learn-OCaml is distributed under the terms of the MIT license. See the + * included LICENSE file for details. *) + +open Learnocaml_data +open Editor +module H = Tyxml_js.Html + +val update_index : Editor.editor_state -> unit + +(** Getters of an editor exercise + * @param the id *) +val get_editor_state : string -> Editor.editor_state +val get_titre : string -> string +val get_description : string -> string +val get_diff : string -> float +val get_solution : string -> string +val get_question : string -> string +val get_template : string -> string +val get_testml : string -> string +val get_prelude : string -> string +val get_prepare : string -> string + +val with_test_lib_prepare :string->string + +(** Remove an exercise from the local storage *) +val remove_exo : Map.Make(String).key -> unit + +(** @return a bool depending on whether the id is already used or not *) +val idUnique : string -> bool +(** @return a bool depending on whether the title is already used or not *) +val titleUnique : string -> bool + +val new_state : Exercise.Meta.t -> editor_state +(** arguments Dom element , string *) +val setInnerHtml : < innerHTML : < set : Js.js_string Js.t -> unit; .. > + Js_of_ocaml.Js.gen_prop; .. > Js_of_ocaml.Js.t -> string -> unit + +(** Fragment of a test.ml code + * @see definition *) +val init : string + +(** Create the code of a section + * @param name_of_the_function associated_report *) +val section : string -> string -> string + +(* TODO: Remove commented code +(** @param content_of_the_toplevel [[]] + * @return a list + * The first value is the type of the first val, etc. *) +val get_all_val : char list -> char list list -> char list list + +(** Remove atomic values from a list of types + * @return a list of type of function (var function_name : type = ) + * @param content_of_the_toplevel [[]] result_list (second parameter must be []) *) +val get_only_fct : char list -> char list -> char list + +(** Associate each function with its type + * @ return a list of couple (function_name, function_type) + * @ param content_of_the_toplevel result_list (second param must be []) *) +val get_questions : char list list -> (string * string) list -> (string * string) list + *) + +(* +(** Create a list of triples (key, alea, "monorphic type"): + polymorph_detector [("f", "'a -> 'b"); ("p", "int -> int")] = + [("f", 5, "int -> bool"); ("f", 5, "bool -> char"); ("p", 10, "int -> int")] *) +val polymorph_detector : ('a * string) list -> ('a * int * string) list + *) + +(** [genTemplate top ?(on_err=fun()->()) sol]: + evaluate the solution [sol] using the toplevel [top], + generate then return a template string. + Run also [on_err] if there is a typecheck error. *) +val genTemplate : + Learnocaml_toplevel.t -> ?on_err:(unit -> unit) -> string -> string Lwt.t + +(** [typecheck set_class ace editor top prelprep ?(mock=false) ?onpasterr code]: + check if [code] (taken from buffer [ace, editor], with [prelprep] + prepended and with test_lib mock code if [mock=true]) compiles, + using toplevel [top]. Raise a CSS class if [set_class=true] (among + "ocaml-check-success", "ocaml-check-warn", "ocaml-check-error"). + Run [onpasterr] if some error line of code occurs with [loc < 0]. +*) +val typecheck : + bool -> 'a Ace.editor -> Ocaml_mode.editor -> Learnocaml_toplevel.t -> + string -> ?mock:bool -> ?onpasterr:(unit -> unit Lwt.t) -> string -> unit Lwt.t +(* Note: the type of ?onpasterr could be simplified, using more monadic style *) + +(** Create an exercise with the data of the local storage + * @param editor_exercise_id *) +val exo_creator : string -> Learnocaml_exercise.t + +(** @return the output of toplevel buffer *) +val get_answer : Learnocaml_toplevel.t -> string +val typecheck_dialog_box : string-> 'a Toploop_results.toplevel_result -> unit Lwt.t + + +(** Extract the function definitions from a toplevel output + @return the list of their (name, type) *) +val extract_functions : string -> (string * string) list + +(** Generate monomorphic test specifications + @return a list of ("function_name", list_of_monomorphic_test_cases) *) +val monomorph_generator : (string * string) list -> (string * Editor.test_qst_untyped list) list + +val show_load : Html_types.text Tyxml_js.Html.wrap -> +[< Html_types.div_content_fun ] Tyxml_js.Html.elt Tyxml_js.Html.list_wrap -> +unit + +module Editor_io : sig + val download : Learnocaml_data.SMap.key -> unit + val upload : unit -> unit + val download_all : unit -> unit +end + +module Templates : sig + val give_templates : unit -> Learnocaml_data.Editor.editor_template list + val give_first_templates : + unit -> Learnocaml_data.Editor.editor_template list + val against_solution_template : Learnocaml_data.Editor.editor_template + val test_suite_template : Learnocaml_data.Editor.editor_template + val save : Learnocaml_data.Editor.editor_template list -> unit + val init : unit -> unit + val to_string : Learnocaml_data.Editor.editor_template list -> string + val from_string : string -> Learnocaml_data.Editor.editor_template list + val template_to_a_elt : 'a Ace.editor -> Learnocaml_data.Editor.editor_template -> + [> [> Html_types.pcdata ] Html_types.a ] H.elt +end + +module Editor_components : sig + + val dropup : + icon:string -> + theme:string -> + string H.wrap -> + [< Html_types.div_content_fun ] H.elt H.list_wrap -> + [> Html_types.div ] H.elt + + val editor_overlay : unit -> [> Html_types.div ] H.elt + + val ace_editor_container : + save:(unit -> 'a) -> + size:string * string -> + editor:[< Html_types.div_content_fun > `Div `H3 ] H.elt -> + box_title:string H.wrap -> + box_header:string H.wrap -> [> Html_types.div ] H.elt + + val all_templates_container : + size:string * string -> + elements:[< `A of Html_types.flow5_without_interactive & 'a ] H.elt + H.list_wrap -> + box_title:string H.wrap -> + box_header:[< Html_types.div_content_fun ] H.elt -> + [> Html_types.div ] H.elt +end diff --git a/src/editor/jszip.mli b/src/editor/jszip.mli new file mode 100644 index 000000000..e69de29bb diff --git a/src/editor/learnocaml_editor_tab.ml b/src/editor/learnocaml_editor_tab.ml new file mode 100644 index 000000000..ea19f42d7 --- /dev/null +++ b/src/editor/learnocaml_editor_tab.ml @@ -0,0 +1,146 @@ +(* This file is part of Learn-OCaml. + * + * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2016-2018 OCamlPro. + * + * The main authors of the editor part is the pfitaxel team see + * https://github.com/pfitaxel/learn-ocaml-editor for more information + * + * Learn-OCaml is distributed under the terms of the MIT license. See the + * included LICENSE file for details. *) + +open Js_utils +open Lwt +open Learnocaml_data +open Learnocaml_common +open Learnocaml_config +open Editor +open Editor_lib +open Tyxml_js.Html5 + + +let fetch_editor_index ()= + let index= + Learnocaml_local_storage.(retrieve editor_index ) in + + let json = + Json_repr_browser.Json_encoding.construct + (SMap.enc editor_state_enc) index in + try Lwt.return (Json_repr_browser.Json_encoding.destruct + (SMap.enc editor_state_enc) json) with exn -> + let msg = + Format.asprintf "bad structure for %s@.%a" + "index" + (fun ppf -> Json_encoding.print_error ppf) exn in + Lwt.fail (Server_caller.Cannot_fetch msg);; + + +let delete_button_handler exercise_id = + (fun _ -> + let message = + pcdata [%i"Are you sure you want to delete this exercise?\n"] + in + Learnocaml_common.confirm + ~title:"Confirmation" + ~ok_label:"Yes" + [message] + (fun () -> + remove_exo exercise_id; + Dom_html.window##.location##reload); + true) ;; + +let import_bar = + a ~a:[ a_onclick (fun _ -> Editor_io.upload ();true); + a_class [ "exercise"] ] + [ div ~a:[ a_class [ "descr" ] ] [ + h1 [ pcdata [%i"Import"] ]; + p [pcdata [%i"Import from a zip file"]]]] + +let export_all_bar = + a ~a:[ a_onclick (fun _ -> Editor_io.download_all ();true); + a_class [ "exercise"] ] + [ div ~a:[ a_class [ "descr" ] ] [ + h1 [ pcdata [%i"Export all"] ]; + p [pcdata [%i"Export all exercises to a zip file"]]]] + +let new_exercise_bar = + a ~a:[ a_href "new-exercise.html"; + a_class [ "exercise" ] ] [ + div ~a:[ a_class [ "descr" ] ] [ + h1 [ pcdata [%i"New exercise"] ]; + p [pcdata [%i"Create \ + a new exercise"]]]] + + +let editor_tab _ _ () = + + + Lwt_js.sleep 0.5 >>= fun () -> + let _ =match Learnocaml_local_storage.(retrieve editor_index) with + | exception Not_found -> + Learnocaml_local_storage.(store editor_index) SMap.empty + | _ -> () + in + let content_div = find_component "learnocaml-main-content" in + fetch_editor_index () >>= + fun index -> + let format_exercise_list contents = + let open Tyxml_js.Html5 in + let open Exercise.Meta in + SMap.fold + (fun exercise_id editor_sate acc -> + div ~a:[a_id "toolbar"; a_class ["button"]] [ + (let button = button ~a:[a_id exercise_id] + [img ~src:(api_server ^ "/icons/icon_cleanup_dark.svg") + ~alt:"" () ; pcdata "" ] in + Manip.Ev.onclick button + (delete_button_handler exercise_id); button); + (let download_button = button ~a:[a_id exercise_id] + [img ~src:(api_server ^ "/icons/icon_download_dark.svg") + ~alt:"" () ; pcdata "" ] in + Manip.Ev.onclick download_button + (fun _ -> Editor_io.download exercise_id; true) ;download_button + )] :: + a ~a:[ a_href ("editor.html#id="^exercise_id) ; + a_class [ "exercise" ] ] + [div ~a:[ a_class [ "descr" ] ] [ + h1 [ pcdata editor_sate.metadata.title ] ; + p [ match editor_sate.metadata.short_description with + | None -> pcdata [%i"No description available."] + | Some text -> pcdata text ] ; + ] ; + div ~a:[ a_class [ "time-left" ] ] [pcdata ("id: " ^ editor_sate.exercise.id ) ]; + + div ~a:[a_class["stats"]] [ + div ~a:[ a_class [ "stars" ] ] [ + let num = 5 * int_of_float (editor_sate.metadata.stars*. 2.) in + let num = max (min num 40) 0 in + let alt = + Format.asprintf "difficulty: %d / 40" num in + let src = + api_server ^ (Format.asprintf "/icons/stars_%02d.svg" num) in + img ~alt ~src () + ] ; + div ~a:[ a_class [ "length" ] ] [ + match editor_sate.metadata.kind with + | Project -> pcdata "editor project" + | Problem -> pcdata "editor problem" + | Exercise -> + pcdata "editor exercise" ] ; + ]; + ] :: + acc) index contents + in + let c= List.rev + (format_exercise_list + [ new_exercise_bar; + export_all_bar; + import_bar] ) + in + let list_div = + Tyxml_js.Html5.(div ~a: + [ Tyxml_js.Html5.a_id "learnocaml-main-exercise-list" ]) + c in + Manip.appendChild content_div list_div ; + hide_loading ~id:"learnocaml-main-loading" () ; + Lwt.return list_div;; diff --git a/src/editor/learnocaml_editor_tab.mli b/src/editor/learnocaml_editor_tab.mli new file mode 100644 index 000000000..d2fbb8b85 --- /dev/null +++ b/src/editor/learnocaml_editor_tab.mli @@ -0,0 +1,12 @@ +(* This file is part of Learn-OCaml. + * + * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2016-2018 OCamlPro. + * + * The main authors of the editor part is the pfitaxel team see + * https://github.com/pfitaxel/learn-ocaml-editor for more information + * + * Learn-OCaml is distributed under the terms of the MIT license. See the + * included LICENSE file for details. *) + +val editor_tab : 'b -> 'c -> unit -> [> Html_types.div ] Tyxml_js.Html5.elt Lwt.t diff --git a/src/editor/new_exercise.ml b/src/editor/new_exercise.ml new file mode 100644 index 000000000..8c999cd39 --- /dev/null +++ b/src/editor/new_exercise.ml @@ -0,0 +1,221 @@ +(* This file is part of Learn-OCaml. + * + * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2016-2018 OCamlPro. + * + * The main authors of the editor part is the pfitaxel team see + * https://github.com/pfitaxel/learn-ocaml-editor for more information + * + * Learn-OCaml is distributed under the terms of the MIT license. See the + * included LICENSE file for details. *) + +open Js_of_ocaml +open Dom_html +open Js_utils +open Learnocaml_common +open Editor_lib +open Learnocaml_config +open Learnocaml_data +open Learnocaml_data.Editor +open Learnocaml_data.Exercise.Meta + +module StringMap = Map.Make (String) +(* +(* Internationalization *) +let () = Translate.set_lang () +let () = + let translations = [ + "txt_new_exo", [%i"New exercise"]; + "txt_id", [%i"Unique identifier:
"]; + "txt_title", [%i"Title (unique too):
"]; + "txt_descr", [%i"Description of the exercise:
"]; + "txt_diff", [%i"Difficulty level:
"]; + "cancel", [%i"Cancel"]; + "save", [%i"Save"]; + ] in + Translate.set_string_translations translations + *) + + +let getString element= Js.to_string element##.value +let getStringOpt = function + | None -> None + | Some input -> Some (Js.to_string input##.value) + +let getFloat element= float_of_string (Js.to_string element##.value) + +let get = function + None -> failwith "Element not found" + | Some s -> s + +let string_with_spaces list= + let s = + List.fold_left + (fun acc elt ->acc^" "^elt) + "" + list + in + if s="" then + "" + else + String.sub s 1 (String.length s - 1 ) + +let resultOptionToBool = function + | None -> false + | Some _ -> true + + +let isIdCorrect s = + resultOptionToBool (Regexp.string_match (Regexp.regexp "^[a-z0-9_-]+$") s 0) + +let isTitleCorrect s = + (resultOptionToBool (Regexp.string_match (Regexp.regexp "^[^ \t]") s 0)) && + (resultOptionToBool (Regexp.string_match (Regexp.regexp ".*[^ \t]$") s 0)) + + +module H = Tyxml_js.Html +open Lwt.Infix + +(*getting html elements*) +let previous_id = match (arg "id") with + | exception Not_found -> "" + | s -> s +let save_element = getElementById "save" +let identifier_input = + get (getElementById_coerce "identifier" CoerceTo.input) +let title_input = + get (getElementById_coerce "title" CoerceTo.input) +let authors_input = + get (getElementById_coerce "authors" CoerceTo.input) +let required_input = + get (getElementById_coerce "required" CoerceTo.input) +let trained_input = + get (getElementById_coerce "focus" CoerceTo.input) +let description_textarea = + get (getElementById_coerce "description" CoerceTo.textarea) +let difficulty_select = + get (getElementById_coerce "difficulty" CoerceTo.select) + +let backward_input =get (getElementById_coerce "backward" CoerceTo.input) +let forward_input = get (getElementById_coerce "forward" CoerceTo.input) +let previous_state = + match get_editor_state previous_id with + | exception Not_found -> None + | state->Some state + +(*filling the form eventualy *) +let _ = match previous_state with + | None -> () + | Some state -> identifier_input##.value := Js.string previous_id; + title_input##.value := Js.string state.metadata.title; + let s = List.fold_left + (fun acc (a, b) -> acc ^ a ^ " <" ^ b ^ ">, ") + "" + state.metadata.author in + authors_input##.value := + if s="" then Js.string "" + else + Js.string (String.sub s 0 (String.length s - 2)); + + required_input##.value := Js.string + (string_with_spaces + state.metadata.requirements); + trained_input##.value := Js.string + (string_with_spaces + state.metadata.focus); + setInnerHtml + description_textarea + (match state.metadata.short_description with + None -> "" + |Some s -> s); + + difficulty_select##.value := Js.string (string_of_float state.metadata.stars); + + backward_input##.value := Js.string + (string_with_spaces + state.metadata.backward); + + forward_input##.value := Js.string + (string_with_spaces + state.metadata.forward) + + +(* handling the save of metadata *) +let store metadata = + let state = + match get_editor_state previous_id with + | exception Not_found -> new_state metadata + | e -> + {exercise=e.exercise ;metadata} + in + update_index state + +let id_error = getElementById "id_error" +let title_error = getElementById "title_error" + +let _ = + save_element##.onclick := handler (fun _ -> + let id = getString identifier_input + and title = getString title_input + and short_description = Some (getString description_textarea) + and stars = getFloat difficulty_select + and string_parser string = Regexp.split + (Regexp.regexp " ") + string + in + let requirements = string_parser (Js.to_string required_input##.value) + and focus = string_parser (Js.to_string trained_input##.value) + and backward = string_parser (Js.to_string backward_input##.value) + and forward = string_parser (Js.to_string forward_input##.value) + and authors = + if String.trim (Js.to_string authors_input##.value) = "" then [] + else + Regexp.split (Regexp.regexp ", ?") (Js.to_string authors_input##.value) + |> List.map @@ fun s -> + match Regexp.string_match + (Regexp.regexp "^([^<>]+) <([^<>]*)>$") s 0 with + (* TODO Keep up-to-date with static/editor/new-exercise.html *) + | Some res -> let odflt = (function Some s -> s | None -> "") in + String.trim @@ odflt @@ Regexp.matched_group res 1, + String.trim @@ odflt @@ Regexp.matched_group res 2 + | None -> + Dom_html.window##alert (Js.string "Incorrect value for the authors field"); + failwith "bad syntax" + in + let metadata={requirements;focus;backward;forward; + kind= Exercise;title; id=Some id; + author=authors;short_description;stars} + and id_correct = isIdCorrect id + and id_unique = idUnique id || (previous_id = id) + and title_correct = isTitleCorrect title + and previous_title = match previous_state with + None -> None + | Some state -> + Some state.metadata.title + in + let title_unique = titleUnique title || previous_title = (Some title) in + (if not id_correct then + setInnerHtml id_error [%i"Incorrect identifier: an identifier \ + can't be empty, \ + and only lower case letters, numerals, dashes \ + and underscores are allowed"] + else if not id_unique then + setInnerHtml id_error [%i"This identifier is already used, \ + please choose another one"] + else + setInnerHtml id_error ""); + (if not title_correct then + setInnerHtml title_error [%i"Incorrect title: a title can't be empty, \ + or begin or end with a space or a tab"] + else if not title_unique then + setInnerHtml title_error + [%i"This title is already used, please choose another one"] + else + setInnerHtml title_error ""); + if id_correct && title_correct && id_unique && title_unique then + begin + store metadata; + Dom_html.window##.location##assign + (Js.string (api_server ^ "/editor.html#id=" ^ id)); + end; + Js._true); diff --git a/src/editor/test_spec.ml b/src/editor/test_spec.ml new file mode 100644 index 000000000..f89a5f6ee --- /dev/null +++ b/src/editor/test_spec.ml @@ -0,0 +1,309 @@ +(* This file is part of Learn-OCaml. + * + * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2016-2018 OCamlPro. + * + * The main authors of the editor part is the pfitaxel team see + * https://github.com/pfitaxel/learn-ocaml-editor for more information + * + * Learn-OCaml is distributed under the terms of the MIT license. See the + * included LICENSE file for details. *) + +open Editor_lib + +let string_of_char ch = String.make 1 ch + +let rec to_string_aux char_list =match char_list with + | []-> "" + | c::l -> (string_of_char c) ^ ( to_string_aux l) + +let to_funty str = "[%funty: " ^ str ^ "]" + +(* The tester arg could take into account exceptions/sorted lists/etc. *) +let question_typed ?num question = + let open Learnocaml_data.Editor in + let opt_string param = function + | "" -> "" + | v -> Format.sprintf " ~%s:(%s)" param v + and sampler_args = function + | "" -> "" + | f -> Format.sprintf "fun () -> last ((%s) ())" f + in + let prefix = match num with None -> "" | Some n -> string_of_int n in + match question with + | TestAgainstSpec a -> + (* FIXME *) + "(* Question #" ^ " about " ^ a.name ^ " was not translated\n" + ^ "(TestAgainstSpec not currently supported by the learn-ocaml runtime) *)" + | TestSuite a -> + let name, prot, tester, suite = + a.name, to_funty a.ty, opt_string "test" a.tester, a.suite in + (* Naming convention: [q_name], [q1_name; q2_name] (occurrence 1/3) *) + Format.sprintf "let q%s_%s =@. \ + let prot = %s in@. \ + test_function%s prot@. \ + (lookup_student (ty_of_prot prot) %s)@. \ + %s;;@." + prefix name prot tester name suite + | TestAgainstSol a -> + let name = a.name + and prot = to_funty a.ty + and gen = a.gen + and sampler = opt_string "sampler" (sampler_args a.sampler) + and tester = opt_string "test" a.tester + and suite = a.suite + in + (* Naming convention: [q_name], [q1_name; q2_name] (occurrence 2/3) *) + Format.sprintf "let q%s_%s =@. \ + let prot = %s in@. \ + test_function_against_solution ~gen:(%d)%s%s prot@. \ + \"%s\"@. \ + %s;;@." + prefix name prot gen sampler tester name suite + +(*****************) +(* compile stuff *) +(*****************) + +let test_prel = "open Test_lib\nopen Learnocaml_report;;\n" + +let quality_function = {| +let avoid_thentrue = let already = ref false in fun _ -> + if !already then [] else begin + already := true ; + Learnocaml_report.[ Message ([ Text "* Do not write the following code patterns:"; + Code "[if ... then true else ...; + if ... then false else ...; + if ... then ... else true; + if ... then ... else false]"; Text " +Preferably use Boolean operators (&&), (||), not."], Success ~-4) ] + end + +let check_thentrue e = + Parsetree.( + match e with + | {pexp_desc = Pexp_ifthenelse (_, e1, (Some e2))} -> + begin + match e1 with + | {pexp_desc = Pexp_construct ({Asttypes.txt = (Longident.Lident "false")}, None)} + | {pexp_desc = Pexp_construct ({Asttypes.txt = (Longident.Lident "true")}, None)} -> + avoid_thentrue e1 + | _ -> [] + end @ begin + match e2 with + | {pexp_desc = Pexp_construct ({Asttypes.txt = (Longident.Lident "false")}, None)} + | {pexp_desc = Pexp_construct ({Asttypes.txt = (Longident.Lident "true")}, None)} -> + avoid_thentrue e2 + | _ -> [] + end + | _ -> []) + +let avoid_list1app = let already = ref false in fun _ -> + if !already then [] else begin + already := true ; + Learnocaml_report.[ Message ([ Text "* Do not write:"; + Code "[x] @ l"; + Text ". Preferably write:"; + Code "x :: l"; + Text "."], Success ~-4) ] + end + +let check_list1app e = + Parsetree.( + match e.pexp_desc with + | Pexp_apply (app0, [(_, lst1); _]) -> + (match app0.pexp_desc, lst1.pexp_desc with + | Pexp_ident {Asttypes.txt = app0'}, + Pexp_construct ({Asttypes.txt = (Longident.Lident "::")}, Some lst1') + when List.mem (Longident.flatten app0') [["List"; "append"]; ["@"]] -> + (match lst1'.pexp_desc with + | Pexp_tuple [_; nil0] -> + (match nil0.pexp_desc with + | Pexp_construct ({Asttypes.txt = (Longident.Lident "[]")}, None) -> + avoid_list1app e + | _ -> []) + | _ -> []) + | _ -> []) + | _ -> []) + +let avoid_eqphy = let already = ref false in fun _ -> + if !already then [] else begin + already := true ; + Learnocaml_report.[ Message ([ Text "* Do not use physical equality"; + Code "(==)"; + Text ". Preferably use structural equality"; + Code "(=)"; + Text "."], Success ~-1) ] + end + +let avoid_neqphy = let already = ref false in fun _ -> + if !already then [] else begin + already := true ; + Learnocaml_report.[ Message ([ Text "* Do not use physical inequality"; + Code "(!=)"; + Text ". Preferably use structural inequality"; + Code "(<>)"; + Text "."], Success ~-1) ] + end + +let check_eqphy e = + Parsetree.( + match e.pexp_desc with + | Pexp_ident {Asttypes.txt = Longident.Lident "=="} -> avoid_eqphy e + | _ -> []) + +let check_neqphy e = + Parsetree.( + match e.pexp_desc with + | Pexp_ident {Asttypes.txt = Longident.Lident "!="} -> avoid_neqphy e + | _ -> []) +|} + +let imperative_function = {|let ast_imperative_check ast = + let chk_expr e = + Parsetree.( + match e with + | {pexp_desc = Pexp_sequence _} -> forbid_syntax ";" e + | {pexp_desc = Pexp_while _} -> forbid_syntax "while" e + | {pexp_desc = Pexp_for _} -> forbid_syntax "for" e + | {pexp_desc = Pexp_array _} -> forbid_syntax "array" e + | _ -> [] ) in + let imperative_report = + ast_check_structure + ~on_expression:chk_expr + ast |> List.sort_uniq compare in + if snd (Learnocaml_report.result imperative_report) then + imperative_report + else + []|} + + +let ast_fonction quality imperative = + let fonction = if quality then + quality_function + else + "" in + let fonction = if imperative then + fonction ^ imperative_function + else + fonction ^ "" in + let fonction = fonction ^ "\n\nlet ast_quality ast =" in + let fonction = + if imperative then + fonction ^ {| + let imperative_report = + let tempReport = ast_imperative_check ast in + if tempReport = [] then [] + else (Message ([ Text "Imperative features have been detected:" ], + Success ~-4)) :: tempReport + |} + else + fonction ^ {| + let imperative_report = [] + |} in + let fonction = + if quality then + fonction ^ {| + and report = + let tempReport = ast_check_structure + ~on_expression:(check_thentrue @@@ check_list1app @@@ + check_eqphy @@@ check_neqphy) + ast |> List.sort_uniq compare in + if tempReport = [] then [] + else (Message ([Text "Unwanted code patterns have been detected:"], + Failure)) :: tempReport + |} + else fonction ^ " and report = []" in + let fonction = fonction ^ {| + in if imperative_report = [] && report = [] + then [ Message ([ Text "OK (no prohibited construction detected)"], Important) ] + else imperative_report @ report;; + + |} in + fonction + +let ast_code quality imperative = +let fonction = + if quality || imperative then + {|Section ([ Text "Code quality:" ], ast_quality code_ast); + |} + else + "" in + fonction + +(* Naming convention: [q_name], [q1_name; q2_name] (occurrence 3/3) *) +(* [cat_question "foo" [42] = "q_foo"] + [cat_question "foo" [42; 42; 42] = "q1_foo @ q2_foo @ q3_foo"] *) +let cat_question name list_qst = + match list_qst with + | [] -> invalid_arg "cat_question" + | [_] -> "q_" ^ name + | _q :: ((_ :: _) as l) -> + List.fold_left (fun (i, acc) _e -> + (i + 1, acc ^ " @ q" ^ string_of_int i ^ "_" ^ name)) + (2, "q1_" ^ name) l + |> snd + +let compile indexed_list = + let tests = test_prel ^ (ast_fonction true true) in + let tests = List.fold_left (fun acc (name, list_qst) -> + acc ^ + Format.sprintf {|let () = set_progress "Q. %s"@.@.|} name ^ + if List.length list_qst > 1 then + List.fold_left (fun (i, acc) qst -> + (i + 1, acc ^ question_typed ~num:i qst ^" \n")) + (1, "") list_qst + |> snd + else List.fold_left (fun acc qst -> + acc ^ question_typed qst ^" \n") + "" list_qst) + tests indexed_list in + let tests = tests ^ init ^ "[\n" ^ ast_code true true in + let tests = + List.fold_left (fun acc (name, list_qst) -> + acc ^ section name (cat_question name list_qst)) + tests indexed_list in + tests ^ " ]" + +(* +module Generate_1 = struct + open Tyxml_js.Html5 + open Learnocaml_common + open Learnocaml_data + open Lwt.Infix + + let ask_name top = + ask_string ~title:"Name" [ pcdata "Enter the function name"] >>= + (fun name -> + let questions = extract_functions (get_answer top) in + match List.assoc_opt name questions with + | None -> Learnocaml_common.alert "No question with this name found"; Lwt.return None + | Some s -> Lwt.return (Some (name,s)) + ) + + let generate_against_solution tuple_opt = + match tuple_opt with + | None -> "" + | Some (name, ty) -> let question = + Editor.TestAgainstSol + {name = name; ty; + gen=10; suite= "[1 @:!! 4 ; 3 @:!! 3 ]"; + tester = ""; sampler = ""} + in + question_typed question + + let generate_test_suite tuple_opt = + match tuple_opt with + | None -> "" + | Some (name, ty) -> let question = + Editor.TestSuite + {name = name; ty; + gen=10; suite= "[1 @:!! 4 ; 3 @:!! 3 ]"; + tester = ""; sampler = ""} + in + question_typed question + + +end + + *) diff --git a/src/editor/test_spec.mli b/src/editor/test_spec.mli new file mode 100644 index 000000000..b35c1c90a --- /dev/null +++ b/src/editor/test_spec.mli @@ -0,0 +1,18 @@ +(* This file is part of Learn-OCaml. + * + * Copyright (C) 2019 OCaml Software Foundation. + * Copyright (C) 2016-2018 OCamlPro. + * + * The main authors of the editor part is the pfitaxel team see + * https://github.com/pfitaxel/learn-ocaml-editor for more information + * + * Learn-OCaml is distributed under the terms of the MIT license. See the + * included LICENSE file for details. *) + +(** [question_typed] is used by [compile] + @return the compiled string of a single question *) +val question_typed : ?num:int -> Learnocaml_data.Editor.test_qst_untyped -> string + +(** [compile] is used by the "Generate" feature of the test.ml tab + @return the compiled string of all given questions *) +val compile : (string * Learnocaml_data.Editor.test_qst_untyped list) list -> string diff --git a/src/main/learnocaml_main.ml b/src/main/learnocaml_main.ml index 859cfaad6..82c481e9b 100644 --- a/src/main/learnocaml_main.ml +++ b/src/main/learnocaml_main.ml @@ -155,6 +155,14 @@ module Args = struct value & opt dir default & info ["contents-dir"] ~docv:"DIR" ~doc: "directory containing the base learn-ocaml app contents" + let editor_dir = + let default = + readlink (Filename.dirname (Filename.dirname (Sys.executable_name)) + /"share"/"learn-ocaml"/"editor") + in + value & opt dir default & info ["editor-dir"] ~docv:"DIR" ~doc: + "directory containing the learn-ocaml-editor app contents" + let enable opt doc = value & vflag None [ Some true, info ["enable-"^opt] ~doc:("Enable "^doc); @@ -173,6 +181,10 @@ module Args = struct "the 'Lessons' tab (enabled by default if the repository contains a \ $(i,lessons) directory)" + let editor = enable "editor" + "the 'Editor' tab (enabled by default), for users connected with a \ + teacher token, or all users if static deployment is used." + let exercises = enable "exercises" "the 'Exercises' tab (enabled by default if the repository contains an \ $(i,exercises) directory)" @@ -191,8 +203,10 @@ module Args = struct type t = { contents_dir: string; + editor_dir: string; try_ocaml: bool option; lessons: bool option; + editor: bool option; exercises: bool option; playground: bool option; toplevel: bool option; @@ -201,10 +215,10 @@ module Args = struct 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 } + contents_dir editor_dir try_ocaml lessons editor exercises playground toplevel base_url + = { contents_dir; editor_dir; try_ocaml; lessons; editor; exercises; playground; toplevel; base_url } in - Term.(const apply $contents_dir $try_ocaml $lessons $exercises $playground $toplevel $base_url) + Term.(const apply $contents_dir $editor_dir $try_ocaml $lessons $editor $exercises $playground $toplevel $base_url) let repo_conf = let apply repo_dir exercises_filtered jobs = @@ -304,17 +318,22 @@ let main o = else Lwt.return_none in let generate () = + let copy title src_dir = + Printf.printf "Updating %s at %s\n%!" title o.app_dir; + Lwt.catch + (fun () -> Lwt_utils.copy_tree src_dir o.app_dir) + (function + | Failure _ -> + Lwt.fail_with @@ + Printf.sprintf "Failed to copy %s app contents from %s" + title (readlink src_dir) + | e -> Lwt.fail e) + in 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 () -> + (copy "learn-ocaml" o.builder.Builder.contents_dir >>= fun () -> + (if o.builder.Builder.editor <> Some false then + copy "learn-ocaml-editor" o.builder.Builder.editor_dir + else Lwt.return_unit) >>= 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 @@ -372,6 +391,7 @@ let main o = \ enablePlayground: %b,\n\ \ enableLessons: %b,\n\ \ enableExercises: %b,\n\ + \ enableEditor: %b,\n\ \ enableToplevel: %b,\n\ \ baseUrl: \"%s\"\n\ }\n" @@ -379,9 +399,11 @@ let main o = (playground_ret <> None) (lessons_ret <> None) (exercises_ret <> None) + (o.builder.Builder.editor <> Some false) (o.builder.Builder.toplevel <> Some false) o.builder.Builder.base_url >>= fun () -> Lwt.return (tutorials_ret <> Some false && exercises_ret <> Some false))) + (* TODO: double-check if a condition is not missing in previous line *) else Lwt.return true in diff --git a/src/repo/learnocaml_exercise.ml b/src/repo/learnocaml_exercise.ml index 74f329e2a..0ce4baded 100644 --- a/src/repo/learnocaml_exercise.ml +++ b/src/repo/learnocaml_exercise.ml @@ -21,6 +21,8 @@ type t = dependencies : string list; } +(* XXX: a change to this type may require another change in editor_lib.ml *) + let encoding = let open Json_encoding in conv @@ -192,7 +194,7 @@ module File = struct let depend = { key = "depend.txt" ; ciphered = false ; - decode = (fun v -> Some v) ; + decode = (fun v -> Some v) ; (* XXX should this produce None sometimes? *) encode = (function | None -> "" (* no `depend` ~ empty `depend` *) | Some txt -> txt) ; @@ -289,7 +291,7 @@ module File = struct * return () * with _ -> return () * in *) - let descrs = ref [] in + let descrs : (string option * string) list ref = ref [] in let rec read_descr lang = function | [] -> (* If there are no extensions to try, we just give up. *) @@ -314,7 +316,7 @@ module File = struct | Some raw -> (* If it does, we apply the function, add the description to [!descrs] and return. *) - descrs := (lang, f raw) :: !descrs; + descrs := (lang, f raw) :: !descrs ; return () in let override_url = function @@ -339,15 +341,24 @@ module File = struct in let read_descrs () = let langs = [] in + (* XXX: Really [] ? *) 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; + let res = (List.map (function (None, v) -> "", v | (Some l, v) -> l, v) !descrs) in + (* let res' = descr.encode res in + let stack_overflow = descr.decode res' in () *) + + (* Also reproducible with: + let html = (* markdown_to_html md (* length = 13711 *) *) + String.init 14000 (fun n -> if n mod 2 = 0 then 'A' else ' ') in + let res' = descrs_to_string [("", html)] (* length = 13857 *) in + let stack_overflow = descrs_from_string res' in () *) + + ex := set descr res !ex; return () in join diff --git a/src/state/learnocaml_api.ml b/src/state/learnocaml_api.ml index 63043e4c3..2cff8e6d5 100644 --- a/src/state/learnocaml_api.ml +++ b/src/state/learnocaml_api.ml @@ -396,10 +396,12 @@ module Server (Json: JSON_CODEC) (Rh: REQUEST_HANDLER) = struct | `GET, ( ["index.html"] - | ["exercise.html"] + | ["exercise.html"] | ["playground.html"] | ["student-view.html"] | ["description.html"] + | ["new-exercise.html"] + | ["editor.html"] | ["partition-view.html"] | ("js"|"fonts"|"icons"|"css"|"static") :: _ as path), _ -> diff --git a/src/state/learnocaml_data.ml b/src/state/learnocaml_data.ml index 39ce6f6d0..0ee371083 100644 --- a/src/state/learnocaml_data.ml +++ b/src/state/learnocaml_data.ml @@ -1294,3 +1294,108 @@ module Playground = struct end end + +module Editor = struct + + + type type_question= Suite | Solution | Spec ;; + + + type test_qst_untyped = + | TestAgainstSol of + { name: string + ; ty: string + ; gen: int + ; suite: string + ; tester: string + ; sampler : string } + | TestAgainstSpec of + { name: string + ; ty: string + ; gen: int + ; suite: string + ; spec : string + ; tester: string + ; sampler: string } + | TestSuite of + { name: string; + ty: string; + suite: string; + tester :string };; + + + +type exercise = + { id : string ; + prelude : string ; + template : string ; + descr : string ; + prepare : string ; + test : string ; + solution : string ; + max_score : int ; + } + + + let exercise_encoding = + let open Json_encoding in + conv + (fun { id; prelude; template; descr; prepare; test; solution; max_score } -> + id, prelude, template, descr, prepare, test, solution, max_score) + (fun (id, prelude, template, descr, prepare, test, solution, max_score) -> + { id ; prelude ; template ; descr ; prepare ; test ; solution ; max_score }) + (obj8 + (req "id" string) + (req "prelude" string) + (req "template" string) + (req "descr" string) + (req "prepare" string) + (req "test" string) + (req "solution" string) + (req "max_score" int)) + + + + type editor_state = + { exercise : exercise; + metadata : Exercise.Meta.t;} + + let editor_state_enc = + J.conv + (fun {exercise; metadata } -> + (exercise, metadata)) + (fun (exercise, metadata) -> + {exercise; metadata }) + (J.obj2 + (J.req "exercise" exercise_encoding) + (J.req "metadata" Exercise.Meta.enc)) + + type editor_template = + { name : string; + template : string} + + let editor_template_enc = + J.conv + (fun {name; template } -> + (name, template)) + (fun (name, template) -> + {name; template}) + (J.obj2 + (J.req "name" J.string) + (J.req "template" J.string)) + + + module IMap = struct + + include Map.Make(struct type t = int let compare (x:t) y = compare x y end) + + (** Useful for serialization *) + let string_of_int_map iv = + + fold (fun i v sv -> SMap.add (string_of_int i) v sv) iv SMap.empty + + let int_of_string_map iv = + SMap.fold (fun s v iv -> add (int_of_string s) v iv) iv empty + end + +end diff --git a/src/state/learnocaml_data.mli b/src/state/learnocaml_data.mli index 2408c5eec..6b9694720 100644 --- a/src/state/learnocaml_data.mli +++ b/src/state/learnocaml_data.mli @@ -438,3 +438,54 @@ module Playground : sig end end + +module Editor : sig + + type type_question = Suite | Solution | Spec + + type test_qst_untyped = + | TestAgainstSol of + { name: string + ; ty: string + ; gen: int + ; suite: string + ; tester: string + ; sampler: string } + | TestAgainstSpec of + { name: string + ; ty: string + ; gen: int + ; suite: string + ; spec : string + ; tester: string + ; sampler: string } + | TestSuite of + { name: string + ; ty: string + ; suite: string + ; tester: string } ;; + + type exercise = + { id : string ; + prelude : string ; + template : string ; + descr : string ; + prepare : string ; + test : string ; + solution : string ; + max_score : int ; + } + + type editor_state = + { exercise : exercise; + metadata : Exercise.Meta.t; } + + val editor_state_enc : editor_state Json_encoding.encoding + + type editor_template = + { name : string; + template : string} + + val editor_template_enc : editor_template Json_encoding.encoding + +end diff --git a/src/toplevel/dune b/src/toplevel/dune index 67b1005a6..3c27b5556 100644 --- a/src/toplevel/dune +++ b/src/toplevel/dune @@ -20,6 +20,8 @@ toploop_results ocplib-ocamlres.runtime embedded_cmis + grading + learnocaml_ppx_metaquot_lib learnocaml_toplevel_worker_messages) (modules Learnocaml_toplevel_worker_main) (preprocess (pps js_of_ocaml.ppx)) diff --git a/src/toplevel/learnocaml_toplevel.ml b/src/toplevel/learnocaml_toplevel.ml index 20fdfa239..1e28819ce 100644 --- a/src/toplevel/learnocaml_toplevel.ml +++ b/src/toplevel/learnocaml_toplevel.ml @@ -200,15 +200,18 @@ let execute_phrase top ?timeout content = let execute top = Learnocaml_toplevel_input.execute top.input +let execute_test top = + Learnocaml_toplevel_output.get_blocks top.output + let go_backward top = Learnocaml_toplevel_input.go_backward top.input let go_forward top = Learnocaml_toplevel_input.go_forward top.input -let check top code = +let check ?grading top code = protect_execution top @@ fun () -> - Learnocaml_toplevel_worker_caller.check top.worker code + Learnocaml_toplevel_worker_caller.check ?grading top.worker code let set_checking_environment top = protect_execution top @@ fun () -> diff --git a/src/toplevel/learnocaml_toplevel.mli b/src/toplevel/learnocaml_toplevel.mli index f2cb5a8f2..592dfd0ed 100644 --- a/src/toplevel/learnocaml_toplevel.mli +++ b/src/toplevel/learnocaml_toplevel.mli @@ -125,8 +125,11 @@ val load: ?message: string -> string -> bool Lwt.t -(** Parse and typecheck a given source code. *) -val check: t -> string -> unit Toploop_results.toplevel_result Lwt.t +(** Parse and typecheck a given source code. + + @param grading + Load [Embedded_grading_cmis] and ppx-metaquot in the checker toploop. *) +val check: ?grading:bool -> t -> string -> unit Toploop_results.toplevel_result Lwt.t (** Freezes the environment for future calls to {!check}. *) val set_checking_environment: t -> unit Lwt.t @@ -158,7 +161,9 @@ val scroll: 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 execute_test: t -> string + (** Go backward in the input's history. This is equivalent to pressing [Up] when the toplevel is focused. *) val go_backward: t -> unit diff --git a/src/toplevel/learnocaml_toplevel_output.ml b/src/toplevel/learnocaml_toplevel_output.ml index 642300e43..7088f3fad 100644 --- a/src/toplevel/learnocaml_toplevel_output.ml +++ b/src/toplevel/learnocaml_toplevel_output.ml @@ -364,3 +364,11 @@ let oldify output = let format_ocaml_code code = pretty_html (fst (prettify_ocaml code)) + +let get_blocks output = + let rec process = function + | [] -> "" + | (Answer (s,_,_,_)) :: suite -> process suite ^ s + | (Phrase (_,l)) :: suite -> process suite ^ (process (!l)) (* OK? *) + | b :: suite->(process suite) in + process output.blocks diff --git a/src/toplevel/learnocaml_toplevel_output.mli b/src/toplevel/learnocaml_toplevel_output.mli index e30d7bff7..29bcd136e 100644 --- a/src/toplevel/learnocaml_toplevel_output.mli +++ b/src/toplevel/learnocaml_toplevel_output.mli @@ -101,3 +101,5 @@ val output_warning : ?phrase: phrase -> output -> Toploop_results.warning -> uni (** Format OCaml code in the style of {!output_code}. *) val format_ocaml_code : string -> [> `Span | `PCDATA ] Tyxml_js.Html5.elt list + +val get_blocks : output -> string diff --git a/src/toplevel/learnocaml_toplevel_worker_caller.ml b/src/toplevel/learnocaml_toplevel_worker_caller.ml index a39e7d82a..44c2f455e 100644 --- a/src/toplevel/learnocaml_toplevel_worker_caller.ml +++ b/src/toplevel/learnocaml_toplevel_worker_caller.ml @@ -238,8 +238,8 @@ 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 ?(grading = false) worker code = + post worker @@ Check (code, grading) let set_checking_environment worker = post worker @@ Set_checking_environment diff --git a/src/toplevel/learnocaml_toplevel_worker_caller.mli b/src/toplevel/learnocaml_toplevel_worker_caller.mli index 6a4f24692..cfe760056 100644 --- a/src/toplevel/learnocaml_toplevel_worker_caller.mli +++ b/src/toplevel/learnocaml_toplevel_worker_caller.mli @@ -41,14 +41,13 @@ val create: unit -> t Lwt.t -(** Parse and typecheck a given source code +(** Parse and typecheck a given source code. + @param grading + Load [Embedded_grading_cmis] and ppx-metaquot in the checker toploop. @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 - + where [err] contains the error message otherwise. *) + val check: ?grading:bool -> t -> string -> unit 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 diff --git a/src/toplevel/learnocaml_toplevel_worker_main.ml b/src/toplevel/learnocaml_toplevel_worker_main.ml index 03d0eeacc..dfaf93360 100644 --- a/src/toplevel/learnocaml_toplevel_worker_main.ml +++ b/src/toplevel/learnocaml_toplevel_worker_main.ml @@ -10,6 +10,7 @@ open Js_of_ocaml open Learnocaml_toplevel_worker_messages let debug = ref false +let grading = ref false let (>>=) = Lwt.bind @@ -138,6 +139,8 @@ let iter_option f o = match o with | None -> () | Some o -> f o let checking_environment = ref !Toploop.toplevel_env +let setup_ppx = lazy (Ast_mapper.register "ppx_metaquot" Ppx_metaquot.expander) + let handler : type a. a host_msg -> a return Lwt.t = function | Set_checking_environment -> checking_environment := !Toploop.toplevel_env ; @@ -207,13 +210,14 @@ let handler : type a. a host_msg -> a return Lwt.t = function !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 ; - let result = Toploop_ext.check code in - Toploop.toplevel_env := saved ; - unwrap_result result - + | Check (code, grading_cmi_and_ppx_meta) -> + let saved = !Toploop.toplevel_env in + Toploop.toplevel_env := !checking_environment ; + if grading_cmi_and_ppx_meta then (grading := true; Lazy.force setup_ppx) ; + let result = Toploop_ext.check code in + Toploop.toplevel_env := saved ; + unwrap_result result + let ty_of_host_msg : type t. t host_msg -> t msg_ty = function | Init -> Unit | Reset -> Unit @@ -240,13 +244,25 @@ let () = Lwt.return_unit in let path = "/worker_cmis" in + (* we don't use + [OCamlRes.Res.merge Embedded_cmis.root Embedded_grading_cmis.root] + because we don't want to unconditionally load [Embedded_grading_cmis] *) + let root1 = Embedded_cmis.root in + let root2 = Embedded_grading_cmis.root in Sys_js.mount ~path (fun ~prefix:_ ~path -> - match OCamlRes.Res.find (OCamlRes.Path.of_string path) Embedded_cmis.root with + match OCamlRes.Res.find (OCamlRes.Path.of_string path) root1 with | cmi -> Js.Unsafe.set cmi (Js.string "t") 9 ; (* XXX hack *) Some cmi - | exception Not_found -> None) ; + | exception Not_found -> + if !grading then + match OCamlRes.Res.find (OCamlRes.Path.of_string path) root2 with + | cmi -> + Js.Unsafe.set cmi (Js.string "t") 9 ; (* XXX hack *) + Some cmi + | exception Not_found -> None + else None) ; Config.load_path := [ path ] ; Toploop_jsoo.initialize (); Hashtbl.add Toploop.directive_table diff --git a/src/toplevel/learnocaml_toplevel_worker_messages.mli b/src/toplevel/learnocaml_toplevel_worker_messages.mli index 1906d2520..6f7c33a21 100644 --- a/src/toplevel/learnocaml_toplevel_worker_messages.mli +++ b/src/toplevel/learnocaml_toplevel_worker_messages.mli @@ -19,8 +19,8 @@ type _ host_msg = | Set_debug : bool -> unit host_msg | Register_callback : string * int -> unit host_msg | Set_checking_environment : unit host_msg - | Check : string -> unit host_msg - + | Check : string * bool -> unit host_msg + type _ msg_ty = | Unit : unit msg_ty | Bool : bool msg_ty diff --git a/static/Makefile b/static/Makefile index 823a18a52..40d0ebcb3 100644 --- a/static/Makefile +++ b/static/Makefile @@ -5,6 +5,7 @@ all: icons dune FILES = $(wildcard \ js/*.js\ js/ace/*.js\ + js/jszip/*.js\ *.html\ icons/*.svg\ icons/*.gif\ @@ -13,9 +14,13 @@ FILES = $(wildcard \ css/*.css\ ) $(shell find js/mathjax ! -type d) +EDITOR_FILES = $(wildcard \ + editor/*.html\ +) + ALWAYS: file-list: icons ALWAYS - @echo ${FILES} >$@ + @echo ${FILES} ${EDITOR_FILES} >$@ icons: @${MAKE} -C icons @@ -26,6 +31,7 @@ dune: file-list @echo ' (package learn-ocaml)' >>$@ @echo ' (files' >>$@ @$(foreach f,$(FILES),echo ' ($f as ${addprefix www/,$f})' >>$@;) + @$(foreach f,$(EDITOR_FILES),echo ' ($f as $f)' >>$@;) @echo ' )' >>$@ @echo ')' >>$@ diff --git a/static/css/learnocaml_editor.css b/static/css/learnocaml_editor.css new file mode 100644 index 000000000..3d7b173c4 --- /dev/null +++ b/static/css/learnocaml_editor.css @@ -0,0 +1,1502 @@ +*{ +box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + background: #666; +overflow: hidden; +} + +#learnocaml-main-loading { + position: absolute; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; +} +#learnocaml-main-exercise-list > .patterns{ + display :flex; + flex-direction: row; + position: relative; + z-index: 999; + background: linear-gradient(to top, #aaa 0px, #eee 10px, #ddd 100%); + color: black; + text-decoration: none; + height: 90px; +} +#learnocaml-main-exercise-list > .patterns > h1{ + margin: 0 0 10px 0; + padding: 0; + font-size: 18px; + font-weight: bold; + left:10px; top:10px; + position:absolute; +} + +#learnocaml-main-exercise-list > .patterns > .quality{ + left:10px; + position: absolute; + width : 100%; +} +#learnocaml-main-exercise-list > .patterns > .quality > p{ + left :40px; + top: 20px; + position: absolute; +} +#quality_box{ + top: 34px; + position:absolute; +} +#learnocaml-main-exercise-list > .patterns > .imperative{ + left:10px; + position: absolute; + width: 100%; + top: 20px; +} +#learnocaml-main-exercise-list > .patterns > .imperative > p{ + left :40px; + top: 20px; + position: absolute; +} +#imperative_box{ + top:34px; + position:absolute; +} + +/* -------------------- toolbar ----------------------------------- */ +#learnocaml-exo-toolbar { + position: absolute; + left: 0; top: 0; + color: #fff; + z-index: 1004; + border-bottom: 1px #eee solid; + display: flex; + background: #222; +} +#learnocaml-exo-toolbar a { + text-decoration: none; + color: #fff; +} +#learnocaml-exo-toolbar > button { + border-left: 1px #eee solid !important; + padding: 0 10px 0 10px; + margin: 0; + background: none; + border: none; + flex: 0; + position: relative; + display: block; +} +#learnocaml-exo-toolbar > button > * { + padding: 5px; + color: #eee; + text-align: center; +} +#learnocaml-exo-toolbar > .special_grade{ + background:#aaa; +} +#learnocaml-exo-toolbar::before { + z-index: 1004; + position: absolute; + left: 0px; height: 5px; width: 100%; + background: linear-gradient(to bottom, rgba(0,0,0,0.4) 0px, rgba(0,0,0,0) 5px); + content:""; +} +@media (max-width: 549px) { + #learnocaml-exo-toolbar { + height: 40px; + } + #learnocaml-exo-toolbar::before { + top: 41px; + } + #learnocaml-exo-toolbar > button > .label { + display: none; + } +} +@media (min-width: 550px) { + #learnocaml-exo-toolbar { + height: 60px; + } + #learnocaml-exo-toolbar::before { + top: 61px; + } +} +/* -------------------- tabs and tab buttons ---------------------- */ +#learnocaml-exo-tab-buttons { + position: absolute; + z-index: 999; + display: flex; + height: 40px; +} +#learnocaml-exo-tab-buttons > button { + flex: 1; + padding: 0; + border: none; + color: #333; + background: #eee; + position: relative; + z-index: 1000; +} +#learnocaml-exo-tab-buttons > button:not(:first-child) { + border-left: 1px #333 solid; +} +#learnocaml-exo-tab-buttons > button { + border-bottom: 1px #333 solid; +} +#learnocaml-exo-tab-buttons > button[disabled]::before { + display: none; +} +#learnocaml-exo-tab-buttons > button:not([disabled])::after { + position: absolute; + left:0; right:0; bottom:0; top:0; + content:""; + z-index: 1009; + background: linear-gradient(to top, rgba(0,0,0,0.4) 0, transparent 5px); +} +#learnocaml-exo-tab-buttons > button[disabled]::after { + position: absolute; + left:0; right:0; bottom:0; top:0; + content:""; + z-index: 1009; + background: rgba(0,0,0,0.4); +} +#learnocaml-exo-tabs > * { + position: absolute; + z-index: 997; + background: #eee; + padding: 0; + opacity: 0; +} +#learnocaml-exo-tabs > *.front-tab { + z-index: 998; + opacity: 1; +} +/* -------------------- two columns mode -------------------------- */ +@media (min-width: 1200px) { + #learnocaml-exo-toolbar { + width: 800px; + } + #learnocaml-exo-tab-editor::after { + position: absolute; + left: 800px; top: -61px; bottom: 0; width: 10px; + background: linear-gradient(to right, #fff 0px, #fff 1px, rgba(0,0,0,0.6) 1px, rgba(0,0,0,0) 10px); + content:""; + } + #learnocaml-exo-button-editor { + display: none; + } + #learnocaml-exo-tab-buttons { + left: 800px; right: 0px; top: 0px; + } + #learnocaml-exo-button-editor.front-tab ~ #learnocaml-exo-button-toplevel { + border-bottom: none; + background: #bbb; + } + #learnocaml-exo-button-editor.front-tab ~ #learnocaml-exo-button-toplevel::after { + display: none; + } + #learnocaml-exo-tab-editor.front-tab ~ #learnocaml-exo-tab-toplevel { + z-index: 998; + opacity: 1; + } + + #learnocaml-exo-button-editor.front-tab ~ #learnocaml-exo-button-template { + border-bottom: none; + background: #bbb; + } + #learnocaml-exo-button-editor.front-tab ~ #learnocaml-exo-button-template::after { + display: none; + } + #learnocaml-exo-tab-editor.front-tab ~ #learnocaml-exo-tab-template { + z-index: 998; + opacity: 1; + } + + #learnocaml-exo-button-editor.front-tab ~ #learnocaml-exo-button-test { + border-bottom: none; + background: #bbb; + } + #learnocaml-exo-button-editor.front-tab ~ #learnocaml-exo-button-test::after { + display: none; + } + #learnocaml-exo-tab-editor.front-tab ~ #learnocaml-exo-tab-test { + z-index: 998; + opacity: 1; + } + + #learnocaml-exo-button-editor.front-tab ~ #learnocaml-exo-button-testhaut { + border-bottom: none; + background: #bbb; + } + #learnocaml-exo-button-editor.front-tab ~ #learnocaml-exo-button-testhaut::after { + display: none; + } + #learnocaml-exo-tab-editor.front-tab ~ #learnocaml-exo-tab-testhaut { + z-index: 998; + opacity: 1; + } + + #learnocaml-exo-tabs > * { + left: 800px; top: 40px; right: 0px; bottom: 0px; + } + #learnocaml-exo-tabs > #learnocaml-exo-tab-editor { + width: 800px; left: 0; bottom: 0; top: 61px; + z-index: 1000; + opacity: 1; + } +} +/* -------------------- one column mode --------------------------- */ +@media (max-width: 1199px) { + #learnocaml-exo-toolbar { + right: 0px; + } + #learnocaml-exo-tab-buttons { + left: 0; right: 0px; + } + #learnocaml-exo-tabs > * { + position: absolute; + left: 0; right: 0px; bottom: 0px; + } + #learnocaml-exo-tab-editor > .pane { + margin-top: 5px; + } + #learnocaml-exo-tab-editor::after { + position: absolute; + z-index: 1005; + left: 0; top: 0; height: 10px; width: 100%; + content: ""; + background: linear-gradient(to bottom, #444 0, #444 5px, + rgba(0,0,0,0.5) 5px, transparent 10px) ; + } +} +@media (min-width: 550px) and (max-width: 1199px) { + #learnocaml-exo-tab-buttons { + top: 60px; + } + #learnocaml-exo-tabs > * { + top: 100px; + } +} +@media (max-width: 549px) { + #learnocaml-exo-tab-buttons { + top: 40px; + } + #learnocaml-exo-tabs > * { + top: 80px; + } +} +/* -------------------- editor tab -------------------------------- */ +#learnocaml-exo-tab-buttons > #learnocaml-exo-button-editor { + background: #eee; + color: black; +} +#learnocaml-exo-tab-editor > .pane { + position: absolute; + left: 0; top: 0; bottom: 40px; width: 100%; + background: #666; + color: #fff; + z-index: 1002; +} +#learnocaml-exo-tab-editor > .buttons { + position: absolute; + left: 0; bottom: 0px; width: 100%; height: 40px; + background: linear-gradient(to bottom, #666 0px, #444 10px, #222 60px); + color: #fff; + line-height: 40px; + display: flex; + flex-direction: row; + z-index: 1003; +} +#learnocaml-exo-tab-editor > .buttons::after { + position: absolute; + bottom: 40px; left: 0px; height: 5px; width: 100%; + background: linear-gradient(to top, rgba(0,0,0,0.4) 0px, rgba(0,0,0,0) 5px); + content:""; +} +#learnocaml-exo-tab-editor > .buttons > button { + flex: 1; + background: none; + border: none; + color: #eee; + text-shadow: 2px 2px 5px rgba(0,0,0,0.4); + border-top: 1px #eee solid; + position: relative; + padding: 0; +} +#learnocaml-exo-tab-editor > .buttons > button:not(:first-child) { + border-left: 1px #eee solid; +} +@media (max-width: 550px) { + #learnocaml-exo-tab-editor > .buttons > button > .label { + display: none; + } +} +/*--------------------- template ---------------------------------- */ +#learnocaml-exo-tab-buttons > #learnocaml-exo-button-template { + background: #eee; + color: black; +} +#learnocaml-exo-tab-template > .template-pane { + position: absolute; + left: 0; top: 0; bottom: 40px; width: 100%; + background: #666; + color: #fff; + z-index: 1002; +} +#learnocaml-exo-tab-template > .buttons { + position: absolute; + left: 0; bottom: 0px; width: 100%; height: 40px; + background: linear-gradient(to bottom, #666 0px, #444 10px, #222 60px); + color: #fff; + line-height: 40px; + display: flex; + flex-direction: row; + z-index: 1003; +} +#learnocaml-exo-tab-template > .buttons::after { + position: absolute; + bottom: 40px; left: 0px; height: 5px; width: 100%; + background: linear-gradient(to top, rgba(0,0,0,0.4) 0px, rgba(0,0,0,0) 5px); + content:""; +} +#learnocaml-exo-tab-template > .buttons > button { + flex: 1; + background: none; + border: none; + color: #eee; + text-shadow: 2px 2px 5px rgba(0,0,0,0.4); + border-top: 1px #eee solid; + position: relative; + padding: 0; +} +#learnocaml-exo-tab-template > .buttons > button:not(:first-child) { + border-left: 1px #eee solid; +} +@media (max-width: 550px) { + #learnocaml-exo-tab-template > .buttons > button > .label { + display: none; + } +} + +/*--------------------- prepare ---------------------------------- */ +#learnocaml-exo-tab-buttons > #learnocaml-exo-button-prepare { + background: #eee; + color: black; +} +#learnocaml-exo-tab-prepare > .prepare-pane { + position: absolute; + left: 0; top: 0; bottom: 40px; width: 100%; + background: #666; + color: #fff; + z-index: 1002; +} +#learnocaml-exo-tab-prepare > .buttons { + position: absolute; + left: 0; bottom: 0px; width: 100%; height: 40px; + background: linear-gradient(to bottom, #666 0px, #444 10px, #222 60px); + color: #fff; + line-height: 40px; + display: flex; + flex-direction: row; + z-index: 1003; +} +#learnocaml-exo-tab-prepare > .buttons::after { + position: absolute; + bottom: 40px; left: 0px; height: 5px; width: 100%; + background: linear-gradient(to top, rgba(0,0,0,0.4) 0px, rgba(0,0,0,0) 5px); + content:""; +} +#learnocaml-exo-tab-prepare > .buttons > button { + flex: 1; + background: none; + border: none; + color: #eee; + text-shadow: 2px 2px 5px rgba(0,0,0,0.4); + border-top: 1px #eee solid; + position: relative; + padding: 0; +} +#learnocaml-exo-tab-prepare > .buttons > button:not(:first-child) { + border-left: 1px #eee solid; +} +@media (max-width: 550px) { + #learnocaml-exo-tab-prepare > .buttons > button > .label { + display: none; + } +} + +/*--------------------- prelude ---------------------------------- */ +#learnocaml-exo-tab-buttons > #learnocaml-exo-button-prelude { + background: #eee; + color: black; +} +#learnocaml-exo-tab-prelude > .prelude-pane { + position: absolute; + left: 0; top: 0; bottom: 40px; width: 100%; + background: #666; + color: #fff; + z-index: 1002; +} +#learnocaml-exo-tab-prelude > .buttons { + position: absolute; + left: 0; bottom: 0px; width: 100%; height: 40px; + background: linear-gradient(to bottom, #666 0px, #444 10px, #222 60px); + color: #fff; + line-height: 40px; + display: flex; + flex-direction: row; + z-index: 1003; +} +#learnocaml-exo-tab-prelude > .buttons::after { + position: absolute; + bottom: 40px; left: 0px; height: 5px; width: 100%; + background: linear-gradient(to top, rgba(0,0,0,0.4) 0px, rgba(0,0,0,0) 5px); + content:""; +} +#learnocaml-exo-tab-prelude > .buttons > button { + flex: 1; + background: none; + border: none; + color: #eee; + text-shadow: 2px 2px 5px rgba(0,0,0,0.4); + border-top: 1px #eee solid; + position: relative; + padding: 0; +} +#learnocaml-exo-tab-prelude > .buttons > button:not(:first-child) { + border-left: 1px #eee solid; +} +@media (max-width: 550px) { + #learnocaml-exo-tab-prelude > .buttons > button > .label { + display: none; + } +} + + +/* -------------------- toplevel tab ------------------------------ */ +#learnocaml-exo-tab-toplevel > .toplevel-pane { + position: absolute; + left: 0; top: 0; bottom: 40px; width: 100%; + z-index: 1002; + margin: 5px 0 0 0; +} +#learnocaml-exo-tab-buttons > #learnocaml-exo-button-toplevel { + background: #eee; +} +#learnocaml-exo-tab-toplevel { + background: #bbb; +} +#learnocaml-exo-tab-toplevel > .buttons { + position: absolute; + left: 0; bottom: 0px; width: 100%; height: 40px; + background: linear-gradient(to bottom, #fff 0px, #ddd 10px, #aaa 60px); + color: #fff; + line-height: 40px; + display: flex; + flex-direction: row; + z-index: 1003; +} +#learnocaml-exo-tab-toplevel > .buttons > button { + flex: 1; + background: none; + border: none; + color: #222; + text-shadow: 2px 2px 5px rgba(0,0,0,0.2); + border-top: 1px #666 solid; + position: relative; + padding: 0; +} +#learnocaml-exo-tab-toplevel > .buttons > button:not(:first-child) { + border-left: 1px #666 solid; +} +@media (max-width: 550px) { + #learnocaml-exo-tab-toplevel > .buttons > button > .label { + display: none; + } +} +#learnocaml-exo-toplevel-pane { + position: absolute; + left: 0; top: 0; bottom: 40px; width: 100%; + z-index: 1002; + display: flex; + flex-direction: column; + margin: 5px 0 0 0; +} +#learnocaml-exo-tab-toplevel::after { + position: absolute; + z-index: 1005; + left: 0; top: 5px; height: 10px; width: 100%; + content: ""; + background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0, transparent 5px) ; +} + +/* -------------- test tab -----------------------------*/ + +#learnocaml-exo-tab-buttons > #learnocaml-exo-button-test { + background: #eee; + color: black; +} +#learnocaml-exo-tab-test > .test-pane { + position: absolute; + left: 0; top: 0; bottom: 40px; width: 100%; + background: #666; + color: #fff; + z-index: 1002; +} + + +#learnocaml-exo-tab-test > .buttons { + position: absolute; + left: 0; bottom: 0px; width: 100%; height: 40px; + background: linear-gradient(to bottom, #666 0px, #444 10px, #222 60px); + color: #fff; + line-height: 40px; + display: flex; + flex-direction: row; + z-index: 1003; +} +#learnocaml-exo-tab-test > .buttons::after { + position: absolute; + bottom: 40px; left: 0px; height: 5px; width: 100%; + background: linear-gradient(to top, rgba(0,0,0,0.4) 0px, rgba(0,0,0,0) 5px); + content:""; +} +#learnocaml-exo-tab-test > .buttons > button { + flex: 1; + background: none; + border: none; + color: #eee; + text-shadow: 2px 2px 5px rgba(0,0,0,0.4); + border-top: 1px #eee solid; + position: relative; + padding: 0; +} +#learnocaml-exo-tab-test > .buttons > button:not(:first-child) { + border-left: 1px #eee solid; +} +@media (max-width: 550px) { + #learnocaml-exo-tab-test > .buttons > button > .label { + display: none; + } +} + +/* templaates of Test tab */ + + /* Dropup Button */ + .dropbtn { + background: none; + border: none; + color: inherit; + width: 100%; + height: 100%; +} + + +/* Dropup content (Hidden by Default) */ +.dropup-content { + display: none; + position: absolute; + bottom: 41px; + background-color: #333 ; + width: 100%; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +/* Links inside the dropup */ +.dropup-content a { + color: #eee; + padding: 12px 16px; + text-decoration: none; + display: block; + text-shadow: 2px 2px 5px rgba(0,0,0,0.4); +} + +/* Change color of dropup links on hover */ +.dropup-content a:hover {background-color: cadetblue ; } + + +.show {display:block;} + +#learnocaml-exo-tab-test > .buttons > div { + flex: 1; + background: none; + border: none; + color: #eee; + text-shadow: 2px 2px 5px rgba(0,0,0,0.4); + border-top: 1px #eee solid; + position: relative; + padding: 0; +} + +.dropup { + position: relative; + display: inline-block; +} + +#learnocaml-exo-tab-test > .buttons > div:not(:first-child) { + border-left: 1px #eee solid; +} +/* end templates 1*/ + +/*template editor */ + +.config-editor { + font-size: 18px; + font-family: 'Inconsolata', monospace; + position: relative !important; + flex-grow: 12; + background: #666; + color: #fff; + z-index: 1002; + width: 80%; + margin: 0 10%; +} + +div.config-editor-overlay > div { + display:flex; + flex-direction:column; + max-width: 100% !important; + +} + +div.config-editor-overlay > div::before, div.config-editor-overlay > div::after { + content: ""; + flex: 0 !important; +} + +/* end template editor */ + +/*all templates */ + +.editor-template{ + color: #eee; + padding: 12px 16px; + text-decoration: none; + display: block; + text-shadow: 2px 2px 5px rgba(0,0,0,0.4); + background-color: #333 ; + +} + +.editor-template:hover{ + background-color: cadetblue; +} + +/* all templates */ + + +/* question tab */ + + +#learnocaml-exo-question-html > div { + border: none; + overflow: auto; + flex: 1 3 auto ; +font-family: 'Fontin', 'Linux Biolinum', sans-serif; +} + +#learnocaml-exo-question-html > div > pre { + background: #ddd; +line-height: 25px; +overflow: auto; +padding: 0 10px; +border-radius : 8px; +} + + +#learnocaml-exo-question-html > div > h2 { + background: #ddd; +line-height: 40px; +} + +#learnocaml-exo-tab-question > .questions-mark { + position: absolute; + left: 0; top: 0; bottom: 50%; width: 100%; + background: #666; + color: #fff; + z-index: 1002; +} + +#learnocaml-exo-tab-question > .questions-html { + position: absolute; + left: 0; top: 50%; bottom: 0px; width: 100%; + background: white; + z-index: 1002; +} + +#learnocaml-exo-tab-question > .questions-html > * { + position: absolute; + padding-left: 20px; + left: 0; top: 0; bottom: 0px; width: 100%; height: 100%; +} + + +#learnocaml-exo-tab-question > .buttons { + position: absolute; + left: 0; bottom: 0px; width: 100%; height: 40px; + background: linear-gradient(to bottom, #666 0px, #444 10px, #222 60px); + color: #fff; + line-height: 40px; + display: flex; + flex-direction: row; + z-index: 1003; +} +#learnocaml-exo-tab-question > .buttons::after { + position: absolute; + bottom: 40px; left: 0px; height: 5px; width: 100%; + background: linear-gradient(to top, rgba(0,0,0,0.4) 0px, rgba(0,0,0,0) 5px); + content:""; +} +#learnocaml-exo-tab-question > .buttons > button { + flex: 1; + background: none; + border: none; + color: #eee; + text-shadow: 2px 2px 5px rgba(0,0,0,0.4); + border-top: 1px #eee solid; + position: relative; + padding: 0; +} +#learnocaml-exo-tab-question > .buttons > button:not(:first-child) { + border-left: 1px #eee solid; +} +@media (max-width: 550px) { + #learnocaml-exo-tab-question > .buttons > button > .label { + display: none; + } +} + +/* -------------------- report tab -------------------------------- */ +#learnocaml-exo-tab-report { + border: none; + overflow: hidden; +} +#learnocaml-exo-button-report > .score { + padding: 0 5px; + float: right; +} +#learnocaml-exo-tab-report > iframe { + border: none; + overflow: auto; + width: 100%; + height: 100%; +} +@media (max-width: 550px) { + #learnocaml-exo-button-report > .score { + position: absolute; + right: 0; left: 0; top: 0; bottom: 0; + display: block; + line-height: 40px; + background: inherit; + } +} +#learnocaml-exo-tab-buttons > #learnocaml-exo-button-report.success { + background: #0a0; + color: white; +} +#learnocaml-exo-tab-buttons > #learnocaml-exo-button-report.failure { + background: #b00; + color: white; +} +#learnocaml-exo-tab-buttons > #learnocaml-exo-button-report.partial { + background: #e80; + color: white; +} +/* -------------------- text tab ---------------------------------- */ +#learnocaml-exo-tab-text { + display: flex; + flex-direction: column; + overflow: hidden; +} +#learnocaml-exo-tab-text > h1 { + flex: 0 0 auto; + background: #222; + color: #eee; + font-size: 20px; + line-height: 22px; + margin: 0; + padding: 10px; + display: block; + font-weight: normal; + position: relative; +} +#learnocaml-exo-tab-text > h1:first-child { + margin-top: 5px; +} +#learnocaml-exo-tab-text > h1 > button { + float: right; + border: none; + border-left: 1px #eee solid; + color: #eee; + background: none; + margin: -10px; + padding: 10px; + font-size: 20px; + line-height: 22px; +} +#learnocaml-exo-tab-text > iframe { + border: none; + overflow: auto; + flex: 1 3 auto ; +} +#learnocaml-exo-tab-text > pre.toplevel-code { + flex: 0 1 auto; + max-height: 45%; + background: #666; + margin: 0; + padding: 5px 10px 5px 5px; + overflow: auto; +} +#learnocaml-exo-tab-text > h1::after { + position: absolute; + left: 0px; bottom: -5px; width: 100%; + content:""; + height:5px; background: pink; + background: linear-gradient(to bottom, rgba(0,0,0,0.3) 0, transparent 100%) +} + +/* -------------------- loading splash screen --------------------- */ +#learnocaml-exo-loading { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; +} +#learnocaml-exo-loading.loading, +#learnocaml-exo-loading.loaded { + background: rgba(200,200,200,0.9); +} +#learnocaml-exo-loading > iframe { + border: none; + overflow: auto; + flex: 1 3 auto; +} + +#learnocaml-loading { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; +} +#learnocaml-loading.loading, +#learnocaml-loading.loaded { + background: rgba(200,200,200,0.9); +} +#learnocaml-loading > iframe { + border: none; + overflow: auto; + flex: 1 3 auto; +} +/* -------------------- ACE overriding ---------------------------- */ +#learnocaml-exo-editor-pane { + font-size: 18px; + font-family: 'Inconsolata', monospace; +} +#learnocaml-exo-editor-pane .ace_gutter { + background: linear-gradient(to left, transparent 0, #444 8px, #ccc 8px, #ccc 9px, #444 9px) ; + color: #ccc; +} +#learnocaml-exo-editor-pane .ace_gutter-active-line { + background: linear-gradient(to left, #777 0, #456 8px, #ccc 8px, #ccc 9px, #678 9px) ; + color: #888; +} +#learnocaml-exo-editor-pane .ace_gutter-cell { + padding: 0 14px 0 0; +} +#learnocaml-exo-editor-pane .ace_gutter-cell.ace_warning { + background: linear-gradient(to right, #980 0, #980 4px, transparent 80%) ; +} +#learnocaml-exo-editor-pane .ace_gutter-cell.ace_error { + background: linear-gradient(to right, #900 0, #900 4px, transparent 80%) ; +} +#learnocaml-exo-editor-pane .ace_comment { color: #aaa; font-style: italic; } +#learnocaml-exo-editor-pane .ace_keyword { color: #e80; font-weight:bold; } +#learnocaml-exo-editor-pane .ace_constant { color: #acf; } +#learnocaml-exo-editor-pane .ace_string { color: #acf; } +#learnocaml-exo-editor-pane .ace_function { color: #fff; } +#learnocaml-exo-editor-pane .ace_type { color: #fff; } +#learnocaml-exo-editor-pane .ace_operator { color: #fff; } +#learnocaml-exo-editor-pane .ace_meta { color: #fff; } +#learnocaml-exo-editor-pane .ace_variable { color: #fff; } +#learnocaml-exo-editor-pane .ace_text { color: #fff; } +#learnocaml-exo-editor-pane .error { + border-bottom: 2px #b00 solid; + position: absolute; +} + +#learnocaml-exo-editor-pane .warning { + border-bottom: 2px #ca0 solid; + position: absolute; +} + +#learnocaml-exo-editor-pane .ace_selection { background: #e80; opacity: 0.4; } +#learnocaml-exo-editor-pane .ace_active-line { background: #acf; opacity: 0.2; } +#learnocaml-exo-editor-pane .ace_selected-word { background: #e80; opacity: 0.2; } + +#learnocaml-exo-editor-pane.ocaml-check-success::after, +#learnocaml-exo-editor-pane.ocaml-check-warn::after, +#learnocaml-exo-editor-pane.ocaml-check-error::after { + animation: 1s check_status_animation; + animation-fill-mode: forwards; + position: absolute; + margin: -100px 0 0 0; + top: 50%; + text-align: center; + font-size: 200px; + line-height: 200px; + width: 100%; +} + +#learnocaml-exo-editor-pane.ocaml-check-success::after { + content: "✌"; + color: #0a0; + text-shadow: 0px 0px 40px #6F6; +} + +#learnocaml-exo-editor-pane.ocaml-check-warn::after { + content: "✋"; + color: #ec0; + text-shadow: 0px 0px 40px #Fe6; +} + +#learnocaml-exo-editor-pane.ocaml-check-error::after { + content: "☠"; + color: #b00; + text-shadow: 0px 0px 40px #F66; +} + +#learnocaml-exo-test-pane { + font-size: 18px; + font-family: 'Inconsolata', monospace; +} +#learnocaml-exo-test-pane .ace_gutter { + background: linear-gradient(to left, transparent 0, #444 8px, #ccc 8px, #ccc 9px, #444 9px) ; + color: #ccc; +} +#learnocaml-exo-test-pane .ace_gutter-active-line { + background: linear-gradient(to left, #777 0, #456 8px, #ccc 8px, #ccc 9px, #678 9px) ; + color: #888; +} +#learnocaml-exo-test-pane .ace_gutter-cell { + padding: 0 14px 0 0; +} +#learnocaml-exo-test-pane .ace_gutter-cell.ace_warning { + background: linear-gradient(to right, #980 0, #980 4px, transparent 80%) ; +} +#learnocaml-exo-test-pane .ace_gutter-cell.ace_error { + background: linear-gradient(to right, #900 0, #900 4px, transparent 80%) ; +} +#learnocaml-exo-test-pane .ace_comment { color: #aaa; font-style: italic; } +#learnocaml-exo-test-pane .ace_keyword { color: #e80; font-weight:bold; } +#learnocaml-exo-test-pane .ace_constant { color: #acf; } +#learnocaml-exo-test-pane .ace_string { color: #acf; } +#learnocaml-exo-test-pane .ace_function { color: #fff; } +#learnocaml-exo-test-pane .ace_type { color: #fff; } +#learnocaml-exo-test-pane .ace_operator { color: #fff; } +#learnocaml-exo-test-pane .ace_meta { color: #fff; } +#learnocaml-exo-test-pane .ace_variable { color: #fff; } +#learnocaml-exo-test-pane .ace_text { color: #fff; } +#learnocaml-exo-test-pane .error { + border-bottom: 2px #b00 solid; + position: absolute; +} + +#learnocaml-exo-test-pane .warning { + border-bottom: 2px #ca0 solid; + position: absolute; +} + +#learnocaml-exo-test-pane .ace_selection { background: #e80; opacity: 0.4; } +#learnocaml-exo-test-pane .ace_active-line { background: #acf; opacity: 0.2; } +#learnocaml-exo-test-pane .ace_selected-word { background: #e80; opacity: 0.2; } + +#learnocaml-exo-test-pane.ocaml-check-success::after, +#learnocaml-exo-test-pane.ocaml-check-warn::after, +#learnocaml-exo-test-pane.ocaml-check-error::after { + animation: 1s check_status_animation; + animation-fill-mode: forwards; + position: absolute; + margin: -100px 0 0 0; + top: 50%; + text-align: center; + font-size: 200px; + line-height: 200px; + width: 100%; +} + +#learnocaml-exo-test-pane.ocaml-check-success::after { + content: "✌"; + color: #0a0; + text-shadow: 0px 0px 40px #6F6; +} + +#learnocaml-exo-test-pane.ocaml-check-warn::after { + content: "✋"; + color: #ec0; + text-shadow: 0px 0px 40px #Fe6; +} + +#learnocaml-exo-test-pane.ocaml-check-error::after { + content: "☠"; + color: #b00; + text-shadow: 0px 0px 40px #F66; +} + +/* testhaut */ + +#learnocaml-exo-testhaut-edit { + font-size: 18px; + font-family: 'Inconsolata', monospace; +} +#toolbar{ + height: 30px; +} +#button_delete{ + position: absolute; +} +#up{ + position: absolute; + left: 45px; +} +#down{ + position: absolute; + left: 90px; +} +#duplicate{ + position: absolute; + left: 135px; +} +#learnocaml-exo-tab-testhaut > .testhaut-edit { + position: absolute; + left: 0; top: 0; bottom: 64%; width: 100%; + background: #666; + color: #fff; + z-index: 1002; +} + +#learnocaml-exo-tab-testhaut > .testhaut-pane { + position: absolute; + left: 0; top: 33%; bottom: 40px; width: 100%; + background: #eee; + color: #fff; + z-index: 1002; + overflow-y: auto +} +#learnocaml-exo-testhaut-edit .ace_gutter { + background: linear-gradient(to left, transparent 0, #444 8px, #ccc 8px, #ccc 9px, #444 9px) ; + color: #ccc; +} +#learnocaml-exo-testhaut-edit .ace_gutter-active-line { + background: linear-gradient(to left, #777 0, #456 8px, #ccc 8px, #ccc 9px, #678 9px) ; + color: #888; +} +#learnocaml-exo-testhaut-edit .ace_gutter-cell { + padding: 0 14px 0 0; +} +#learnocaml-exo-testhaut-edit .ace_gutter-cell.ace_warning { + background: linear-gradient(to right, #980 0, #980 4px, transparent 80%) ; +} +#learnocaml-exo-testhaut-edit .ace_gutter-cell.ace_error { + background: linear-gradient(to right, #900 0, #900 4px, transparent 80%) ; +} +#learnocaml-exo-testhaut-edit .ace_comment { color: #aaa; font-style: italic; } +#learnocaml-exo-testhaut-edit .ace_keyword { color: #e80; font-weight:bold; } +#learnocaml-exo-testhaut-edit .ace_constant { color: #acf; } +#learnocaml-exo-testhaut-edit .ace_string { color: #acf; } +#learnocaml-exo-testhaut-edit .ace_function { color: #fff; } +#learnocaml-exo-testhaut-edit .ace_type { color: #fff; } +#learnocaml-exo-testhaut-edit .ace_operator { color: #fff; } +#learnocaml-exo-testhaut-edit .ace_meta { color: #fff; } +#learnocaml-exo-testhaut-edit .ace_variable { color: #fff; } +#learnocaml-exo-testhaut-edit .ace_text { color: #fff; } +#learnocaml-exo-testhaut-edit .error { + border-bottom: 2px #b00 solid; + position: absolute; +} + +#learnocaml-exo-testhaut-edit .warning { + border-bottom: 2px #ca0 solid; + position: absolute; +} + +#learnocaml-exo-testhaut-edit .ace_selection { background: #e80; opacity: 0.4; } +#learnocaml-exo-testhaut-edit .ace_active-line { background: #acf; opacity: 0.2; } +#learnocaml-exo-testhaut-edit .ace_selected-word { background: #e80; opacity: 0.2; } + +#learnocaml-exo-testhaut-edit.ocaml-check-success::after, +#learnocaml-exo-testhaut-edit.ocaml-check-warn::after, +#learnocaml-exo-testhaut-edit.ocaml-check-error::after { + animation: 1s check_status_animation; + animation-fill-mode: forwards; + position: absolute; + margin: -100px 0 0 0; + top: 50%; + text-align: center; + font-size: 200px; + line-height: 200px; + width: 100%; +} + +#learnocaml-exo-testhaut-edit.ocaml-check-success::after { + content: "✌"; + color: #0a0; + text-shadow: 0px 0px 40px #6F6; +} + +#learnocaml-exo-testhaut-edit.ocaml-check-warn::after { + content: "✋"; + color: #ec0; + text-shadow: 0px 0px 40px #Fe6; +} + +#learnocaml-exo-testhaut-edit.ocaml-check-error::after { + content: "☠"; + color: #b00; + text-shadow: 0px 0px 40px #F66; +} +/* end testhaut */ + + +/* question mark */ + +#learnocaml-exo-question-mark { + font-size: 18px; + font-family: 'Inconsolata', monospace; +} +#learnocaml-exo-question-mark .ace_gutter { + background: linear-gradient(to left, transparent 0, #444 8px, #ccc 8px, #ccc 9px, #444 9px) ; + color: #ccc; +} +#learnocaml-exo-question-mark .ace_gutter-active-line { + background: linear-gradient(to left, #777 0, #456 8px, #ccc 8px, #ccc 9px, #678 9px) ; + color: #888; +} +#learnocaml-exo-question-mark .ace_gutter-cell { + padding: 0 14px 0 0; +} +#learnocaml-exo-question-mark .ace_gutter-cell.ace_warning { + background: linear-gradient(to right, #980 0, #980 4px, transparent 80%) ; +} +#learnocaml-exo-question-mark .ace_gutter-cell.ace_error { + background: linear-gradient(to right, #900 0, #900 4px, transparent 80%) ; +} +#learnocaml-exo-question-mark .ace_comment { color: #aaa; font-style: italic; } +#learnocaml-exo-question-mark .ace_keyword { color: #e80; font-weight:bold; } +#learnocaml-exo-question-mark .ace_constant { color: #acf; } +#learnocaml-exo-question-mark .ace_string { color: #acf; } +#learnocaml-exo-question-mark .ace_function { color: #fff; } +#learnocaml-exo-question-mark .ace_type { color: #fff; } +#learnocaml-exo-question-mark .ace_operator { color: #fff; } +#learnocaml-exo-question-mark .ace_meta { color: #fff; } +#learnocaml-exo-question-mark .ace_variable { color: #fff; } +#learnocaml-exo-question-mark .ace_text { color: #fff; } +#learnocaml-exo-question-mark .error { + border-bottom: 2px #b00 solid; + position: absolute; +} + +#learnocaml-exo-question-mark .warning { + border-bottom: 2px #ca0 solid; + position: absolute; +} + +#learnocaml-exo-question-mark .ace_selection { background: #e80; opacity: 0.4; } +#learnocaml-exo-question-mark .ace_active-line { background: #acf; opacity: 0.2; } +#learnocaml-exo-question-mark .ace_selected-word { background: #e80; opacity: 0.2; } + +/* question html */ + +#learnocaml-exo-question-html { + font-size: 18px; + font-family: 'Inconsolata', monospace; +} +#learnocaml-exo-question-html .ace_gutter { + background: linear-gradient(to left, transparent 0, #444 8px, #ccc 8px, #ccc 9px, #444 9px) ; + color: #ccc; +} +#learnocaml-exo-question-html .ace_gutter-active-line { + background: linear-gradient(to left, #777 0, #456 8px, #ccc 8px, #ccc 9px, #678 9px) ; + color: #888; +} +#learnocaml-exo-question-html .ace_gutter-cell { + padding: 0 14px 0 0; +} +#learnocaml-exo-question-html .ace_gutter-cell.ace_warning { + background: linear-gradient(to right, #980 0, #980 4px, transparent 80%) ; +} +#learnocaml-exo-question-html .ace_gutter-cell.ace_error { + background: linear-gradient(to right, #900 0, #900 4px, transparent 80%) ; +} +#learnocaml-exo-question-html .ace_comment { color: #aaa; font-style: italic; } +#learnocaml-exo-question-html .ace_keyword { color: #e80; font-weight:bold; } +#learnocaml-exo-question-html .ace_constant { color: #acf; } +#learnocaml-exo-question-html .ace_string { color: #acf; } +#learnocaml-exo-question-html .ace_function { color: #fff; } +#learnocaml-exo-question-html .ace_type { color: #fff; } +#learnocaml-exo-question-html .ace_operator { color: #fff; } +#learnocaml-exo-question-html .ace_meta { color: #fff; } +#learnocaml-exo-question-html .ace_variable { color: #fff; } +#learnocaml-exo-question-html .ace_text { color: #fff; } +#learnocaml-exo-question-html .error { + border-bottom: 2px #b00 solid; + position: absolute; +} + +#learnocaml-exo-question-html .warning { + border-bottom: 2px #ca0 solid; + position: absolute; +} + +#learnocaml-exo-question-html .ace_selection { background: #e80; opacity: 0.4; } +#learnocaml-exo-question-html .ace_active-line { background: #acf; opacity: 0.2; } +#learnocaml-exo-question-html .ace_selected-word { background: #e80; opacity: 0.2; } + + +/* template */ +#learnocaml-exo-template-pane { + font-size: 18px; + font-family: 'Inconsolata', monospace; +} +#learnocaml-exo-template-pane .ace_gutter { + background: linear-gradient(to left, transparent 0, #444 8px, #ccc 8px, #ccc 9px, #444 9px) ; + color: #ccc; +} +#learnocaml-exo-template-pane .ace_gutter-active-line { + background: linear-gradient(to left, #777 0, #456 8px, #ccc 8px, #ccc 9px, #678 9px) ; + color: #888; +} +#learnocaml-exo-template-pane .ace_gutter-cell { + padding: 0 14px 0 0; +} +#learnocaml-exo-template-pane .ace_gutter-cell.ace_warning { + background: linear-gradient(to right, #980 0, #980 4px, transparent 80%) ; +} +#learnocaml-exo-template-pane .ace_gutter-cell.ace_error { + background: linear-gradient(to right, #900 0, #900 4px, transparent 80%) ; +} +#learnocaml-exo-template-pane .ace_comment { color: #aaa; font-style: italic; } +#learnocaml-exo-template-pane .ace_keyword { color: #e80; font-weight:bold; } +#learnocaml-exo-template-pane .ace_constant { color: #acf; } +#learnocaml-exo-template-pane .ace_string { color: #acf; } +#learnocaml-exo-template-pane .ace_function { color: #fff; } +#learnocaml-exo-template-pane .ace_type { color: #fff; } +#learnocaml-exo-template-pane .ace_operator { color: #fff; } +#learnocaml-exo-template-pane .ace_meta { color: #fff; } +#learnocaml-exo-template-pane .ace_variable { color: #fff; } +#learnocaml-exo-template-pane .ace_text { color: #fff; } +#learnocaml-exo-template-pane .error { + border-bottom: 2px #b00 solid; + position: absolute; +} + +#learnocaml-exo-template-pane .warning { + border-bottom: 2px #ca0 solid; + position: absolute; +} + +#learnocaml-exo-template-pane .ace_selection { background: #e80; opacity: 0.4; } +#learnocaml-exo-template-pane .ace_active-line { background: #acf; opacity: 0.2; } +#learnocaml-exo-template-pane .ace_selected-word { background: #e80; opacity: 0.2; } + +#learnocaml-exo-template-pane.ocaml-check-success::after, +#learnocaml-exo-template-pane.ocaml-check-warn::after, +#learnocaml-exo-template-pane.ocaml-check-error::after { + animation: 1s check_status_animation; + animation-fill-mode: forwards; + position: absolute; + margin: -100px 0 0 0; + top: 50%; + text-align: center; + font-size: 200px; + line-height: 200px; + width: 100%; +} + +#learnocaml-exo-template-pane.ocaml-check-success::after { + content: "✌"; + color: #0a0; + text-shadow: 0px 0px 40px #6F6; +} + +#learnocaml-exo-template-pane.ocaml-check-warn::after { + content: "✋"; + color: #ec0; + text-shadow: 0px 0px 40px #Fe6; +} + +#learnocaml-exo-template-pane.ocaml-check-error::after { + content: "☠"; + color: #b00; + text-shadow: 0px 0px 40px #F66; +} + +/* prepare */ + +#learnocaml-exo-prepare-pane { + font-size: 18px; + font-family: 'Inconsolata', monospace; +} +#learnocaml-exo-prepare-pane .ace_gutter { + background: linear-gradient(to left, transparent 0, #444 8px, #ccc 8px, #ccc 9px, #444 9px) ; + color: #ccc; +} +#learnocaml-exo-prepare-pane .ace_gutter-active-line { + background: linear-gradient(to left, #777 0, #456 8px, #ccc 8px, #ccc 9px, #678 9px) ; + color: #888; +} +#learnocaml-exo-prepare-pane .ace_gutter-cell { + padding: 0 14px 0 0; +} +#learnocaml-exo-prepare-pane .ace_gutter-cell.ace_warning { + background: linear-gradient(to right, #980 0, #980 4px, transparent 80%) ; +} +#learnocaml-exo-prepare-pane .ace_gutter-cell.ace_error { + background: linear-gradient(to right, #900 0, #900 4px, transparent 80%) ; +} +#learnocaml-exo-prepare-pane .ace_comment { color: #aaa; font-style: italic; } +#learnocaml-exo-prepare-pane .ace_keyword { color: #e80; font-weight:bold; } +#learnocaml-exo-prepare-pane .ace_constant { color: #acf; } +#learnocaml-exo-prepare-pane .ace_string { color: #acf; } +#learnocaml-exo-prepare-pane .ace_function { color: #fff; } +#learnocaml-exo-prepare-pane .ace_type { color: #fff; } +#learnocaml-exo-prepare-pane .ace_operator { color: #fff; } +#learnocaml-exo-prepare-pane .ace_meta { color: #fff; } +#learnocaml-exo-prepare-pane .ace_variable { color: #fff; } +#learnocaml-exo-prepare-pane .ace_text { color: #fff; } +#learnocaml-exo-prepare-pane .error { + border-bottom: 2px #b00 solid; + position: absolute; +} + +#learnocaml-exo-prepare-pane .warning { + border-bottom: 2px #ca0 solid; + position: absolute; +} + +#learnocaml-exo-prepare-pane .ace_selection { background: #e80; opacity: 0.4; } +#learnocaml-exo-prepare-pane .ace_active-line { background: #acf; opacity: 0.2; } +#learnocaml-exo-prepare-pane .ace_selected-word { background: #e80; opacity: 0.2; } + +#learnocaml-exo-prepare-pane.ocaml-check-success::after, +#learnocaml-exo-prepare-pane.ocaml-check-warn::after, +#learnocaml-exo-prepare-pane.ocaml-check-error::after { + animation: 1s check_status_animation; + animation-fill-mode: forwards; + position: absolute; + margin: -100px 0 0 0; + top: 50%; + text-align: center; + font-size: 200px; + line-height: 200px; + width: 100%; +} + +#learnocaml-exo-prepare-pane.ocaml-check-success::after { + content: "✌"; + color: #0a0; + text-shadow: 0px 0px 40px #6F6; +} + +#learnocaml-exo-prepare-pane.ocaml-check-warn::after { + content: "✋"; + color: #ec0; + text-shadow: 0px 0px 40px #Fe6; +} + +#learnocaml-exo-prepare-pane.ocaml-check-error::after { + content: "☠"; + color: #b00; + text-shadow: 0px 0px 40px #F66; +} + +/* prelude */ + +#learnocaml-exo-prelude-pane { + font-size: 18px; + font-family: 'Inconsolata', monospace; +} +#learnocaml-exo-prelude-pane .ace_gutter { + background: linear-gradient(to left, transparent 0, #444 8px, #ccc 8px, #ccc 9px, #444 9px) ; + color: #ccc; +} +#learnocaml-exo-prelude-pane .ace_gutter-active-line { + background: linear-gradient(to left, #777 0, #456 8px, #ccc 8px, #ccc 9px, #678 9px) ; + color: #888; +} +#learnocaml-exo-prelude-pane .ace_gutter-cell { + padding: 0 14px 0 0; +} +#learnocaml-exo-prelude-pane .ace_gutter-cell.ace_warning { + background: linear-gradient(to right, #980 0, #980 4px, transparent 80%) ; +} +#learnocaml-exo-prelude-pane .ace_gutter-cell.ace_error { + background: linear-gradient(to right, #900 0, #900 4px, transparent 80%) ; +} +#learnocaml-exo-prelude-pane .ace_comment { color: #aaa; font-style: italic; } +#learnocaml-exo-prelude-pane .ace_keyword { color: #e80; font-weight:bold; } +#learnocaml-exo-prelude-pane .ace_constant { color: #acf; } +#learnocaml-exo-prelude-pane .ace_string { color: #acf; } +#learnocaml-exo-prelude-pane .ace_function { color: #fff; } +#learnocaml-exo-prelude-pane .ace_type { color: #fff; } +#learnocaml-exo-prelude-pane .ace_operator { color: #fff; } +#learnocaml-exo-prelude-pane .ace_meta { color: #fff; } +#learnocaml-exo-prelude-pane .ace_variable { color: #fff; } +#learnocaml-exo-prelude-pane .ace_text { color: #fff; } +#learnocaml-exo-prelude-pane .error { + border-bottom: 2px #b00 solid; + position: absolute; +} + +#learnocaml-exo-prelude-pane .warning { + border-bottom: 2px #ca0 solid; + position: absolute; +} + +#learnocaml-exo-prelude-pane .ace_selection { background: #e80; opacity: 0.4; } +#learnocaml-exo-prelude-pane .ace_active-line { background: #acf; opacity: 0.2; } +#learnocaml-exo-prelude-pane .ace_selected-word { background: #e80; opacity: 0.2; } + +#learnocaml-exo-prelude-pane.ocaml-check-success::after, +#learnocaml-exo-prelude-pane.ocaml-check-warn::after, +#learnocaml-exo-prelude-pane.ocaml-check-error::after { + animation: 1s check_status_animation; + animation-fill-mode: forwards; + position: absolute; + margin: -100px 0 0 0; + top: 50%; + text-align: center; + font-size: 200px; + line-height: 200px; + width: 100%; +} + +#learnocaml-exo-prelude-pane.ocaml-check-success::after { + content: "✌"; + color: #0a0; + text-shadow: 0px 0px 40px #6F6; +} + +#learnocaml-exo-prelude-pane.ocaml-check-warn::after { + content: "✋"; + color: #ec0; + text-shadow: 0px 0px 40px #Fe6; +} + +#learnocaml-exo-prelude-pane.ocaml-check-error::after { + content: "☠"; + color: #b00; + text-shadow: 0px 0px 40px #F66; +} + +@keyframes check_status_animation { + 0% { opacity: 0; z-index: 9999; } + 49% { opacity: 1; transform: scale(2); z-index: 9999; } + 99% { opacity: 0; transform: scale(2); z-index: 9999; } + 100% { opacity: 0; z-index: -9999; } +} + +@-webkit-keyframes check_status_animation { + 0% { opacity: 0; z-index: 9999; } + 49% { opacity: 1; transform: scale(2); z-index: 9999; } + 99% { opacity: 0; transform: scale(2); z-index: 9999; } + 100% { opacity: 0; z-index: -9999; } +} diff --git a/static/css/learnocaml_new_exercise.css b/static/css/learnocaml_new_exercise.css new file mode 100644 index 000000000..62aec1371 --- /dev/null +++ b/static/css/learnocaml_new_exercise.css @@ -0,0 +1,98 @@ +body{ + background: #eee; +} +#page_title{ + text-align: center; + font-size: 25px; + line-height: 50px; +} +#learnocaml-main-toolbar { + position: fixed; + left: 0; top: 0; + color: #fff; + z-index: 1004; + border-bottom: 1px #eee solid; + display: flex; + background: #222; +} +#learnocaml-main-toolbar a { + text-decoration: none; + color: #fff; +} +#learnocaml-main-toolbar > button { + border-left: 1px #eee solid !important; + padding: 0 10px 0 10px; + margin: 0; + background: none; + border: none; + flex: 0; + position: relative; + display: block; +} +#learnocaml-main-toolbar > button > * { + padding: 5px; + color: #eee; + text-align: center; +} +#learnocaml-main-toolbar::before { + z-index: 1004; + position: fixed; + left: 0px; height: 5px; width: 100%; + background: linear-gradient(to bottom, rgba(0,0,0,0.4) 0px, rgba(0,0,0,0) 5px); + content:""; +} +@media (max-width: 549px) { + #learnocaml-main-toolbar { + height: 40px; + width: 100% + } + .button { + height: 40px; + min-width: 40px; + } + #learnocaml-main-toolbar::before { + top: 41px; + } + #learnocaml-main-toolbar > button > .label { + display: none; + } +} +@media (min-width: 550px) { + #learnocaml-main-toolbar { + height: 60px; + width: 100%; + } + .button { + height: 60px; + min-width: 60px; + } + #learnocaml-main-toolbar::before { + top: 61px; + } +} + +#learnocaml-tabs > * { + position: absolute; + z-index: 997; + background: #eee; + padding: 0; + opacity: 0; +} +#learnocaml-tabs > *.front-tab { + z-index: 998; + opacity: 1; +} +#learnocaml-tabs > * { + left: 0px; top: 15px; right: 0px; bottom: 0px; +} + + +.splitted { + display: flex; + justify-content: start + +} + +.splitted:first-child{ + margin-right: 50px; +} \ No newline at end of file diff --git a/static/editor/editor.html b/static/editor/editor.html new file mode 100644 index 000000000..00bbcc0bf --- /dev/null +++ b/static/editor/editor.html @@ -0,0 +1,191 @@ + + + + + + + Learn OCaml by OCamlPro - Editor + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+
+
+
    +
  • Preparing the environment
  • +
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
Editor
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + +
+
+ + + + + + + + + + + + + +
+
+ +
+ + + + + + +
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + +
+ + + diff --git a/static/editor/new-exercise.html b/static/editor/new-exercise.html new file mode 100644 index 000000000..653eee00b --- /dev/null +++ b/static/editor/new-exercise.html @@ -0,0 +1,125 @@ + + + + + + + Learn OCaml by OCamlPro - Editor + + + + + + + + + + + + + +
+ + + + + +
+
+ +
+ + + + +
+
+
+
+

New Exercise

+
+ +
+

+ + + +

+

+ + + +

+

+ +
+ + Each author follows the syntax:   + Firstname Lastname <address@email.com>
+ But their email can be omitted:   + Firstname Lastname <> +

+
+
+ + +
+
+
+ + +
+ +
+

+ + +

+

+ + +

+
+
+ + +
+
+
+ + +
+
+ +
+ + + + diff --git a/static/index.html b/static/index.html index b574371b6..ba22db650 100644 --- a/static/index.html +++ b/static/index.html @@ -8,6 +8,8 @@ + + diff --git a/static/js/ace/mode-ocaml.js b/static/js/ace/mode-ocaml.js new file mode 100644 index 000000000..3b1a3e7be --- /dev/null +++ b/static/js/ace/mode-ocaml.js @@ -0,0 +1,421 @@ +ace.define("ace/mode/ocaml_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var OcamlHighlightRules = function() { + + var keywords = ( + "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" + ); + + var builtinConstants = ("true|false"); + + var builtinFunctions = ( + "abs|abs_big_int|abs_float|abs_num|abstract_tag|accept|access|acos|add|" + + "add_available_units|add_big_int|add_buffer|add_channel|add_char|" + + "add_initializer|add_int_big_int|add_interfaces|add_num|add_string|" + + "add_substitute|add_substring|alarm|allocated_bytes|allow_only|" + + "allow_unsafe_modules|always|append|appname_get|appname_set|" + + "approx_num_exp|approx_num_fix|arg|argv|arith_status|array|" + + "array1_of_genarray|array2_of_genarray|array3_of_genarray|asin|asr|" + + "assoc|assq|at_exit|atan|atan2|auto_synchronize|background|basename|" + + "beginning_of_input|big_int_of_int|big_int_of_num|big_int_of_string|bind|" + + "bind_class|bind_tag|bits|bits_of_float|black|blit|blit_image|blue|bool|" + + "bool_of_string|bounded_full_split|bounded_split|bounded_split_delim|" + + "bprintf|break|broadcast|bscanf|button_down|c_layout|capitalize|cardinal|" + + "cardinal|catch|catch_break|ceil|ceiling_num|channel|char|char_of_int|" + + "chdir|check|check_suffix|chmod|choose|chop_extension|chop_suffix|chown|" + + "chown|chr|chroot|classify_float|clear|clear_available_units|" + + "clear_close_on_exec|clear_graph|clear_nonblock|clear_parser|" + + "close|close|closeTk|close_box|close_graph|close_in|close_in_noerr|" + + "close_out|close_out_noerr|close_process|close_process|" + + "close_process_full|close_process_in|close_process_out|close_subwindow|" + + "close_tag|close_tbox|closedir|closedir|closure_tag|code|combine|" + + "combine|combine|command|compact|compare|compare_big_int|compare_num|" + + "complex32|complex64|concat|conj|connect|contains|contains_from|contents|" + + "copy|cos|cosh|count|count|counters|create|create_alarm|create_image|" + + "create_matrix|create_matrix|create_matrix|create_object|" + + "create_object_and_run_initializers|create_object_opt|create_process|" + + "create_process|create_process_env|create_process_env|create_table|" + + "current|current_dir_name|current_point|current_x|current_y|curveto|" + + "custom_tag|cyan|data_size|decr|decr_num|default_available_units|delay|" + + "delete_alarm|descr_of_in_channel|descr_of_out_channel|destroy|diff|dim|" + + "dim1|dim2|dim3|dims|dirname|display_mode|div|div_big_int|div_num|" + + "double_array_tag|double_tag|draw_arc|draw_char|draw_circle|draw_ellipse|" + + "draw_image|draw_poly|draw_poly_line|draw_rect|draw_segments|draw_string|" + + "dummy_pos|dummy_table|dump_image|dup|dup2|elements|empty|end_of_input|" + + "environment|eprintf|epsilon_float|eq_big_int|eq_num|equal|err_formatter|" + + "error_message|escaped|establish_server|executable_name|execv|execve|execvp|" + + "execvpe|exists|exists2|exit|exp|failwith|fast_sort|fchmod|fchown|field|" + + "file|file_exists|fill|fill_arc|fill_circle|fill_ellipse|fill_poly|fill_rect|" + + "filter|final_tag|finalise|find|find_all|first_chars|firstkey|flatten|" + + "float|float32|float64|float_of_big_int|float_of_bits|float_of_int|" + + "float_of_num|float_of_string|floor|floor_num|flush|flush_all|flush_input|" + + "flush_str_formatter|fold|fold_left|fold_left2|fold_right|fold_right2|" + + "for_all|for_all2|force|force_newline|force_val|foreground|fork|" + + "format_of_string|formatter_of_buffer|formatter_of_out_channel|" + + "fortran_layout|forward_tag|fprintf|frexp|from|from_channel|from_file|" + + "from_file_bin|from_function|from_string|fscanf|fst|fstat|ftruncate|" + + "full_init|full_major|full_split|gcd_big_int|ge_big_int|ge_num|" + + "genarray_of_array1|genarray_of_array2|genarray_of_array3|get|" + + "get_all_formatter_output_functions|get_approx_printing|get_copy|" + + "get_ellipsis_text|get_error_when_null_denominator|get_floating_precision|" + + "get_formatter_output_functions|get_formatter_tag_functions|get_image|" + + "get_margin|get_mark_tags|get_max_boxes|get_max_indent|get_method|" + + "get_method_label|get_normalize_ratio|get_normalize_ratio_when_printing|" + + "get_print_tags|get_state|get_variable|getcwd|getegid|getegid|getenv|" + + "getenv|getenv|geteuid|geteuid|getgid|getgid|getgrgid|getgrgid|getgrnam|" + + "getgrnam|getgroups|gethostbyaddr|gethostbyname|gethostname|getitimer|" + + "getlogin|getpeername|getpid|getppid|getprotobyname|getprotobynumber|" + + "getpwnam|getpwuid|getservbyname|getservbyport|getsockname|getsockopt|" + + "getsockopt_float|getsockopt_int|getsockopt_optint|gettimeofday|getuid|" + + "global_replace|global_substitute|gmtime|green|grid|group_beginning|" + + "group_end|gt_big_int|gt_num|guard|handle_unix_error|hash|hash_param|" + + "hd|header_size|i|id|ignore|in_channel_length|in_channel_of_descr|incr|" + + "incr_num|index|index_from|inet_addr_any|inet_addr_of_string|infinity|" + + "infix_tag|init|init_class|input|input_binary_int|input_byte|input_char|" + + "input_line|input_value|int|int16_signed|int16_unsigned|int32|int64|" + + "int8_signed|int8_unsigned|int_of_big_int|int_of_char|int_of_float|" + + "int_of_num|int_of_string|integer_num|inter|interactive|inv|invalid_arg|" + + "is_block|is_empty|is_implicit|is_int|is_int_big_int|is_integer_num|" + + "is_relative|iter|iter2|iteri|join|junk|key_pressed|kill|kind|kprintf|" + + "kscanf|land|last_chars|layout|lazy_from_fun|lazy_from_val|lazy_is_val|" + + "lazy_tag|ldexp|le_big_int|le_num|length|lexeme|lexeme_char|lexeme_end|" + + "lexeme_end_p|lexeme_start|lexeme_start_p|lineto|link|list|listen|lnot|" + + "loadfile|loadfile_private|localtime|lock|lockf|log|log10|logand|lognot|" + + "logor|logxor|lor|lower_window|lowercase|lseek|lsl|lsr|lstat|lt_big_int|" + + "lt_num|lxor|magenta|magic|mainLoop|major|major_slice|make|make_formatter|" + + "make_image|make_lexer|make_matrix|make_self_init|map|map2|map_file|mapi|" + + "marshal|match_beginning|match_end|matched_group|matched_string|max|" + + "max_array_length|max_big_int|max_elt|max_float|max_int|max_num|" + + "max_string_length|mem|mem_assoc|mem_assq|memq|merge|min|min_big_int|" + + "min_elt|min_float|min_int|min_num|minor|minus_big_int|minus_num|" + + "minus_one|mkdir|mkfifo|mktime|mod|mod_big_int|mod_float|mod_num|modf|" + + "mouse_pos|moveto|mul|mult_big_int|mult_int_big_int|mult_num|nan|narrow|" + + "nat_of_num|nativeint|neg|neg_infinity|new_block|new_channel|new_method|" + + "new_variable|next|nextkey|nice|nice|no_scan_tag|norm|norm2|not|npeek|" + + "nth|nth_dim|num_digits_big_int|num_dims|num_of_big_int|num_of_int|" + + "num_of_nat|num_of_ratio|num_of_string|O|obj|object_tag|ocaml_version|" + + "of_array|of_channel|of_float|of_int|of_int32|of_list|of_nativeint|" + + "of_string|one|openTk|open_box|open_connection|open_graph|open_hbox|" + + "open_hovbox|open_hvbox|open_in|open_in_bin|open_in_gen|open_out|" + + "open_out_bin|open_out_gen|open_process|open_process_full|open_process_in|" + + "open_process_out|open_subwindow|open_tag|open_tbox|open_temp_file|" + + "open_vbox|opendbm|opendir|openfile|or|os_type|out_channel_length|" + + "out_channel_of_descr|output|output_binary_int|output_buffer|output_byte|" + + "output_char|output_string|output_value|over_max_boxes|pack|params|" + + "parent_dir_name|parse|parse_argv|partition|pause|peek|pipe|pixels|" + + "place|plot|plots|point_color|polar|poll|pop|pos_in|pos_out|pow|" + + "power_big_int_positive_big_int|power_big_int_positive_int|" + + "power_int_positive_big_int|power_int_positive_int|power_num|" + + "pp_close_box|pp_close_tag|pp_close_tbox|pp_force_newline|" + + "pp_get_all_formatter_output_functions|pp_get_ellipsis_text|" + + "pp_get_formatter_output_functions|pp_get_formatter_tag_functions|" + + "pp_get_margin|pp_get_mark_tags|pp_get_max_boxes|pp_get_max_indent|" + + "pp_get_print_tags|pp_open_box|pp_open_hbox|pp_open_hovbox|pp_open_hvbox|" + + "pp_open_tag|pp_open_tbox|pp_open_vbox|pp_over_max_boxes|pp_print_as|" + + "pp_print_bool|pp_print_break|pp_print_char|pp_print_cut|pp_print_float|" + + "pp_print_flush|pp_print_if_newline|pp_print_int|pp_print_newline|" + + "pp_print_space|pp_print_string|pp_print_tab|pp_print_tbreak|" + + "pp_set_all_formatter_output_functions|pp_set_ellipsis_text|" + + "pp_set_formatter_out_channel|pp_set_formatter_output_functions|" + + "pp_set_formatter_tag_functions|pp_set_margin|pp_set_mark_tags|" + + "pp_set_max_boxes|pp_set_max_indent|pp_set_print_tags|pp_set_tab|" + + "pp_set_tags|pred|pred_big_int|pred_num|prerr_char|prerr_endline|" + + "prerr_float|prerr_int|prerr_newline|prerr_string|print|print_as|" + + "print_bool|print_break|print_char|print_cut|print_endline|print_float|" + + "print_flush|print_if_newline|print_int|print_newline|print_space|" + + "print_stat|print_string|print_tab|print_tbreak|printf|prohibit|" + + "public_method_label|push|putenv|quo_num|quomod_big_int|quote|raise|" + + "raise_window|ratio_of_num|rcontains_from|read|read_float|read_int|" + + "read_key|read_line|readdir|readdir|readlink|really_input|receive|recv|" + + "recvfrom|red|ref|regexp|regexp_case_fold|regexp_string|" + + "regexp_string_case_fold|register|register_exception|rem|remember_mode|" + + "remove|remove_assoc|remove_assq|rename|replace|replace_first|" + + "replace_matched|repr|reset|reshape|reshape_1|reshape_2|reshape_3|rev|" + + "rev_append|rev_map|rev_map2|rewinddir|rgb|rhs_end|rhs_end_pos|rhs_start|" + + "rhs_start_pos|rindex|rindex_from|rlineto|rmdir|rmoveto|round_num|" + + "run_initializers|run_initializers_opt|scanf|search_backward|" + + "search_forward|seek_in|seek_out|select|self|self_init|send|sendto|set|" + + "set_all_formatter_output_functions|set_approx_printing|" + + "set_binary_mode_in|set_binary_mode_out|set_close_on_exec|" + + "set_close_on_exec|set_color|set_ellipsis_text|" + + "set_error_when_null_denominator|set_field|set_floating_precision|" + + "set_font|set_formatter_out_channel|set_formatter_output_functions|" + + "set_formatter_tag_functions|set_line_width|set_margin|set_mark_tags|" + + "set_max_boxes|set_max_indent|set_method|set_nonblock|set_nonblock|" + + "set_normalize_ratio|set_normalize_ratio_when_printing|set_print_tags|" + + "set_signal|set_state|set_tab|set_tag|set_tags|set_text_size|" + + "set_window_title|setgid|setgid|setitimer|setitimer|setsid|setsid|" + + "setsockopt|setsockopt|setsockopt_float|setsockopt_float|setsockopt_int|" + + "setsockopt_int|setsockopt_optint|setsockopt_optint|setuid|setuid|" + + "shift_left|shift_left|shift_left|shift_right|shift_right|shift_right|" + + "shift_right_logical|shift_right_logical|shift_right_logical|show_buckets|" + + "shutdown|shutdown|shutdown_connection|shutdown_connection|sigabrt|" + + "sigalrm|sigchld|sigcont|sigfpe|sighup|sigill|sigint|sigkill|sign_big_int|" + + "sign_num|signal|signal|sigpending|sigpending|sigpipe|sigprocmask|" + + "sigprocmask|sigprof|sigquit|sigsegv|sigstop|sigsuspend|sigsuspend|" + + "sigterm|sigtstp|sigttin|sigttou|sigusr1|sigusr2|sigvtalrm|sin|singleton|" + + "sinh|size|size|size_x|size_y|sleep|sleep|sleep|slice_left|slice_left|" + + "slice_left_1|slice_left_2|slice_right|slice_right|slice_right_1|" + + "slice_right_2|snd|socket|socket|socket|socketpair|socketpair|sort|sound|" + + "split|split_delim|sprintf|sprintf|sqrt|sqrt|sqrt_big_int|square_big_int|" + + "square_num|sscanf|stable_sort|stable_sort|stable_sort|stable_sort|stable_sort|" + + "stable_sort|stat|stat|stat|stat|stat|stats|stats|std_formatter|stdbuf|" + + "stderr|stderr|stderr|stdib|stdin|stdin|stdin|stdout|stdout|stdout|" + + "str_formatter|string|string_after|string_before|string_match|" + + "string_of_big_int|string_of_bool|string_of_float|string_of_format|" + + "string_of_inet_addr|string_of_inet_addr|string_of_int|string_of_num|" + + "string_partial_match|string_tag|sub|sub|sub_big_int|sub_left|sub_num|" + + "sub_right|subset|subset|substitute_first|substring|succ|succ|" + + "succ|succ|succ_big_int|succ_num|symbol_end|symbol_end_pos|symbol_start|" + + "symbol_start_pos|symlink|symlink|sync|synchronize|system|system|system|" + + "tag|take|tan|tanh|tcdrain|tcdrain|tcflow|tcflow|tcflush|tcflush|" + + "tcgetattr|tcgetattr|tcsendbreak|tcsendbreak|tcsetattr|tcsetattr|" + + "temp_file|text_size|time|time|time|timed_read|timed_write|times|times|" + + "tl|tl|tl|to_buffer|to_channel|to_float|to_hex|to_int|to_int32|to_list|" + + "to_list|to_list|to_nativeint|to_string|to_string|to_string|to_string|" + + "to_string|top|top|total_size|transfer|transp|truncate|truncate|truncate|" + + "truncate|truncate|truncate|try_lock|umask|umask|uncapitalize|uncapitalize|" + + "uncapitalize|union|union|unit_big_int|unlink|unlink|unlock|unmarshal|" + + "unsafe_blit|unsafe_fill|unsafe_get|unsafe_get|unsafe_set|unsafe_set|" + + "update|uppercase|uppercase|uppercase|uppercase|usage|utimes|utimes|wait|" + + "wait|wait|wait|wait_next_event|wait_pid|wait_read|wait_signal|" + + "wait_timed_read|wait_timed_write|wait_write|waitpid|white|" + + "widen|window_id|word_size|wrap|wrap_abort|write|yellow|yield|zero|zero_big_int|" + + + "Arg|Arith_status|Array|Array1|Array2|Array3|ArrayLabels|Big_int|Bigarray|" + + "Buffer|Callback|CamlinternalOO|Char|Complex|Condition|Dbm|Digest|Dynlink|" + + "Event|Filename|Format|Gc|Genarray|Genlex|Graphics|GraphicsX11|Hashtbl|" + + "Int32|Int64|LargeFile|Lazy|Lexing|List|ListLabels|Make|Map|Marshal|" + + "MoreLabels|Mutex|Nativeint|Num|Obj|Oo|Parsing|Pervasives|Printexc|" + + "Printf|Queue|Random|Scanf|Scanning|Set|Sort|Stack|State|StdLabels|Str|" + + "Stream|String|StringLabels|Sys|Thread|ThreadUnix|Tk|Unix|UnixLabels|Weak" + ); + + var keywordMapper = this.createKeywordMapper({ + "variable.language": "this", + "keyword": keywords, + "constant.language": builtinConstants, + "support.function": builtinFunctions + }, "identifier"); + + var decimalInteger = "(?:(?:[1-9]\\d*)|(?:0))"; + var octInteger = "(?:0[oO]?[0-7]+)"; + var hexInteger = "(?:0[xX][\\dA-Fa-f]+)"; + var binInteger = "(?:0[bB][01]+)"; + var integer = "(?:" + decimalInteger + "|" + octInteger + "|" + hexInteger + "|" + binInteger + ")"; + + var exponent = "(?:[eE][+-]?\\d+)"; + var fraction = "(?:\\.\\d+)"; + var intPart = "(?:\\d+)"; + var pointFloat = "(?:(?:" + intPart + "?" + fraction + ")|(?:" + intPart + "\\.))"; + var exponentFloat = "(?:(?:" + pointFloat + "|" + intPart + ")" + exponent + ")"; + var floatNumber = "(?:" + exponentFloat + "|" + pointFloat + ")"; + + this.$rules = { + "start" : [ + { + token : "comment", + regex : '\\(\\*.*?\\*\\)\\s*?$' + }, + { + token : "comment", + regex : '\\(\\*.*', + next : "comment" + }, + { + token : "string", // single line + regex : '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' + }, + { + token : "string", // single char + regex : "'.'" + }, + { + token : "string", // " string + regex : '"', + next : "qstring" + }, + { + token : "constant.numeric", // imaginary + regex : "(?:" + floatNumber + "|\\d+)[jJ]\\b" + }, + { + token : "constant.numeric", // float + regex : floatNumber + }, + { + token : "constant.numeric", // integer + regex : integer + "\\b" + }, + { + token : keywordMapper, + regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b" + }, + { + token : "keyword.operator", + regex : "\\+\\.|\\-\\.|\\*\\.|\\/\\.|#|;;|\\+|\\-|\\*|\\*\\*\\/|\\/\\/|%|<<|>>|&|\\||\\^|~|<|>|<=|=>|==|!=|<>|<-|=" + }, + { + token : "paren.lparen", + regex : "[[({]" + }, + { + token : "paren.rparen", + regex : "[\\])}]" + }, + { + token : "text", + regex : "\\s+" + } + ], + "comment" : [ + { + token : "comment", // closing comment + regex : "\\*\\)", + next : "start" + }, + { + defaultToken : "comment" + } + ], + + "qstring" : [ + { + token : "string", + regex : '"', + next : "start" + }, { + token : "string", + regex : '.+' + } + ] + }; +}; + +oop.inherits(OcamlHighlightRules, TextHighlightRules); + +exports.OcamlHighlightRules = OcamlHighlightRules; +}); + +ace.define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"], function(require, exports, module) { +"use strict"; + +var Range = require("../range").Range; + +var MatchingBraceOutdent = function() {}; + +(function() { + + this.checkOutdent = function(line, input) { + if (! /^\s+$/.test(line)) + return false; + + return /^\s*\}/.test(input); + }; + + this.autoOutdent = function(doc, row) { + var line = doc.getLine(row); + var match = line.match(/^(\s*\})/); + + if (!match) return 0; + + var column = match[1].length; + var openBracePos = doc.findMatchingBracket({row: row, column: column}); + + if (!openBracePos || openBracePos.row == row) return 0; + + var indent = this.$getIndent(doc.getLine(openBracePos.row)); + doc.replace(new Range(row, 0, row, column-1), indent); + }; + + this.$getIndent = function(line) { + return line.match(/^\s*/)[0]; + }; + +}).call(MatchingBraceOutdent.prototype); + +exports.MatchingBraceOutdent = MatchingBraceOutdent; +}); + +ace.define("ace/mode/ocaml",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/ocaml_highlight_rules","ace/mode/matching_brace_outdent","ace/range"], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var OcamlHighlightRules = require("./ocaml_highlight_rules").OcamlHighlightRules; +var MatchingBraceOutdent = require("./matching_brace_outdent").MatchingBraceOutdent; +var Range = require("../range").Range; + +var Mode = function() { + this.HighlightRules = OcamlHighlightRules; + this.$behaviour = this.$defaultBehaviour; + + this.$outdent = new MatchingBraceOutdent(); +}; +oop.inherits(Mode, TextMode); + +var indenter = /(?:[({[=:]|[-=]>|\b(?:else|try|with))\s*$/; + +(function() { + + this.toggleCommentLines = function(state, doc, startRow, endRow) { + var i, line; + var outdent = true; + var re = /^\s*\(\*(.*)\*\)/; + + for (i=startRow; i<= endRow; i++) { + if (!re.test(doc.getLine(i))) { + outdent = false; + break; + } + } + + var range = new Range(0, 0, 0, 0); + for (i=startRow; i<= endRow; i++) { + line = doc.getLine(i); + range.start.row = i; + range.end.row = i; + range.end.column = line.length; + + doc.replace(range, outdent ? line.match(re)[1] : "(*" + line + "*)"); + } + }; + + this.getNextLineIndent = function(state, line, tab) { + var indent = this.$getIndent(line); + var tokens = this.getTokenizer().getLineTokens(line, state).tokens; + + if (!(tokens.length && tokens[tokens.length - 1].type === 'comment') && + state === 'start' && indenter.test(line)) + indent += tab; + return indent; + }; + + this.checkOutdent = function(state, line, input) { + return this.$outdent.checkOutdent(line, input); + }; + + this.autoOutdent = function(state, doc, row) { + this.$outdent.autoOutdent(doc, row); + }; + + this.$id = "ace/mode/ocaml"; +}).call(Mode.prototype); + +exports.Mode = Mode; +}); (function() { + ace.require(["ace/mode/ocaml"], function(m) { + if (typeof module == "object" && typeof exports == "object" && module) { + module.exports = m; + } + }); + })(); + \ No newline at end of file diff --git a/static/js/jszip/jszip.min.js b/static/js/jszip/jszip.min.js new file mode 100644 index 000000000..520db0742 --- /dev/null +++ b/static/js/jszip/jszip.min.js @@ -0,0 +1,13 @@ +/*! + +JSZip v3.2.1 - A JavaScript class for generating and reading zip files + + +(c) 2009-2016 Stuart Knightley +Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/master/LICENSE.markdown. + +JSZip uses the library pako released under the MIT license : +https://github.com/nodeca/pako/blob/master/LICENSE +*/ + +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=t()}}(function(){return function s(a,o,h){function u(r,t){if(!o[r]){if(!a[r]){var e="function"==typeof require&&require;if(!t&&e)return e(r,!0);if(l)return l(r,!0);var i=new Error("Cannot find module '"+r+"'");throw i.code="MODULE_NOT_FOUND",i}var n=o[r]={exports:{}};a[r][0].call(n.exports,function(t){var e=a[r][1][t];return u(e||t)},n,n.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,t=0;t>2,s=(3&e)<<4|r>>4,a=1>6:64,o=2>4,r=(15&n)<<4|(s=p.indexOf(t.charAt(o++)))>>2,i=(3&s)<<6|(a=p.indexOf(t.charAt(o++))),l[h++]=e,64!==s&&(l[h++]=r),64!==a&&(l[h++]=i);return l}},{"./support":30,"./utils":32}],2:[function(t,e,r){"use strict";var i=t("./external"),n=t("./stream/DataWorker"),s=t("./stream/DataLengthProbe"),a=t("./stream/Crc32Probe");s=t("./stream/DataLengthProbe");function o(t,e,r,i,n){this.compressedSize=t,this.uncompressedSize=e,this.crc32=r,this.compression=i,this.compressedContent=n}o.prototype={getContentWorker:function(){var t=new n(i.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new s("data_length")),e=this;return t.on("end",function(){if(this.streamInfo.data_length!==e.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),t},getCompressedWorker:function(){return new n(i.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(t,e,r){return t.pipe(new a).pipe(new s("uncompressedSize")).pipe(e.compressWorker(r)).pipe(new s("compressedSize")).withStreamInfo("compression",e)},e.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,e,r){"use strict";var i=t("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(t){return new i("STORE compression")},uncompressWorker:function(){return new i("STORE decompression")}},r.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,e,r){"use strict";var i=t("./utils");var o=function(){for(var t,e=[],r=0;r<256;r++){t=r;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e){return void 0!==t&&t.length?"string"!==i.getTypeOf(t)?function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}(0|e,t,t.length,0):function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e.charCodeAt(a))];return-1^t}(0|e,t,t.length,0):0}},{"./utils":32}],5:[function(t,e,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(t,e,r){"use strict";var i=null;i="undefined"!=typeof Promise?Promise:t("lie"),e.exports={Promise:i}},{lie:37}],7:[function(t,e,r){"use strict";var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,n=t("pako"),s=t("./utils"),a=t("./stream/GenericWorker"),o=i?"uint8array":"array";function h(t,e){a.call(this,"FlateWorker/"+t),this._pako=null,this._pakoAction=t,this._pakoOptions=e,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(t){this.meta=t.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,t.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new n[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var e=this;this._pako.onData=function(t){e.push({data:t,meta:e.meta})}},r.compressWorker=function(t){return new h("Deflate",t)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,e,r){"use strict";function A(t,e){var r,i="";for(r=0;r>>=8;return i}function i(t,e,r,i,n,s){var a,o,h=t.file,u=t.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),d=I.transformTo("string",O.utf8encode(h.name)),c=h.comment,p=I.transformTo("string",s(c)),m=I.transformTo("string",O.utf8encode(c)),_=d.length!==h.name.length,g=m.length!==c.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};e&&!r||(x.crc32=t.crc32,x.compressedSize=t.compressedSize,x.uncompressedSize=t.uncompressedSize);var S=0;e&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===n?(C=798,z|=function(t,e){var r=t;return t||(r=e?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(t){return 63&(t||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+d,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(i,4)+f+b+p}}var I=t("../utils"),n=t("../stream/GenericWorker"),O=t("../utf8"),B=t("../crc32"),R=t("../signature");function s(t,e,r,i){n.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=e,this.zipPlatform=r,this.encodeFileName=i,this.streamFiles=t,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,n),s.prototype.push=function(t){var e=t.meta.percent||0,r=this.entriesCount,i=this._sources.length;this.accumulate?this.contentBuffer.push(t):(this.bytesWritten+=t.data.length,n.prototype.push.call(this,{data:t.data,meta:{currentFile:this.currentFile,percent:r?(e+100*(r-i-1))/r:100}}))},s.prototype.openedSource=function(t){this.currentSourceOffset=this.bytesWritten,this.currentFile=t.file.name;var e=this.streamFiles&&!t.file.dir;if(e){var r=i(t,e,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(t){this.accumulate=!1;var e=this.streamFiles&&!t.file.dir,r=i(t,e,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),e)this.push({data:function(t){return R.DATA_DESCRIPTOR+A(t.crc32,4)+A(t.compressedSize,4)+A(t.uncompressedSize,4)}(t),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var t=this.bytesWritten,e=0;e=this.index;e--)r=(r<<8)+this.byteAt(e);return this.index+=t,r},readString:function(t){return i.transformTo("string",this.readData(t))},readData:function(t){},lastIndexOfSignature:function(t){},readAndCheckSignature:function(t){},readDate:function(){var t=this.readInt(4);return new Date(Date.UTC(1980+(t>>25&127),(t>>21&15)-1,t>>16&31,t>>11&31,t>>5&63,(31&t)<<1))}},e.exports=n},{"../utils":32}],19:[function(t,e,r){"use strict";var i=t("./Uint8ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(t,e,r){"use strict";var i=t("./DataReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.byteAt=function(t){return this.data.charCodeAt(this.zero+t)},n.prototype.lastIndexOfSignature=function(t){return this.data.lastIndexOf(t)-this.zero},n.prototype.readAndCheckSignature=function(t){return t===this.readData(4)},n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./DataReader":18}],21:[function(t,e,r){"use strict";var i=t("./ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){if(this.checkOffset(t),0===t)return new Uint8Array(0);var e=this.data.subarray(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./ArrayReader":17}],22:[function(t,e,r){"use strict";var i=t("../utils"),n=t("../support"),s=t("./ArrayReader"),a=t("./StringReader"),o=t("./NodeBufferReader"),h=t("./Uint8ArrayReader");e.exports=function(t){var e=i.getTypeOf(t);return i.checkSupport(e),"string"!==e||n.uint8array?"nodebuffer"===e?new o(t):n.uint8array?new h(i.transformTo("uint8array",t)):new s(i.transformTo("array",t)):new a(t)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,e,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(t,e,r){"use strict";var i=t("./GenericWorker"),n=t("../utils");function s(t){i.call(this,"ConvertWorker to "+t),this.destType=t}n.inherits(s,i),s.prototype.processChunk=function(t){this.push({data:n.transformTo(this.destType,t.data),meta:t.meta})},e.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(t,e,r){"use strict";var i=t("./GenericWorker"),n=t("../crc32");function s(){i.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}t("../utils").inherits(s,i),s.prototype.processChunk=function(t){this.streamInfo.crc32=n(t.data,this.streamInfo.crc32||0),this.push(t)},e.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,e,r){"use strict";var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataLengthProbe for "+t),this.propName=t,this.withStreamInfo(t,0)}i.inherits(s,n),s.prototype.processChunk=function(t){if(t){var e=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=e+t.data.length}n.prototype.processChunk.call(this,t)},e.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(t,e,r){"use strict";var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataWorker");var e=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,t.then(function(t){e.dataIsReady=!0,e.data=t,e.max=t&&t.length||0,e.type=i.getTypeOf(t),e.isPaused||e._tickAndRepeat()},function(t){e.error(t)})}i.inherits(s,n),s.prototype.cleanUp=function(){n.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!n.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,i.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(i.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var t=null,e=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":t=this.data.substring(this.index,e);break;case"uint8array":t=this.data.subarray(this.index,e);break;case"array":case"nodebuffer":t=this.data.slice(this.index,e)}return this.index=e,this.push({data:t,meta:{percent:this.max?this.index/this.max*100:0}})},e.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(t,e,r){"use strict";function i(t){this.name=t||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}i.prototype={push:function(t){this.emit("data",t)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(t){this.emit("error",t)}return!0},error:function(t){return!this.isFinished&&(this.isPaused?this.generatedError=t:(this.isFinished=!0,this.emit("error",t),this.previous&&this.previous.error(t),this.cleanUp()),!0)},on:function(t,e){return this._listeners[t].push(e),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(t,e){if(this._listeners[t])for(var r=0;r "+t:t}},e.exports=i},{}],29:[function(t,e,r){"use strict";var h=t("../utils"),n=t("./ConvertWorker"),s=t("./GenericWorker"),u=t("../base64"),i=t("../support"),a=t("../external"),o=null;if(i.nodestream)try{o=t("../nodejs/NodejsStreamOutputAdapter")}catch(t){}function l(t,o){return new a.Promise(function(e,r){var i=[],n=t._internalType,s=t._outputType,a=t._mimeType;t.on("data",function(t,e){i.push(t),o&&o(e)}).on("error",function(t){i=[],r(t)}).on("end",function(){try{var t=function(t,e,r){switch(t){case"blob":return h.newBlob(h.transformTo("arraybuffer",e),r);case"base64":return u.encode(e);default:return h.transformTo(t,e)}}(s,function(t,e){var r,i=0,n=null,s=0;for(r=0;r>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e}(t)},s.utf8decode=function(t){return h.nodebuffer?o.transformTo("nodebuffer",t).toString("utf-8"):function(t){var e,r,i,n,s=t.length,a=new Array(2*s);for(e=r=0;e>10&1023,a[r++]=56320|1023&i)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(t=o.transformTo(h.uint8array?"uint8array":"array",t))},o.inherits(a,i),a.prototype.processChunk=function(t){var e=o.transformTo(h.uint8array?"uint8array":"array",t.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=e;(e=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),e.set(r,this.leftOver.length)}else e=this.leftOver.concat(e);this.leftOver=null}var i=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}(e),n=e;i!==e.length&&(h.uint8array?(n=e.subarray(0,i),this.leftOver=e.subarray(i,e.length)):(n=e.slice(0,i),this.leftOver=e.slice(i,e.length))),this.push({data:s.utf8decode(n),meta:t.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,i),l.prototype.processChunk=function(t){this.push({data:s.utf8encode(t.data),meta:t.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(t,e,a){"use strict";var o=t("./support"),h=t("./base64"),r=t("./nodejsUtils"),i=t("set-immediate-shim"),u=t("./external");function n(t){return t}function l(t,e){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==t&&(this.dosPermissions=63&this.externalFileAttributes),3==t&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(t){if(this.extraFields[1]){var e=i(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(t){var e,r,i,n=t.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});t.index>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e},r.buf2binstring=function(t){return l(t,t.length)},r.binstring2buf=function(t){for(var e=new h.Buf8(t.length),r=0,i=e.length;r>10&1023,o[i++]=56320|1023&n)}return l(o,i)},r.utf8border=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}},{"./common":41}],43:[function(t,e,r){"use strict";e.exports=function(t,e,r,i){for(var n=65535&t|0,s=t>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}},{}],46:[function(t,e,r){"use strict";var h,d=t("../utils/common"),u=t("./trees"),c=t("./adler32"),p=t("./crc32"),i=t("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,n=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(t,e){return t.msg=i[e],e}function T(t){return(t<<1)-(4t.avail_out&&(r=t.avail_out),0!==r&&(d.arraySet(t.output,e.pending_buf,e.pending_out,r,t.next_out),t.next_out+=r,e.pending_out+=r,t.total_out+=r,t.avail_out-=r,e.pending-=r,0===e.pending&&(e.pending_out=0))}function N(t,e){u._tr_flush_block(t,0<=t.block_start?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,F(t.strm)}function U(t,e){t.pending_buf[t.pending++]=e}function P(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function L(t,e){var r,i,n=t.max_chain_length,s=t.strstart,a=t.prev_length,o=t.nice_match,h=t.strstart>t.w_size-z?t.strstart-(t.w_size-z):0,u=t.window,l=t.w_mask,f=t.prev,d=t.strstart+S,c=u[s+a-1],p=u[s+a];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(u[(r=e)+a]===p&&u[r+a-1]===c&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--n);return a<=t.lookahead?a:t.lookahead}function j(t){var e,r,i,n,s,a,o,h,u,l,f=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=f+(f-z)){for(d.arraySet(t.window,t.window,f,f,0),t.match_start-=f,t.strstart-=f,t.block_start-=f,e=r=t.hash_size;i=t.head[--e],t.head[e]=f<=i?i-f:0,--r;);for(e=r=f;i=t.prev[--e],t.prev[e]=f<=i?i-f:0,--r;);n+=f}if(0===t.strm.avail_in)break;if(a=t.strm,o=t.window,h=t.strstart+t.lookahead,u=n,l=void 0,l=a.avail_in,u=x)for(s=t.strstart-t.insert,t.ins_h=t.window[s],t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x)if(i=u._tr_tally(t,t.strstart-t.match_start,t.match_length-x),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=x){for(t.match_length--;t.strstart++,t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x&&t.match_length<=t.prev_length){for(n=t.strstart+t.lookahead-x,i=u._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-x),t.lookahead-=t.prev_length-1,t.prev_length-=2;++t.strstart<=n&&(t.ins_h=(t.ins_h<t.pending_buf_size-5&&(r=t.pending_buf_size-5);;){if(t.lookahead<=1){if(j(t),0===t.lookahead&&e===l)return A;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+r;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,N(t,!1),0===t.strm.avail_out))return A;if(t.strstart-t.block_start>=t.w_size-z&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):(t.strstart>t.block_start&&(N(t,!1),t.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(t,e){return Y(t,e,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?_:(t.state.gzhead=e,m):_},r.deflate=function(t,e){var r,i,n,s;if(!t||!t.state||5>8&255),U(i,i.gzhead.time>>16&255),U(i,i.gzhead.time>>24&255),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(U(i,255&i.gzhead.extra.length),U(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=p(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(U(i,0),U(i,0),U(i,0),U(i,0),U(i,0),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,3),i.status=E);else{var a=v+(i.w_bits-8<<4)<<8;a|=(2<=i.strategy||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(a|=32),a+=31-a%31,i.status=E,P(i,a),0!==i.strstart&&(P(i,t.adler>>>16),P(i,65535&t.adler)),t.adler=1}if(69===i.status)if(i.gzhead.extra){for(n=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending!==i.pending_buf_size));)U(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73)}else i.status=73;if(73===i.status)if(i.gzhead.name){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.gzindex=0,i.status=91)}else i.status=91;if(91===i.status)if(i.gzhead.comment){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.status=103)}else i.status=103;if(103===i.status&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&F(t),i.pending+2<=i.pending_buf_size&&(U(i,255&t.adler),U(i,t.adler>>8&255),t.adler=0,i.status=E)):i.status=E),0!==i.pending){if(F(t),0===t.avail_out)return i.last_flush=-1,m}else if(0===t.avail_in&&T(e)<=T(r)&&e!==f)return R(t,-5);if(666===i.status&&0!==t.avail_in)return R(t,-5);if(0!==t.avail_in||0!==i.lookahead||e!==l&&666!==i.status){var o=2===i.strategy?function(t,e){for(var r;;){if(0===t.lookahead&&(j(t),0===t.lookahead)){if(e===l)return A;break}if(t.match_length=0,r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):3===i.strategy?function(t,e){for(var r,i,n,s,a=t.window;;){if(t.lookahead<=S){if(j(t),t.lookahead<=S&&e===l)return A;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=x&&0t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=x?(r=u._tr_tally(t,1,t.match_length-x),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):h[i.level].func(i,e);if(o!==O&&o!==B||(i.status=666),o===A||o===O)return 0===t.avail_out&&(i.last_flush=-1),m;if(o===I&&(1===e?u._tr_align(i):5!==e&&(u._tr_stored_block(i,0,0,!1),3===e&&(D(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),F(t),0===t.avail_out))return i.last_flush=-1,m}return e!==f?m:i.wrap<=0?1:(2===i.wrap?(U(i,255&t.adler),U(i,t.adler>>8&255),U(i,t.adler>>16&255),U(i,t.adler>>24&255),U(i,255&t.total_in),U(i,t.total_in>>8&255),U(i,t.total_in>>16&255),U(i,t.total_in>>24&255)):(P(i,t.adler>>>16),P(i,65535&t.adler)),F(t),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new d.Buf8(r.w_size),d.arraySet(u,e,l-r.w_size,r.w_size,0),e=u,l=r.w_size),a=t.avail_in,o=t.next_in,h=t.input,t.avail_in=l,t.next_in=0,t.input=e,j(r);r.lookahead>=x;){for(i=r.strstart,n=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(c&(1<>>=y,p-=y),p<15&&(c+=z[i++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(c&(1<>>=y,p-=y,(y=s-a)>3,c&=(1<<(p-=w<<3))-1,t.next_in=i,t.next_out=s,t.avail_in=i>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=P,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new I.Buf32(i),e.distcode=e.distdyn=new I.Buf32(n),e.sane=1,e.back=-1,N):U}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,a(t)):U}function h(t,e){var r,i;return t&&t.state?(i=t.state,e<0?(r=0,e=-e):(r=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15=s.wsize?(I.arraySet(s.window,e,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(i<(n=s.wsize-s.wnext)&&(n=i),I.arraySet(s.window,e,r-i,n,s.wnext),(i-=n)?(I.arraySet(s.window,e,r-i,i,0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){t.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){t.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){t.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(c=r.length)&&(c=o),c&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,i,s,c,k)),512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,r.length-=c),r.length))break t;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break t;for(c=0;k=i[s+c++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&c>9&1,r.head.done=!0),t.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break t;o--,u+=i[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==e)break;u>>>=2,l-=2;break t;case 2:r.mode=17;break;case 3:t.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break t;o--,u+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===e)break t;case 15:r.mode=16;case 16:if(c=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){t.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],c=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+c>r.nlen+r.ndist){t.msg="invalid bit length repeat",r.mode=30;break}for(;c--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){t.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){t.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===e)break t;case 20:r.mode=21;case 21:if(6<=o&&258<=h){t.next_out=a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,R(t,d),a=t.next_out,n=t.output,h=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){t.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){t.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){t.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break t;if(c=d-h,r.offset>c){if((c=r.offset-c)>r.whave&&r.sane){t.msg="invalid distance too far back",r.mode=30;break}p=c>r.wnext?(c-=r.wnext,r.wsize-c):r.wnext-c,c>r.length&&(c=r.length),m=r.window}else m=n,p=a-r.offset,c=r.length;for(hc?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=e[r+a[v]]}if(k>>7)]}function U(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function P(t,e,r){t.bi_valid>c-r?(t.bi_buf|=e<>c-t.bi_valid,t.bi_valid+=r-c):(t.bi_buf|=e<>>=1,r<<=1,0<--e;);return r>>>1}function Z(t,e,r){var i,n,s=new Array(g+1),a=0;for(i=1;i<=g;i++)s[i]=a=a+r[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=j(s[o]++,o))}}function W(t){var e;for(e=0;e>1;1<=r;r--)G(t,s,r);for(n=h;r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],G(t,s,1),i=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=i,s[2*n]=s[2*r]+s[2*i],t.depth[n]=(t.depth[r]>=t.depth[i]?t.depth[r]:t.depth[i])+1,s[2*r+1]=s[2*i+1]=n,t.heap[1]=n++,G(t,s,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var r,i,n,s,a,o,h=e.dyn_tree,u=e.max_code,l=e.stat_desc.static_tree,f=e.stat_desc.has_stree,d=e.stat_desc.extra_bits,c=e.stat_desc.extra_base,p=e.stat_desc.max_length,m=0;for(s=0;s<=g;s++)t.bl_count[s]=0;for(h[2*t.heap[t.heap_max]+1]=0,r=t.heap_max+1;r<_;r++)p<(s=h[2*h[2*(i=t.heap[r])+1]+1]+1)&&(s=p,m++),h[2*i+1]=s,u>=7;i>>=1)if(1&r&&0!==t.dyn_ltree[2*e])return o;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e>>3,(s=t.static_len+3+7>>>3)<=n&&(n=s)):n=s=r+5,r+4<=n&&-1!==e?J(t,e,r,i):4===t.strategy||s===n?(P(t,2+(i?1:0),3),K(t,z,C)):(P(t,4+(i?1:0),3),function(t,e,r,i){var n;for(P(t,e-257,5),P(t,r-1,5),P(t,i-4,4),n=0;n>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&r,t.last_lit++,0===e?t.dyn_ltree[2*r]++:(t.matches++,e--,t.dyn_ltree[2*(A[r]+u+1)]++,t.dyn_dtree[2*N(e)]++),t.last_lit===t.lit_bufsize-1},r._tr_align=function(t){P(t,2,3),L(t,m,z),function(t){16===t.bi_valid?(U(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):8<=t.bi_valid&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},{"../utils/common":41}],53:[function(t,e,r){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,e,r){"use strict";e.exports="function"==typeof setImmediate?setImmediate:function(){var t=[].slice.apply(arguments);t.splice(1,0,0),setTimeout.apply(null,t)}},{}]},{},[10])(10)}); \ No newline at end of file diff --git a/static/js/jszip/learnocaml_jszip_wrapper.js b/static/js/jszip/learnocaml_jszip_wrapper.js new file mode 100644 index 000000000..e46296562 --- /dev/null +++ b/static/js/jszip/learnocaml_jszip_wrapper.js @@ -0,0 +1,106 @@ +//keep in sync with editor export datatype +function editor_create_exercise(zip, data, prefix) { + zip.file(prefix + "descr.md", data.exercise.descr); + zip.file(prefix + "meta.json", JSON.stringify(data.metadata, null, 2)); + zip.file(prefix + "prelude.ml", data.exercise.prelude); + zip.file(prefix + "prepare.ml", data.exercise.prepare); + zip.file(prefix + "template.ml", data.exercise.template); + zip.file(prefix + "test.ml", data.exercise.test); + zip.file(prefix + "solution.ml", data.exercise.solution); +} + +function editor_download(brut_data, callback) { + var zip = new JSZip(); + var data = JSON.parse(brut_data); + var dirname = data.exercise.id + zip.folder(dirname); + editor_create_exercise(zip, data, dirname + "/"); + zip.generateAsync({ + type: "blob", + compression: "DEFLATE", + compressionOptions: { + level: 9 + } + }).then(function(blob) { callback(blob) }); +} + +function editor_download_all(brut_exercises, brut_index, callback) { + var zip = new JSZip(); + var all_data = JSON.parse(brut_exercises); + Object.keys(all_data).forEach(function(k) { + zip.folder(k); + var prefix = k + "/"; + editor_create_exercise(zip, all_data[k], prefix) + }); + var index = JSON.stringify(JSON.parse(brut_index), null, 2); + zip.file("index.json", index); + zip.generateAsync({ + type: "blob", + compression: "DEFLATE", + compressionOptions: { + level: 9 + } + }).then(function(blob) { callback(blob) }); +} + +function editor_read_file(loaded_zip, file) { + if (loaded_zip.file(file)) + return loaded_zip.file(file).async("string") + else + return Promise.resolve(""); +} + +function editor_read_exercise(loaded_zip, path, id) { + return new Promise(function(resolve, reject) { + var descr = editor_read_file(loaded_zip, path + "descr.md"); + var meta = editor_read_file(loaded_zip, path + "meta.json"); + var prelude = editor_read_file(loaded_zip, path + "prelude.ml"); + var prepare = editor_read_file(loaded_zip, path + "prepare.ml"); + var template = editor_read_file(loaded_zip, path + "template.ml"); + var test = editor_read_file(loaded_zip, path + "test.ml"); + var solution = editor_read_file(loaded_zip, path + "solution.ml"); + Promise.all([descr, meta, prelude, prepare, template, test, solution]) + .then(function(values) { + var result = { exercise: {}, metadata: {} }; + result.exercise.max_score = 0; + result.exercise.id = id; + result.exercise.descr = values[0]; + var brut_meta = values[1]; + var meta = brut_meta.replace(/\r?\n|\r/g, " "); + if (meta == "") meta = "{}"; + result.metadata = JSON.parse(meta); + result.exercise.prelude = values[2]; + result.exercise.prepare = values[3]; + result.exercise.template = values[4]; + result.exercise.test = values[5]; + result.exercise.solution = values[6]; + resolve(result); + }) + }) +} + +//also to keep in sync +function editor_import(brut_data, callback) { + var zip = new JSZip(); + zip.loadAsync(brut_data) + .then(function(loaded_zip) { + var promises = []; + loaded_zip.forEach(function(relative_path, file) { + if (file.dir) { + var slash_regex = /\//g; + var depth = file.name.match(slash_regex).length; + if (depth == 1) { + var promise = editor_read_exercise(loaded_zip, relative_path, file.name.replace(/\//, "")); + promises.push(promise); + } + } + }); + Promise.all(promises).then(function(values) { + var result = values.reduce(function(acc, elt) { + acc[elt.exercise.id] = elt; + return acc; + }, {}) + callback(JSON.stringify(result)); + }) + }); +} \ No newline at end of file