diff --git a/demo-repository/exercises/demo2/meta.json b/demo-repository/exercises/demo2/meta.json index 6f1d64e8b..bcde52b7e 100644 --- a/demo-repository/exercises/demo2/meta.json +++ b/demo-repository/exercises/demo2/meta.json @@ -1,2 +1,2 @@ -{"learnocaml_version":"1","kind":"exercise","stars":0, +{"learnocaml_version":"1","kind":"exercise","stars":1, "title":"Demo of the exercise environment (MD version)"} diff --git a/src/app/dune b/src/app/dune index addb087bb..c0230bcc1 100644 --- a/src/app/dune +++ b/src/app/dune @@ -25,9 +25,19 @@ learnocaml_data learnocaml_api sha + re.pcre ocplib_i18n) ) +(library + (name learnocaml_app_common_test) + (wrapped false) + (modules Learnocaml_app_common_test) + (libraries learnocaml_app_common) + (inline_tests) + (preprocess (pps ppx_expect ppx_inline_test)) +) + (executable (name learnocaml_index_main) (modes byte js) @@ -35,6 +45,7 @@ (libraries ezjsonm ace sha + re.pcre learnocaml_repository learnocaml_app_common learnocaml_toplevel @@ -141,5 +152,4 @@ (learnocaml_description_main.bc.js as www/js/learnocaml-description.js) (learnocaml_partition_view.bc.js as www/js/learnocaml-partition-view.js) (learnocaml_playground_main.bc.js as www/js/learnocaml-playground.js)) -) - +) \ No newline at end of file diff --git a/src/app/learnocaml_app_common_test.ml b/src/app/learnocaml_app_common_test.ml new file mode 100644 index 000000000..9a4075545 --- /dev/null +++ b/src/app/learnocaml_app_common_test.ml @@ -0,0 +1,112 @@ +open Learnocaml_common + + +let to_test_encode s = Printf.printf "%s " (encode s) +let to_test_decode s = Printf.printf "%s " (decode s) +let to_test_bijection1 s = Printf.printf "%s " (decode (encode s)) +let to_test_bijection2 s = Printf.printf "%s " (encode (decode s)) + +let original_e = ["=test";"test=test";"test=";"==";""] +let coded_e = ["-etest";"test-etest";"test-e";"-e-e";""] + +let original_c = [",test";"test,test";"test,";",,";""] +let coded_c = ["-ctest";"test-ctest";"test-c";"-c-c";""] + +let original_a = ["&test";"test&test";"test&";"&&";""] +let coded_a = ["-atest";"test-atest";"test-a";"-a-a";""] + +let original__ = ["-test";"test-test";"test-";"--";""] +let coded__ = ["--test";"test--test";"test--";"----";""] + +let original_all = ["=,&-";"=,-&";"=&,-";"=&-,";"=-,&";"=-&,"; + ",=&-";",=-&";",&=-";",&-=";",-=&";",-&="; + "&=,-";"&=-,";"&,=-";"&,-=";"&-=,";"&-,="; + "-=,&";"-=&,";"-,=&";"-,&=";"-&=,";"-&,="] +let codedall = ["-e-c-a--";"-e-c---a";"-e-a-c--";"-e-a---c";"-e---c-a";"-e---a-c"; + "-c-e-a--";"-c-e---a";"-c-a-e--";"-c-a---e";"-c---e-a";"-c---a-e"; + "-a-e-c--";"-a-e---c";"-a-c-e--";"-a-c---e";"-a---e-c";"-a---c-e"; + "---e-c-a";"---e-a-c";"---c-a-e";"---c-a-e";"---a-e-c";"---a-c-e"] + + +let%expect_test "encode -e" = + List.iter to_test_encode original_e; + [%expect {|-etest test-etest test-e -e-e |}] + +let%expect_test "encode -c" = + List.iter to_test_encode original_c; + [%expect {|-ctest test-ctest test-c -c-c |}] + +let%expect_test "encode -a" = + List.iter to_test_encode original_a; + [%expect {|-atest test-atest test-a -a-a |}] + +let%expect_test "encode --" = + List.iter to_test_encode original__; + [%expect {|--test test--test test-- ---- |}] + +let%expect_test "encode all" = + List.iter to_test_encode original_all; + [%expect {|-e-c-a-- -e-c---a -e-a-c-- -e-a---c -e---c-a -e---a-c -c-e-a-- -c-e---a -c-a-e-- -c-a---e -c---e-a -c---a-e -a-e-c-- -a-e---c -a-c-e-- -a-c---e -a---e-c -a---c-e ---e-c-a ---e-a-c ---c-a-e ---c-a-e ---a-e-c ---a-c-e |}] + + +let%expect_test "decode -e" = + List.iter to_test_decode coded_e; + [%expect {|=test test=test test= == |}] + +let%expect_test "decode -c" = + List.iter to_test_decode coded_c; + [%expect {|,test test,test test, ,, |}] + +let%expect_test "decode -a" = + List.iter to_test_decode coded_a; + [%expect {|&test test&test test& && |}] + +let%expect_test "decode --" = + List.iter to_test_decode coded_e; + [%expect {|-test test-test test- -- |}] + +let%expect_test "decode all" = + List.iter to_test_decode codedall; + [%expect {|=,&- =,-& =&,- =&-, =-,& =-&, ,=&- ,=-& ,&=- ,&-= ,-=& ,-&= &=,- &=-, &,=- &,-= &-=, &-,= -=,& -=&, -,=& -,&= -&=, -&,= |}] + + +let%expect_test "bijection 1 -e" = + List.iter to_test_bijection1 original_e; + [%expect {|=test test=test test= == |}] + +let%expect_test "bijection 1 -c" = + List.iter to_test_bijection1 original_c; + [%expect {|,test test,test test, ,, |}] + +let%expect_test "bijection 1 -a" = + List.iter to_test_bijection1 original_a; + [%expect {|&test test&test test& && |}] + +let%expect_test "bijection 1 --" = + List.iter to_test_bijection1 original__; + [%expect {|-test test-test test- -- |}] + +let%expect_test "bijection 1 all" = + List.iter to_test_bijection1 original_all; + [%expect {|=,&- =,-& =&,- =&-, =-,& =-&, ,=&- ,=-& ,&=- ,&-= ,-=& ,-&= &=,- &=-, &,=- &,-= &-=, &-,= -=,& -=&, -,=& -,&= -&=, -&,= |}] + + +let%expect_test "bijection 2 -e" = + List.iter to_test_bijection2 coded_e; + [%expect {|-etest test-etest test-e -e-e |}] + +let%expect_test "bijection 2 -c" = + List.iter to_test_bijection2 coded_c; + [%expect {|-ctest test-ctest test-c -c-c |}] + +let%expect_test "bijection 2 -a" = + List.iter to_test_bijection2 coded_a; + [%expect {|-atest test-atest test-a -a-a |}] + +let%expect_test "bijection 2 --" = + List.iter to_test_bijection2 coded__; + [%expect {|--test test--test test-- ---- |}] + +let%expect_test "bijection 2 all" = + List.iter to_test_bijection2 codedall; + [%expect {|-e-c-a-- -e-c---a -e-a-c-- -e-a---c -e---c-a -e---a-c -c-e-a-- -c-e---a -c-a-e-- -c-a---e -c---e-a -c---a-e -a-e-c-- -a-e---c -a-c-e-- -a-c---e -a---e-c -a---c-e ---e-c-a ---e-a-c ---c-a-e ---c-a-e ---a-e-c ---a-c-e |}] \ No newline at end of file diff --git a/src/app/learnocaml_common.ml b/src/app/learnocaml_common.ml index a5a12d2d6..b87f08276 100644 --- a/src/app/learnocaml_common.ml +++ b/src/app/learnocaml_common.ml @@ -13,9 +13,21 @@ open Js_utils open Lwt.Infix open Learnocaml_data open Learnocaml_config +open Re.Pcre module H = Tyxml_js.Html +let encode str = + Re.Pcre.substitute ~rex:(Re.Pcre.regexp ",") ~subst:(fun _ -> "-c") ( + Re.Pcre.substitute ~rex:(Re.Pcre.regexp "&") ~subst:(fun _ -> "-a") ( + Re.Pcre.substitute ~rex:(Re.Pcre.regexp "=") ~subst:(fun _ -> "-e") ( + Re.Pcre.substitute ~rex:(Re.Pcre.regexp "-") ~subst:(fun _ -> "--") str))) +let decode str = + Re.Pcre.substitute ~rex:(Re.Pcre.regexp "--") ~subst:(fun _ -> "-") ( + Re.Pcre.substitute ~rex:(Re.Pcre.regexp "-e") ~subst:(fun _ -> "=") ( + Re.Pcre.substitute ~rex:(Re.Pcre.regexp "-a") ~subst:(fun _ -> "&") ( + Re.Pcre.substitute ~rex:(Re.Pcre.regexp "-c") ~subst:(fun _ -> ",") str))) + let find_div_or_append_to_body id = match Manip.by_id id with | Some div -> div diff --git a/src/app/learnocaml_common.mli b/src/app/learnocaml_common.mli index eff755d06..1d0f8f8ae 100644 --- a/src/app/learnocaml_common.mli +++ b/src/app/learnocaml_common.mli @@ -10,6 +10,10 @@ open Js_of_ocaml open Js_of_ocaml_tyxml open Learnocaml_data +val encode : string -> string + +val decode : string -> string + val find_div_or_append_to_body : string -> [> Html_types.div ] Tyxml_js.Html.elt val find_component : string -> 'a Tyxml_js.Html.elt diff --git a/src/app/learnocaml_index_main.ml b/src/app/learnocaml_index_main.ml index 29a3d9dc6..8f5160e51 100644 --- a/src/app/learnocaml_index_main.ml +++ b/src/app/learnocaml_index_main.ml @@ -10,11 +10,13 @@ open Js_of_ocaml open Js_of_ocaml_tyxml open Js_of_ocaml_lwt open Js_utils +open Re.Pcre open Lwt open Learnocaml_data open Learnocaml_common open Learnocaml_config + module H = Tyxml_js.Html5 module El = struct @@ -85,8 +87,85 @@ let (exercise_filter_signal: string option React.signal), set_exercise_filter = let (exercise_sort_signal: exercise_ordering React.signal), set_exercise_sort = React.S.create By_category + +let (expand_state_signal: string list React.signal), set_expand_state = + React.S.create [] + +let join_ids l = + match l with + | [] -> "" + | h::t -> List.fold_left (fun acc x -> acc ^ "," ^ x) h t + +let split_ids str = + Re.Pcre.split ~rex:(Re.Pcre.regexp ",") str + +let encode str = + let open Re.Pcre in + substitute ~rex:(regexp ",") ~subst:(fun _ -> "-c") + @@ substitute ~rex:(regexp "&") ~subst:(fun _ -> "-a") + @@ substitute ~rex:(regexp "=") ~subst:(fun _ -> "-e") + @@ substitute ~rex:(regexp "-") ~subst:(fun _ -> "--") + @@ str + +let decode str = + let open Re.Pcre in + substitute ~rex:(regexp "--") ~subst:(fun _ -> "-") + @@ substitute ~rex:(regexp "-e") ~subst:(fun _ -> "=") + @@ substitute ~rex:(regexp "-a") ~subst:(fun _ -> "&") + @@ substitute ~rex:(regexp "-c") ~subst:(fun _ -> ",") + @@ str + +(* Update the expand list in fragment to fit the URL *) +let rec update_expand ?value fragment = + match value with + | Some v -> + if List.mem_assoc "expand" fragment then + match fragment with + | [] -> [] + | ("expand", x)::t -> + let expand_list = split_ids x in + let new_expand_list = + if List.mem v expand_list then + begin + let filtered_list = List.filter (fun x -> x <> v) expand_list in + if filtered_list = [] then [""] else filtered_list + end + else + begin + if expand_list = [""] then [v] else v :: expand_list + end + in + let joined_expand = join_ids new_expand_list in + set_expand_state new_expand_list; + ("expand", joined_expand)::t + | h::t -> + h::(update_expand ~value:v t) + else + begin + set_expand_state [v]; + fragment @ [("expand", v)] + end + | None -> + set_expand_state []; + List.filter (fun (k, _) -> k <> "expand") fragment + +let update_sort value fragment = + let filtered_fragment = List.remove_assoc "sort" (update_expand fragment) in + filtered_fragment @ [("sort", value)] @ [("expand", "")] + +let update_fragment key value = + let fragment = Js_utils.parse_fragment () in + let filtered_fragment = + if key = "expand" then + let v = (Uri.pct_encode (encode value)) in + update_expand ~value:v fragment + else + update_sort value fragment + in + Js_utils.set_fragment filtered_fragment + let make_exercises_to_display_signal index = - let get_index exo_sort exo_filter = + let get_index exo_sort exo_filter _expand = let index = match exo_sort with | By_category -> index @@ -113,7 +192,7 @@ let make_exercises_to_display_signal index = StrMap.fold (fun skill exercises acc -> (skill, {Exercise.Index.title = skill; - contents = Exercise.Index.Exercises (List.rev exercises)}) + contents = Exercise.Index.Exercises (List.rev exercises)}) :: acc) by_skill [] in @@ -165,7 +244,7 @@ let make_exercises_to_display_signal index = Exercise.Index.contents = Exercise.Index.Exercises []; }] else index in - React.S.l2 get_index exercise_sort_signal exercise_filter_signal + React.S.l3 get_index exercise_sort_signal exercise_filter_signal expand_state_signal let retain_signals = ref (React.S.const ()) (* Used to register signals as GC roots *) @@ -173,6 +252,19 @@ let retain_signals = ref (React.S.const ()) let exercises_tab token : tab_handler = fun _ _ () -> let open Tyxml_js.Html5 in + let () = + Dom_html.window##.onhashchange := Dom_html.handler (fun _ -> + let fragment = Js_utils.parse_fragment () in + (match List.assoc_opt "sort" fragment with + | Some "category" -> set_exercise_sort By_category + | Some "skill" -> set_exercise_sort By_skill + | Some "difficulty" -> set_exercise_sort By_difficulty + | _ -> ()); + (match List.assoc_opt "expand" fragment with + | None -> set_expand_state [] + | Some e -> set_expand_state (split_ids e)); + Js._true) + in show_loading [%i"Loading exercises"] @@ fun () -> Lwt_js.sleep 0.5 >>= fun () -> retrieve (Learnocaml_api.Exercise_index token) @@ -236,6 +328,12 @@ let exercises_tab token : tab_handler = ] ] ] in + let update_expand_class id ids = + if List.mem id ids then + [] + else + ["collapsed"] + in let rec format_exercise_list index = match index with | Exercise.Index.Exercises el -> @@ -248,7 +346,25 @@ let exercises_tab token : tab_handler = List.map (fun (id, grp) -> let clas = "group-title" :: - match gl with [] | [_] -> [] | _ -> ["collapsed"] + match gl with + | [] -> [] + | [_] -> + let expand_ids = React.S.value expand_state_signal in + if expand_ids = [] then + begin + let encoded = encode id in + set_expand_state [(encoded)]; + update_fragment "expand" (encoded); + [] + end + else + update_expand_class (encode id) expand_ids + | _ -> + let expand_ids = React.S.value expand_state_signal in + if expand_ids = [] then + ["collapsed"] + else + update_expand_class (encode id) expand_ids in let title = H.div ~a:[a_id id; a_class clas] @@ -257,8 +373,9 @@ let exercises_tab token : tab_handler = let exos = format_exercise_list grp.Exercise.Index.contents in Manip.Ev.onclick title (fun _ -> - ignore (Manip.toggleClass title "collapsed"); - false); + update_fragment "expand" id; + ignore (Manip.toggleClass title "collapsed"); + false); H.li [title; exos]) gl in @@ -269,7 +386,14 @@ let exercises_tab token : tab_handler = List.map (fun (id, sort, name) -> let btn = button ~a:[a_id id] [ txt name ] in Manip.Ev.onclick btn - (fun _ -> set_exercise_sort sort; true); + (fun _ -> + let sort_value = + match sort with + | By_category -> "category" + | By_skill -> "skill" + | By_difficulty -> "difficulty" + in + update_fragment "sort" sort_value; set_exercise_sort sort; true); let signal = React.S.map (fun s -> (if sort = s then Manip.addClass else Manip.removeClass) @@ -311,6 +435,17 @@ let exercises_tab token : tab_handler = in retain_signals := React.S.merge (fun () () -> ()) () (list_update_signal :: btns_sigs); + let () = + ignore (React.S.map (fun expanded_ids -> + List.iter (fun id -> + match Dom_html.getElementById_opt id with + | Some elt -> + let class_list = elt##.classList in + if Js.to_bool (class_list##contains (Js.string "group-title")) && + Js.to_bool (class_list##contains (Js.string "collapsed")) + then ignore (class_list##remove (Js.string "collapsed")) + | None -> ()) expanded_ids) expand_state_signal) + in Lwt.return pane_div let playground_tab token : tab_handler = diff --git a/src/state/learnocaml_common_test.ml b/src/state/learnocaml_common_test.ml new file mode 100644 index 000000000..32931042b --- /dev/null +++ b/src/state/learnocaml_common_test.ml @@ -0,0 +1,57 @@ +open Learnocaml_common + +let pred_bijection s = decode (encode s) = s +let print_encode s = Printf.printf "%s " (encode s) + + +let testcase_eq = ["=test";"test=test";"test=";"==";""] + +let%expect_test "encode '=' as '-e'" = + List.iter print_encode testcase_eq; + [%expect_exact {|-etest test-etest test-e -e-e |}] + +let%test "decode @@ encode == id" = + List.for_all pred_bijection testcase_eq + + +let testcase_co = [",test"; "test,test"; "test,"; ",,"; ""] + +let%expect_test "encode ',' as '-c'" = + List.iter print_encode testcase_co; + [%expect_exact {|-ctest test-ctest test-c -c-c |}] + +let%test "decode @@ encode == id" = + List.for_all pred_bijection testcase_co + + +let testcase_an = ["&test"; "test&test"; "test&"; "&&"; ""] + +let%expect_test "encode '&' as '-a'" = + List.iter print_encode testcase_an; + [%expect_exact {|-atest test-atest test-a -a-a |}] + +let%test "decode @@ encode == id" = + List.for_all pred_bijection testcase_an + + +let testcase_da = ["-test"; "test-test"; "test-"; "--"; ""] + +let%expect_test "encode '-' as '--'" = + List.iter print_encode testcase_co; + [%expect_exact {|--test test--test test-- ---- |}] + +let%test "decode @@ encode == id" = + List.for_all pred_bijection testcase_da + + +let testcase_all = ["=,&-";"=,-&";"=&,-";"=&-,";"=-,&";"=-&,"; + ",=&-";",=-&";",&=-";",&-=";",-=&";",-&="; + "&=,-";"&=-,";"&,=-";"&,-=";"&-=,";"&-,="; + "-=,&";"-=&,";"-,=&";"-,&=";"-&=,";"-&,="] + +let%expect_test "encode all possibilities" = + List.iter print_encode testcase_co; + [%expect_exact {|-e-c-a-- -e-c---a -e-a-c-- -e-a---c -e---c-a -e---a-c -c-e-a-- -c-e---a -c-a-e-- -c-a---e -c---e-a -c---a-e -a-e-c-- -a-e---c -a-c-e-- -a-c---e -a---e-c -a---c-e ---e-c-a ---e-a-c ---c-a-e ---c-a-e ---a-e-c ---a-c-e |}] + +let%test "decode @@ encode == id" = + List.for_all pred_bijection testcase_all \ No newline at end of file