simple-learning

Simple learning web program
git clone git://git.laack.co/simple-learning.git
Log | Files | Refs | README | LICENSE

commit e7e0ebb160e3e2636a8c1afd20dee2b15e4374c1
parent 69e4c48687c778834244e47a84dcc8bd34a79c84
Author: Andrew Laack <andrew@laack.co>
Date:   Wed,  2 Sep 2026 11:53:21 -0500

ocaml formatting

Diffstat:
Alearning/.ocamlformat | 1+
Mlearning/bin/main.ml | 8+++-----
Mlearning/lib/basic_functions.ml | 36++++++++++++++++--------------------
Mlearning/lib/parser.ml | 236+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mlearning/lib/render.ml | 91+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mlearning/lib/server.ml | 117++++++++++++++++++++++++++++++++-----------------------------------------------
Mlearning/lib/shared.ml | 3++-
Mlearning/lib/templates.ml | 130++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mlearning/test/dune | 4++--
Mlearning/test/test_learning.ml | 4++--
Mlearning/test/test_line_parsing.ml | 164++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mlearning/test/test_rendering.ml | 72+++++++++++++++++++++++++++++++++---------------------------------------
Mlearning/test/test_server.ml | 80++++++++++++++++++++++++++++++++++++++++++-------------------------------------
13 files changed, 486 insertions(+), 460 deletions(-)

diff --git a/learning/.ocamlformat b/learning/.ocamlformat @@ -0,0 +1 @@ +version = 0.29.0 diff --git a/learning/bin/main.ml b/learning/bin/main.ml @@ -1,7 +1,5 @@ open Learning -let () = - Dream.run ~error_handler:(Server.handler) - @@ Dream.logger - @@ Dream.router Server.routes - +let () = + Dream.run ~error_handler:Server.handler + @@ Dream.logger @@ Dream.router Server.routes diff --git a/learning/lib/basic_functions.ml b/learning/lib/basic_functions.ml @@ -1,26 +1,22 @@ -let remove_file_extension file_name = - let fin_period_pos = String.rindex file_name '.' in - String.sub file_name 0 fin_period_pos +let remove_file_extension file_name = + let fin_period_pos = String.rindex file_name '.' in + String.sub file_name 0 fin_period_pos -let count_prefix char data = - let rec go idx = - if String.length data > idx && data.[idx] = char - then - go (idx + 1) - else idx - in go 0 +let count_prefix char data = + let rec go idx = + if String.length data > idx && data.[idx] = char then go (idx + 1) else idx + in + go 0 (* Assumes length >= 3*) let has_sl_postfix s = - let len = String.length(s) in - s.[len - 1] = 'l' && s.[len - 2] = 's' && s.[len - 3] = '.' + let len = String.length s in + s.[len - 1] = 'l' && s.[len - 2] = 's' && s.[len - 3] = '.' -let prepend prefix s = - prefix ^ s - -let arr_to_string arr = - let rec go acc idx = - if idx >= Array.length(arr) then acc - else go (acc ^ arr.(idx)) (idx + 1) - in go "" 0 +let prepend prefix s = prefix ^ s +let arr_to_string arr = + let rec go acc idx = + if idx >= Array.length arr then acc else go (acc ^ arr.(idx)) (idx + 1) + in + go "" 0 diff --git a/learning/lib/parser.ml b/learning/lib/parser.ml @@ -1,113 +1,149 @@ - (* TODO: These should be doing prefix matching...*) -type line_format = - | Normal - | HeaderL1 - | HeaderL2 - | HeaderL3 - | Embedded - | Preformatted - | Hidden - | Quote - | Link - | ListItem - -let line_format_map format = - match format with - | Normal -> "Normal", 0 - | HeaderL1 -> "HeaderL1", 2 - | HeaderL2 -> "HeaderL2", 3 - | HeaderL3 -> "HeaderL3", 4 - | Embedded -> "Embedded", 4 - | Preformatted -> "Preformatted", 0 - | Hidden -> "Hidden", 3 - | Quote -> "Quote", 2 - | Link -> "Link", 3 - | ListItem -> "ListItem", 2 - -let line_format_str format = - fst (line_format_map format) - -let line_format_char_cut format = - snd (line_format_map format) - -type page_line = { - content: string; - line_type: line_format; -} - -let permute_line line line_type = - let cut = line_format_char_cut line_type in String.sub line cut (String.length(line) - cut) - -let is_hidden line preformatted = - if not preformatted && String.length(line) > 2 && line.[0] = '?' && line.[1] = '?' && line.[2] = ' ' then true else false +type line_format = + | Normal + | HeaderL1 + | HeaderL2 + | HeaderL3 + | Embedded + | Preformatted + | Hidden + | Quote + | Link + | ListItem + +let line_format_map format = + match format with + | Normal -> ("Normal", 0) + | HeaderL1 -> ("HeaderL1", 2) + | HeaderL2 -> ("HeaderL2", 3) + | HeaderL3 -> ("HeaderL3", 4) + | Embedded -> ("Embedded", 4) + | Preformatted -> ("Preformatted", 0) + | Hidden -> ("Hidden", 3) + | Quote -> ("Quote", 2) + | Link -> ("Link", 3) + | ListItem -> ("ListItem", 2) + +let line_format_str format = fst (line_format_map format) +let line_format_char_cut format = snd (line_format_map format) + +type page_line = { content : string; line_type : line_format } + +let permute_line line line_type = + let cut = line_format_char_cut line_type in + String.sub line cut (String.length line - cut) + +let is_hidden line preformatted = + if + (not preformatted) + && String.length line > 2 + && line.[0] = '?' + && line.[1] = '?' + && line.[2] = ' ' + then true + else false let is_list line preformatted = - if not preformatted && String.length(line) > 1 && line.[0] = '*' && line.[1] = ' ' then true else false + if + (not preformatted) + && String.length line > 1 + && line.[0] = '*' + && line.[1] = ' ' + then true + else false let is_quote line preformatted = - if not preformatted && String.length(line) > 1 && line.[0] = '>' && line.[1] = ' ' then true else false + if + (not preformatted) + && String.length line > 1 + && line.[0] = '>' + && line.[1] = ' ' + then true + else false let is_embedded_link line preformatted = - if not preformatted && String.length(line) > 3 && line.[0] = '=' && line.[1] = '>' && line.[2] = 'e' && line.[3] = ' ' then true else false - -let is_link line preformatted = - if not preformatted && not (is_embedded_link line preformatted) && String.length(line) > 2 && line.[0] = '=' && line.[1] = '>' && line.[2] = ' ' then true else false - -let header_count line preformatted = - let count = Basic_functions.count_prefix '#' line in - if not preformatted && String.length line > count && line.[count] = ' ' then count else 0 + if + (not preformatted) + && String.length line > 3 + && line.[0] = '=' + && line.[1] = '>' + && line.[2] = 'e' + && line.[3] = ' ' + then true + else false + +let is_link line preformatted = + if + (not preformatted) + && (not (is_embedded_link line preformatted)) + && String.length line > 2 + && line.[0] = '=' + && line.[1] = '>' + && line.[2] = ' ' + then true + else false + +let header_count line preformatted = + let count = Basic_functions.count_prefix '#' line in + if (not preformatted) && String.length line > count && line.[count] = ' ' then + count + else 0 + +let is_h1 line preformatted = + header_count line preformatted + = 1 (* TODO: Make this more consistent with the others *) -let is_h1 line preformatted = header_count line preformatted = 1 (* TODO: Make this more consistent with the others *) let is_h2 line preformatted = header_count line preformatted = 2 let is_h3 line preformatted = header_count line preformatted = 3 - -let is_preformatted _ preformatted = - preformatted - -let get_line_type line preformatted = - let funs = [ - (Hidden, is_hidden); - (Preformatted, is_preformatted); - (ListItem, is_list); - (Quote, is_quote); - (Embedded, is_embedded_link); - (Link, is_link); - (HeaderL1, is_h1); - (HeaderL2, is_h2); - (HeaderL3, is_h3); - ] in - let lt = (List.find_opt (fun (_, f) -> f line preformatted) funs) in - match lt with - | Some v -> fst v - | None -> Normal - -let parse_simple_line line preformatted = - let cl_type = get_line_type line preformatted in - - {line_type = cl_type; content = (permute_line line cl_type)} - - -let is_format_line cl = - String.length cl = 3 && cl.[0] = '`' && cl.[1] = '`' && cl.[2] = '`' +let is_preformatted _ preformatted = preformatted + +let get_line_type line preformatted = + let funs = + [ + (Hidden, is_hidden); + (Preformatted, is_preformatted); + (ListItem, is_list); + (Quote, is_quote); + (Embedded, is_embedded_link); + (Link, is_link); + (HeaderL1, is_h1); + (HeaderL2, is_h2); + (HeaderL3, is_h3); + ] + in + let lt = List.find_opt (fun (_, f) -> f line preformatted) funs in + match lt with Some v -> fst v | None -> Normal + +let parse_simple_line line preformatted = + let cl_type = get_line_type line preformatted in + + { line_type = cl_type; content = permute_line line cl_type } + +let is_format_line cl = + String.length cl = 3 && cl.[0] = '`' && cl.[1] = '`' && cl.[2] = '`' let read_simple_learning_file fileName = - let ic = open_in fileName in - let rec readAll preformatted acc = - try - let cl = input_line ic in - let current_format = if is_format_line cl then not preformatted else preformatted in - if not current_format && not preformatted then - let current_line = parse_simple_line cl current_format in - if String.length acc != 0 - then - parse_simple_line acc true :: current_line :: readAll current_format "" - else - current_line :: readAll current_format "" - else - readAll current_format (if not (is_format_line cl) then (if String.length acc != 0 then acc ^ "\n" ^ cl else cl) else acc) - with End_of_file -> - close_in_noerr ic; - if String.length acc != 0 then parse_simple_line acc true :: [] else [] - in readAll false "" + let ic = open_in fileName in + let rec readAll preformatted acc = + try + let cl = input_line ic in + let current_format = + if is_format_line cl then not preformatted else preformatted + in + if (not current_format) && not preformatted then + let current_line = parse_simple_line cl current_format in + if String.length acc != 0 then + parse_simple_line acc true :: current_line + :: readAll current_format "" + else current_line :: readAll current_format "" + else + readAll current_format + (if not (is_format_line cl) then + if String.length acc != 0 then acc ^ "\n" ^ cl else cl + else acc) + with End_of_file -> + close_in_noerr ic; + if String.length acc != 0 then parse_simple_line acc true :: [] else [] + in + readAll false "" diff --git a/learning/lib/render.ml b/learning/lib/render.ml @@ -1,41 +1,48 @@ -open Jingoo +open Jingoo -let to_tval value = - Jg_types.Tobj [("name", Jg_types.Tstr value);] +let to_tval value = Jg_types.Tobj [ ("name", Jg_types.Tstr value) ] -let get_pages_in_order course_name = - let arr = Sys.readdir (Shared.course_dir ^ course_name) in - Array.sort compare arr; - let lst : string list = List.filter Basic_functions.has_sl_postfix (Array.to_list arr) in - lst +let get_pages_in_order course_name = + let arr = Sys.readdir (Shared.course_dir ^ course_name) in + Array.sort compare arr; + let lst : string list = + List.filter Basic_functions.has_sl_postfix (Array.to_list arr) + in + lst -let get_course_home course_name = - List.map to_tval (get_pages_in_order course_name) +let get_course_home course_name = + List.map to_tval (get_pages_in_order course_name) -let get_course_array = - let arr = Sys.readdir Shared.course_dir in - Array.sort compare arr; - arr +let get_course_array = + let arr = Sys.readdir Shared.course_dir in + Array.sort compare arr; + arr let get_courses = - let arr = get_course_array in - Jg_types.Tlist - (Array.to_list (Array.map to_tval arr)) + let arr = get_course_array in + Jg_types.Tlist (Array.to_list (Array.map to_tval arr)) -let render_course_list = - (Jg_template.from_string Templates.course_list_jingoo ~models:[("courses", get_courses)]) +let render_course_list = + Jg_template.from_string Templates.course_list_jingoo + ~models:[ ("courses", get_courses) ] let render_sidebar s = - (Jg_template.from_string Templates.sidebar_jingoo ~models:[("courses", get_courses)]) ^ s + Jg_template.from_string Templates.sidebar_jingoo + ~models:[ ("courses", get_courses) ] + ^ s let page_line_to_tobj (line : Parser.page_line) = - Jg_types.Tobj [("content", Jg_types.Tstr line.content); ("line_type", Jg_types.Tstr (Parser.line_format_str line.line_type))] + Jg_types.Tobj + [ + ("content", Jg_types.Tstr line.content); + ("line_type", Jg_types.Tstr (Parser.line_format_str line.line_type)); + ] -let render_course_home course_name = - (Jg_template.from_string Templates.course_home_jingoo ~models:[("pages", (Jg_types.Tlist (get_course_home course_name)))]) +let render_course_home course_name = + Jg_template.from_string Templates.course_home_jingoo + ~models:[ ("pages", Jg_types.Tlist (get_course_home course_name)) ] -let get_path_to_page course page = - Shared.course_dir ^ course ^ "/" ^ page +let get_path_to_page course page = Shared.course_dir ^ course ^ "/" ^ page (* These two functions are called together. This is inefficient bc they both find the current index and then @@ -45,18 +52,28 @@ let get_path_to_page course page = *) let get_previous_page_link course page = - let pages = get_pages_in_order course in - match List.find_index (fun x -> x = page) pages with - | Some current_idx when current_idx - 1 >= 0 -> List.nth pages (current_idx - 1) - | _ -> "" + let pages = get_pages_in_order course in + match List.find_index (fun x -> x = page) pages with + | Some current_idx when current_idx - 1 >= 0 -> + List.nth pages (current_idx - 1) + | _ -> "" let get_next_page_link course page = - let pages = get_pages_in_order course in - match List.find_index (fun x -> x = page) pages with - | Some current_idx when current_idx + 1 < List.length pages -> List.nth pages (current_idx + 1) - | _ -> "" - -let render_page course page = - let page_lines : Parser.page_line list = Parser.read_simple_learning_file @@ get_path_to_page course page in - (Jg_template.from_string Templates.course_page_jingoo ~models:(["lines", Jg_types.Tlist (List.map page_line_to_tobj page_lines);("previous_page_link", Jg_types.Tstr (get_previous_page_link course page)) ; ("next_page_link", Jg_types.Tstr (get_next_page_link course page))])) + let pages = get_pages_in_order course in + match List.find_index (fun x -> x = page) pages with + | Some current_idx when current_idx + 1 < List.length pages -> + List.nth pages (current_idx + 1) + | _ -> "" +let render_page course page = + let page_lines : Parser.page_line list = + Parser.read_simple_learning_file @@ get_path_to_page course page + in + Jg_template.from_string Templates.course_page_jingoo + ~models: + [ + ("lines", Jg_types.Tlist (List.map page_line_to_tobj page_lines)); + ( "previous_page_link", + Jg_types.Tstr (get_previous_page_link course page) ); + ("next_page_link", Jg_types.Tstr (get_next_page_link course page)); + ] diff --git a/learning/lib/server.ml b/learning/lib/server.ml @@ -1,28 +1,22 @@ (* TODO: Make this a whitelist based on the page / course lists *) (* TODO: Favicon *) -let is_file filename = - if Sys.file_exists filename then - true - else - false +let is_file filename = if Sys.file_exists filename then true else false let is_directory path = - if Sys.file_exists path then - (if Sys.is_directory path then - true - else - false) - else - false + if Sys.file_exists path then if Sys.is_directory path then true else false + else false -let is_safe_path name = +let is_safe_path name = String.length name > 0 - && name <> "." - && name <> ".." - && String.for_all (fun c -> - (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') - || (c >= '0' && c <= '9') || c = '-' || c = '_' || c = '.') name + && name <> "." && name <> ".." + && String.for_all + (fun c -> + (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c = '-' || c = '_' || c = '.') + name (* TODO: Ensure server responds correctly to invalid requests. *) (* @@ -33,70 +27,53 @@ let is_safe_path name = *) let my_error_template _error debug_info suggested_response = - let status = Dream.status suggested_response in - let code = Dream.status_to_int status - and reason = Dream.status_to_string status in + let status = Dream.status suggested_response in + let code = Dream.status_to_int status + and reason = Dream.status_to_string status in Dream.set_header suggested_response "Content-Type" Dream.text_html; - Dream.set_body suggested_response ("<h1>" ^ (string_of_int code) ^ " - " ^ reason ^ "</h1>"); - Lwt.return suggested_response + Dream.set_body suggested_response + ("<h1>" ^ string_of_int code ^ " - " ^ reason ^ "</h1>"); + Lwt.return suggested_response -let get_course_name request = - Dream.param request "course_name" - -let get_page_name request = - Dream.param request "page_name" +let get_course_name request = Dream.param request "course_name" +let get_page_name request = Dream.param request "page_name" (* TODO: When should trailing slash be in a url? *) (* TODO: If sticking with course_name/ then course_name ^ "/" redirect *) -let is_valid_course name = - let pth = Shared.course_dir ^ name in - (is_safe_path name && is_directory pth) - -let is_valid_page course_name page_name = - let pth = Shared.course_dir ^ course_name ^ "/" ^ page_name in - (is_safe_path course_name && is_safe_path page_name && is_file pth) +let is_valid_course name = + let pth = Shared.course_dir ^ name in + is_safe_path name && is_directory pth +let is_valid_page course_name page_name = + let pth = Shared.course_dir ^ course_name ^ "/" ^ page_name in + is_safe_path course_name && is_safe_path page_name && is_file pth -let routes = [ - Dream.get "/course/:course_name/" (fun request -> - let name = get_course_name request in +let routes = + [ + Dream.get "/course/:course_name/" (fun request -> + let name = get_course_name request in if is_valid_course name then - Dream.html ( - Render.render_sidebar @@ Render.render_course_home (name) - ) - else - Dream.empty `Not_Found - ); - + Dream.html (Render.render_sidebar @@ Render.render_course_home name) + else Dream.empty `Not_Found); Dream.get "/course/:course_name/page/:page_name" (fun request -> - let course_name = get_course_name request in - let page_name = get_page_name request in + let course_name = get_course_name request in + let page_name = get_page_name request in if is_valid_course course_name then - if is_valid_page course_name page_name then - Dream.html @@ Render.render_sidebar @@ Render.render_page course_name page_name - else - Dream.empty `Not_Found - else - Dream.empty `Not_Found - ); - - + if is_valid_page course_name page_name then + Dream.html @@ Render.render_sidebar + @@ Render.render_page course_name page_name + else Dream.empty `Not_Found + else Dream.empty `Not_Found); Dream.get "/static/**" (Dream.static "./static"); - Dream.get "/" (fun _ -> Dream.html (Render.render_sidebar @@ Render.render_course_list)); + Dream.get "/" (fun _ -> + Dream.html (Render.render_sidebar @@ Render.render_course_list)); Dream.get "/robots.txt" (Dream.from_filesystem "./static" "robots.txt"); - - Dream.get "/course/:course_name/page/assets/**" (fun request -> - - let name = get_course_name request in - if is_valid_course name then - Dream.static - (Shared.course_dir ^ name - ^ "/assets") request - else - Dream.empty `Not_Found - ); - -] + Dream.get "/course/:course_name/page/assets/**" (fun request -> + let name = get_course_name request in + if is_valid_course name then + Dream.static (Shared.course_dir ^ name ^ "/assets") request + else Dream.empty `Not_Found); + ] let handler = Dream.error_template my_error_template diff --git a/learning/lib/shared.ml b/learning/lib/shared.ml @@ -1,2 +1,3 @@ (* ALWAYS pass in directory to watch. Fallback should only be for tests. *) -let course_dir = if Array.length Sys.argv > 1 then Sys.argv.(1) ^ "/" else "./test_courses/" +let course_dir = + if Array.length Sys.argv > 1 then Sys.argv.(1) ^ "/" else "./test_courses/" diff --git a/learning/lib/templates.ml b/learning/lib/templates.ml @@ -1,68 +1,68 @@ -let sidebar_jingoo = - "<link rel=\"stylesheet\" href=\"/static/styles.css\" type=\"text/css\"> -<div class=\"sidenav\"> - <a href=\"/\">Home</a> - {% for course in courses %} - <a href=\"/course/{{ course.name }}/\">{{ course.name }}</a> - {% endfor %} -</div>" +let sidebar_jingoo = + "<link rel=\"stylesheet\" href=\"/static/styles.css\" type=\"text/css\">\n\ + <div class=\"sidenav\">\n\ + \ <a href=\"/\">Home</a>\n\ + \ {% for course in courses %}\n\ + \ <a href=\"/course/{{ course.name }}/\">{{ course.name }}</a>\n\ + \ {% endfor %}\n\ + </div>" -let course_page_jingoo = - "<div class=\"main\"> - {% for line in lines %} - {% if line.line_type == \"Preformatted\"%} - <pre><pre-formatted>{{line.content}}</pre-formatted></pre> - {% else if line.line_type == \"Quote\"%} - <blockquote>{{line.content}}</blockquote> - {% else if line.line_type == \"ListItem\"%} - <li>{{line.content}}</li> - {% else if line.line_type == \"Link\"%} - <content-block> - <a href=\"{{line.content}}\">{{line.content}}</a> - </content-block> - {% else if line.line_type == \"Hidden\"%} - <details> - <summary>{{line.content}}</summary> - </details> - {% else if line.line_type == \"HeaderL1\"%} - <h1>{{line.content}}</h1> - {% else if line.line_type == \"HeaderL2\"%} - <h2>{{line.content}}</h2> - {% else if line.line_type == \"HeaderL3\"%} - <h3>{{line.content}}</h3> - {% else if line.line_type == \"Embedded\"%} - <iframe - style=\"width: 100%; height: 600px;overflow:auto\"; - src=\"{{line.content}}\"> - </iframe> - {% else if line.line_type == \"Normal\"%} - <content-block>{{line.content}}</content-block> - {% endif %} - {% endfor %} - {% if previous_page_link != \"\" %} - <a class=\"page-nav\" href=\"{{previous_page_link}}\">previous</a> - {% endif %} - {% if next_page_link != \"\" %} - <a class=\"page-nav-next\" href=\"{{next_page_link}}\">next</a> - {% endif %} -</div>" +let course_page_jingoo = + "<div class=\"main\">\n\ + \ {% for line in lines %}\n\ + \ {% if line.line_type == \"Preformatted\"%}\n\ + \ <pre><pre-formatted>{{line.content}}</pre-formatted></pre>\n\ + \ {% else if line.line_type == \"Quote\"%}\n\ + \ <blockquote>{{line.content}}</blockquote>\n\ + \ {% else if line.line_type == \"ListItem\"%}\n\ + \ <li>{{line.content}}</li>\n\ + \ {% else if line.line_type == \"Link\"%}\n\ + \ <content-block>\n\ + \ <a href=\"{{line.content}}\">{{line.content}}</a>\n\ + \ </content-block>\n\ + \ {% else if line.line_type == \"Hidden\"%}\n\ + \ <details>\n\ + \ <summary>{{line.content}}</summary>\n\ + \ </details>\n\ + \ {% else if line.line_type == \"HeaderL1\"%}\n\ + \ <h1>{{line.content}}</h1>\n\ + \ {% else if line.line_type == \"HeaderL2\"%}\n\ + \ <h2>{{line.content}}</h2>\n\ + \ {% else if line.line_type == \"HeaderL3\"%}\n\ + \ <h3>{{line.content}}</h3>\n\ + \ {% else if line.line_type == \"Embedded\"%}\n\ + \ <iframe\n\ + \ style=\"width: 100%; height: 600px;overflow:auto\";\n\ + \ src=\"{{line.content}}\">\n\ + \ </iframe>\n\ + \ {% else if line.line_type == \"Normal\"%}\n\ + \ <content-block>{{line.content}}</content-block>\n\ + \ {% endif %}\n\ + \ {% endfor %}\n\ + \ {% if previous_page_link != \"\" %}\n\ + \ <a class=\"page-nav\" href=\"{{previous_page_link}}\">previous</a>\n\ + \ {% endif %}\n\ + \ {% if next_page_link != \"\" %}\n\ + \ <a class=\"page-nav-next\" href=\"{{next_page_link}}\">next</a>\n\ + \ {% endif %}\n\ + </div>" -let course_home_jingoo = - "<div class=\"main\"> - <h1> Pages </h1> - <ul> - {% for page in pages %} - <li><a href=\"page/{{ page.name }}\">{{ page.name }}</a></li> - {% endfor %} - </ul> -</div>" +let course_home_jingoo = + "<div class=\"main\">\n\ + \ <h1> Pages </h1>\n\ + \ <ul>\n\ + \ {% for page in pages %}\n\ + \ <li><a href=\"page/{{ page.name }}\">{{ page.name }}</a></li>\n\ + \ {% endfor %}\n\ + \ </ul>\n\ + </div>" -let course_list_jingoo = - "<div class=\"main\"> - <h1> courses </h1> - <ul> - {% for course in courses %} - <li><a href=\"/course/{{ course.name }}/\">{{ course.name }}</a></li> - {% endfor %} - </ul> -</div>" +let course_list_jingoo = + "<div class=\"main\">\n\ + \ <h1> courses </h1>\n\ + \ <ul>\n\ + \ {% for course in courses %}\n\ + \ <li><a href=\"/course/{{ course.name }}/\">{{ course.name }}</a></li>\n\ + \ {% endfor %}\n\ + \ </ul>\n\ + </div>" diff --git a/learning/test/dune b/learning/test/dune @@ -1,5 +1,5 @@ (test (name test_learning) (libraries learning) - (deps (glob_files_rec ./test_courses/*)) - ) + (deps + (glob_files_rec ./test_courses/*))) diff --git a/learning/test/test_learning.ml b/learning/test/test_learning.ml @@ -1,3 +1,3 @@ -let () = Test_line_parsing.test_line_parsing () -let () = Test_rendering.test_rendering () +let () = Test_line_parsing.test_line_parsing () +let () = Test_rendering.test_rendering () let () = Test_server.test_server () diff --git a/learning/test/test_line_parsing.ml b/learning/test/test_line_parsing.ml @@ -1,90 +1,90 @@ open Learning -let test_line_parsing () = - assert (Parser.is_preformatted "" true); - assert (not (Parser.is_preformatted "" false)); +let test_line_parsing () = + assert (Parser.is_preformatted "" true); + assert (not (Parser.is_preformatted "" false)); - (* Line-based type tests.*) - (* Only ``` is a format line, no additional characters.*) - assert (Parser.is_format_line "```"); - assert (not (Parser.is_format_line "``` ")); - assert (not (Parser.is_format_line "```a")); - assert (not (Parser.is_format_line "```0")); + (* Line-based type tests.*) + (* Only ``` is a format line, no additional characters.*) + assert (Parser.is_format_line "```"); + assert (not (Parser.is_format_line "``` ")); + assert (not (Parser.is_format_line "```a")); + assert (not (Parser.is_format_line "```0")); - (* Preformatted always overrides all line types *) - assert ((Parser.get_line_type "Test" true) = Preformatted); - assert ((Parser.get_line_type "# Test" true) = Preformatted); - assert ((Parser.get_line_type "## Test" true) = Preformatted); - assert ((Parser.get_line_type "### Test" true) = Preformatted); - assert ((Parser.get_line_type "=>e Test" true) = Preformatted); - assert ((Parser.get_line_type "```" true) = Preformatted); - assert ((Parser.get_line_type "?? Test" true) = Preformatted); - assert ((Parser.get_line_type "> Test" true) = Preformatted); - assert ((Parser.get_line_type "=> Test" true) = Preformatted); - assert ((Parser.get_line_type "* Test" true) = Preformatted); + (* Preformatted always overrides all line types *) + assert (Parser.get_line_type "Test" true = Preformatted); + assert (Parser.get_line_type "# Test" true = Preformatted); + assert (Parser.get_line_type "## Test" true = Preformatted); + assert (Parser.get_line_type "### Test" true = Preformatted); + assert (Parser.get_line_type "=>e Test" true = Preformatted); + assert (Parser.get_line_type "```" true = Preformatted); + assert (Parser.get_line_type "?? Test" true = Preformatted); + assert (Parser.get_line_type "> Test" true = Preformatted); + assert (Parser.get_line_type "=> Test" true = Preformatted); + assert (Parser.get_line_type "* Test" true = Preformatted); - (* Not preformatted, basic type assertions. *) - assert ((Parser.get_line_type "Test" false) = Normal); - assert ((Parser.get_line_type "# Test" false) = HeaderL1); - assert ((Parser.get_line_type "## Test" false) = HeaderL2); - assert ((Parser.get_line_type "### Test" false) = HeaderL3); - assert ((Parser.get_line_type "=>e Test" false) = Embedded); - assert ((Parser.get_line_type "?? Test" false) = Hidden); - assert ((Parser.get_line_type "> Test" false) = Quote); - assert ((Parser.get_line_type "=> Test" false) = Link); - assert ((Parser.get_line_type "* Test" false) = ListItem); + (* Not preformatted, basic type assertions. *) + assert (Parser.get_line_type "Test" false = Normal); + assert (Parser.get_line_type "# Test" false = HeaderL1); + assert (Parser.get_line_type "## Test" false = HeaderL2); + assert (Parser.get_line_type "### Test" false = HeaderL3); + assert (Parser.get_line_type "=>e Test" false = Embedded); + assert (Parser.get_line_type "?? Test" false = Hidden); + assert (Parser.get_line_type "> Test" false = Quote); + assert (Parser.get_line_type "=> Test" false = Link); + assert (Parser.get_line_type "* Test" false = ListItem); - (* Special lines missing expected spacing *) - assert ((Parser.get_line_type "Test" false) = Normal); - assert ((Parser.get_line_type "#Test" false) = Normal); - assert ((Parser.get_line_type "##Test" false) = Normal); - assert ((Parser.get_line_type "###Test" false) = Normal); - assert ((Parser.get_line_type "=>eTest" false) = Normal); - assert ((Parser.get_line_type "??Test" false) = Normal); - assert ((Parser.get_line_type ">Test" false) = Normal); - assert ((Parser.get_line_type "=>Test" false) = Normal); - assert ((Parser.get_line_type "*Test" false) = Normal); + (* Special lines missing expected spacing *) + assert (Parser.get_line_type "Test" false = Normal); + assert (Parser.get_line_type "#Test" false = Normal); + assert (Parser.get_line_type "##Test" false = Normal); + assert (Parser.get_line_type "###Test" false = Normal); + assert (Parser.get_line_type "=>eTest" false = Normal); + assert (Parser.get_line_type "??Test" false = Normal); + assert (Parser.get_line_type ">Test" false = Normal); + assert (Parser.get_line_type "=>Test" false = Normal); + assert (Parser.get_line_type "*Test" false = Normal); - (* No weird error considerations / oboes. *) - assert ((Parser.get_line_type "" false) = Normal); - assert ((Parser.get_line_type "#" false) = Normal); - assert ((Parser.get_line_type "##" false) = Normal); - assert ((Parser.get_line_type "###" false) = Normal); - assert ((Parser.get_line_type "=>e" false) = Normal); - assert ((Parser.get_line_type "??" false) = Normal); - assert ((Parser.get_line_type ">" false) = Normal); - assert ((Parser.get_line_type "=>" false) = Normal); - assert ((Parser.get_line_type "*" false) = Normal); - assert ((Parser.get_line_type "# " false) = HeaderL1); - assert ((Parser.get_line_type "## " false) = HeaderL2); - assert ((Parser.get_line_type "### " false) = HeaderL3); - assert ((Parser.get_line_type "=>e " false) = Embedded); - assert ((Parser.get_line_type "?? " false) = Hidden); - assert ((Parser.get_line_type "> " false) = Quote); - assert ((Parser.get_line_type "=> " false) = Link); - assert ((Parser.get_line_type "* " false) = ListItem); + (* No weird error considerations / oboes. *) + assert (Parser.get_line_type "" false = Normal); + assert (Parser.get_line_type "#" false = Normal); + assert (Parser.get_line_type "##" false = Normal); + assert (Parser.get_line_type "###" false = Normal); + assert (Parser.get_line_type "=>e" false = Normal); + assert (Parser.get_line_type "??" false = Normal); + assert (Parser.get_line_type ">" false = Normal); + assert (Parser.get_line_type "=>" false = Normal); + assert (Parser.get_line_type "*" false = Normal); + assert (Parser.get_line_type "# " false = HeaderL1); + assert (Parser.get_line_type "## " false = HeaderL2); + assert (Parser.get_line_type "### " false = HeaderL3); + assert (Parser.get_line_type "=>e " false = Embedded); + assert (Parser.get_line_type "?? " false = Hidden); + assert (Parser.get_line_type "> " false = Quote); + assert (Parser.get_line_type "=> " false = Link); + assert (Parser.get_line_type "* " false = ListItem); - (*E2E Parsing of individual lines. *) - (* Character stripping expectations. *) - assert((Parser.parse_simple_line "Test" false).content = "Test"); - assert((Parser.parse_simple_line "# Test" false).content = "Test"); - assert((Parser.parse_simple_line "## Test" false).content = "Test"); - assert((Parser.parse_simple_line "### Test" false).content = "Test"); - assert((Parser.parse_simple_line "=>e Test" false).content = "Test"); - assert((Parser.parse_simple_line "Test" true).content = "Test"); - assert((Parser.parse_simple_line "?? Test" false).content = "Test"); - assert((Parser.parse_simple_line "> Test" false).content = "Test"); - assert((Parser.parse_simple_line "=> Test" false).content = "Test"); - assert((Parser.parse_simple_line "* Test" false).content = "Test"); - - (* Character stripping doesn't break on empty line content. *) - assert((Parser.parse_simple_line "" false).content = ""); - assert((Parser.parse_simple_line "# " false).content = ""); - assert((Parser.parse_simple_line "## " false).content = ""); - assert((Parser.parse_simple_line "### " false).content = ""); - assert((Parser.parse_simple_line "=>e " false).content = ""); - assert((Parser.parse_simple_line "" true).content = ""); - assert((Parser.parse_simple_line "?? " false).content = ""); - assert((Parser.parse_simple_line "> " false).content = ""); - assert((Parser.parse_simple_line "=> " false).content = ""); - assert((Parser.parse_simple_line "* " false).content = ""); + (*E2E Parsing of individual lines. *) + (* Character stripping expectations. *) + assert ((Parser.parse_simple_line "Test" false).content = "Test"); + assert ((Parser.parse_simple_line "# Test" false).content = "Test"); + assert ((Parser.parse_simple_line "## Test" false).content = "Test"); + assert ((Parser.parse_simple_line "### Test" false).content = "Test"); + assert ((Parser.parse_simple_line "=>e Test" false).content = "Test"); + assert ((Parser.parse_simple_line "Test" true).content = "Test"); + assert ((Parser.parse_simple_line "?? Test" false).content = "Test"); + assert ((Parser.parse_simple_line "> Test" false).content = "Test"); + assert ((Parser.parse_simple_line "=> Test" false).content = "Test"); + assert ((Parser.parse_simple_line "* Test" false).content = "Test"); + + (* Character stripping doesn't break on empty line content. *) + assert ((Parser.parse_simple_line "" false).content = ""); + assert ((Parser.parse_simple_line "# " false).content = ""); + assert ((Parser.parse_simple_line "## " false).content = ""); + assert ((Parser.parse_simple_line "### " false).content = ""); + assert ((Parser.parse_simple_line "=>e " false).content = ""); + assert ((Parser.parse_simple_line "" true).content = ""); + assert ((Parser.parse_simple_line "?? " false).content = ""); + assert ((Parser.parse_simple_line "> " false).content = ""); + assert ((Parser.parse_simple_line "=> " false).content = ""); + assert ((Parser.parse_simple_line "* " false).content = "") diff --git a/learning/test/test_rendering.ml b/learning/test/test_rendering.ml @@ -2,14 +2,13 @@ open Learning -let courses = Array.to_list (Render.get_course_array) -let expected_courses = ["course_0";"course_1";"course_empty"] +let courses = Array.to_list Render.get_course_array +let expected_courses = [ "course_0"; "course_1"; "course_empty" ] let course_0_page_0 = Render.render_page "course_0" "0_example.sl" let course_0_page_1 = Render.render_page "course_0" "1_example.sl" let course_1_page_0 = Render.render_page "course_1" "0_example.sl" let pages_in_order = Render.get_pages_in_order "course_0" - let contains s1 s2 = let len1 = String.length s1 and len2 = String.length s2 in let rec go idx = @@ -19,28 +18,25 @@ let contains s1 s2 = in go 0 - let test_rendering () = + (* Verify non-sl extension pages are hidden for course listings. *) + assert (List.length pages_in_order = 2); - (* Verify non-sl extension pages are hidden for course listings. *) - assert ((List.length pages_in_order) = 2); - - (*Verify happy path course listing.*) - assert (courses = expected_courses); - assert ((Render.render_page "course_0" "0_example.sl") != ""); + (*Verify happy path course listing.*) + assert (courses = expected_courses); + assert (Render.render_page "course_0" "0_example.sl" != ""); - (* + (* Verify sensible expectations about each line. We assume the mapping between sl headers and html headers, and then check text existence. *) + assert (contains course_0_page_0 "<h1>"); + assert (contains course_0_page_0 "<h2>"); + assert (contains course_0_page_0 "<h3>"); + assert (contains course_0_page_0 "This is an example sl file."); - assert (contains course_0_page_0 "<h1>"); - assert (contains course_0_page_0 "<h2>"); - assert (contains course_0_page_0 "<h3>"); - assert (contains course_0_page_0 "This is an example sl file."); - - (* + (* Verify the following don't render when mis-specified: - # -> h1 - ## -> h2 @@ -53,32 +49,30 @@ let test_rendering () = - * -> ul *) - (* TODO: These are brittle; add classes for rendering / validation in templates. *) - assert (not (contains course_0_page_1 "<h1>TEST")); - assert (not (contains course_0_page_1 "<h2>TEST")); - assert (not (contains course_0_page_1 "<h3>TEST")); - assert (not (contains course_0_page_1 "href=\"TEST")); - assert (not (contains course_0_page_1 "iframe")); - assert (not (contains course_0_page_1 "<pre-formatted>TEST")); - assert (not (contains course_0_page_1 "summary")); - assert (not (contains course_0_page_1 "quote")); - assert (not (contains course_0_page_1 "<li>")); + (* TODO: These are brittle; add classes for rendering / validation in templates. *) + assert (not (contains course_0_page_1 "<h1>TEST")); + assert (not (contains course_0_page_1 "<h2>TEST")); + assert (not (contains course_0_page_1 "<h3>TEST")); + assert (not (contains course_0_page_1 "href=\"TEST")); + assert (not (contains course_0_page_1 "iframe")); + assert (not (contains course_0_page_1 "<pre-formatted>TEST")); + assert (not (contains course_0_page_1 "summary")); + assert (not (contains course_0_page_1 "quote")); + assert (not (contains course_0_page_1 "<li>")); - (* + (* Test well-formed variant of above page renders each line type as expected. *) - assert (contains course_1_page_0 "<h1>TEST"); - assert (contains course_1_page_0 "<h2>TEST"); - assert (contains course_1_page_0 "<h3>TEST"); - assert (contains course_1_page_0 "href=\"TEST"); - assert (contains course_1_page_0 "iframe"); - assert (contains course_1_page_0 "<pre-formatted>TEST"); - assert (contains course_1_page_0 "summary"); - assert (contains course_1_page_0 "quote"); - assert (contains course_1_page_0 "<li>"); - - + assert (contains course_1_page_0 "<h1>TEST"); + assert (contains course_1_page_0 "<h2>TEST"); + assert (contains course_1_page_0 "<h3>TEST"); + assert (contains course_1_page_0 "href=\"TEST"); + assert (contains course_1_page_0 "iframe"); + assert (contains course_1_page_0 "<pre-formatted>TEST"); + assert (contains course_1_page_0 "summary"); + assert (contains course_1_page_0 "quote"); + assert (contains course_1_page_0 "<li>") (* type line_format = diff --git a/learning/test/test_server.ml b/learning/test/test_server.ml @@ -1,43 +1,49 @@ open Learning -(*main.ml server definition: +let test_server = Dream.test @@ Dream.router Server.routes +let root_response = test_server (Dream.request ~target:"/" "") -let () = - Dream.run ~error_handler:(Server.handler) - @@ Dream.logger - @@ Dream.router Server.routes +let course_0_response = + test_server (Dream.request ~target:"/course/course_0/" "") -*) +let course_1_response = + test_server (Dream.request ~target:"/course/course_1/" "") -let test_server = - Dream.test - @@ Dream.router Server.routes +let course_0_page_0_response = + test_server (Dream.request ~target:"/course/course_0/page/0_example.sl" "") -let root_response = test_server (Dream.request ~target:"/" "") -let course_0_response = test_server (Dream.request ~target:"/course/course_0/" "") -let course_1_response = test_server (Dream.request ~target:"/course/course_1/" "") -let course_0_page_0_response = test_server (Dream.request ~target:"/course/course_0/page/0_example.sl" "") -let course_dne_response = test_server (Dream.request ~target:"/course/this_course_doesnt_exist/" "") -let course_dne_page_dne_response = test_server (Dream.request ~target:"/course/this_course_doesnt_exist/page/this_page_doesnt_exist.sl" "") -let page_dne_response = test_server (Dream.request ~target:"/course/course_0/page/this_page_doesnt_exist.sl" "") -let empty_page_response = test_server (Dream.request ~target:"/course/course_1/page/1_example.sl" "") - -let test_server () = - assert (not (Server.is_safe_path "../")); - assert (not (Server.is_safe_path "..")); - assert (not (Server.is_safe_path "smt/../../../")); - assert (not (Server.is_safe_path "smt/../")); - assert (Server.is_safe_path "smt.sl"); - assert (Server.is_safe_path "smt"); - assert (Server.is_safe_path "0_course_1.sl"); - assert (Server.is_safe_path "1_course_1.sl"); - - (* Requests to test dream server handling routes. *) - assert (Dream.status_to_int (Dream.status root_response) = 200); - assert (Dream.status_to_int (Dream.status course_0_response) = 200); - assert (Dream.status_to_int (Dream.status course_1_response) = 200); - assert (Dream.status_to_int (Dream.status course_dne_response) = 404); - assert (Dream.status_to_int (Dream.status course_dne_page_dne_response) = 404); - assert (Dream.status_to_int (Dream.status page_dne_response) = 404); - assert (Dream.status_to_int (Dream.status course_0_page_0_response) = 200); - assert (Dream.status_to_int (Dream.status empty_page_response) = 200); +let course_dne_response = + test_server (Dream.request ~target:"/course/this_course_doesnt_exist/" "") + +let course_dne_page_dne_response = + test_server + (Dream.request + ~target:"/course/this_course_doesnt_exist/page/this_page_doesnt_exist.sl" + "") + +let page_dne_response = + test_server + (Dream.request ~target:"/course/course_0/page/this_page_doesnt_exist.sl" "") + +let empty_page_response = + test_server (Dream.request ~target:"/course/course_1/page/1_example.sl" "") + +let test_server () = + assert (not (Server.is_safe_path "../")); + assert (not (Server.is_safe_path "..")); + assert (not (Server.is_safe_path "smt/../../../")); + assert (not (Server.is_safe_path "smt/../")); + assert (Server.is_safe_path "smt.sl"); + assert (Server.is_safe_path "smt"); + assert (Server.is_safe_path "0_course_1.sl"); + assert (Server.is_safe_path "1_course_1.sl"); + + (* Requests to test dream server handling routes. *) + assert (Dream.status_to_int (Dream.status root_response) = 200); + assert (Dream.status_to_int (Dream.status course_0_response) = 200); + assert (Dream.status_to_int (Dream.status course_1_response) = 200); + assert (Dream.status_to_int (Dream.status course_dne_response) = 404); + assert (Dream.status_to_int (Dream.status course_dne_page_dne_response) = 404); + assert (Dream.status_to_int (Dream.status page_dne_response) = 404); + assert (Dream.status_to_int (Dream.status course_0_page_0_response) = 200); + assert (Dream.status_to_int (Dream.status empty_page_response) = 200)