simple-learning

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

commit 2b9df834b9807aa37d6a5c89ebeb2b87ff73bce1
parent 34cac6ce2cc3be7f7441fb5081d2802d6e009c59
Author: Andrew Laack <andrew@laack.co>
Date:   Thu, 27 Aug 2026 23:22:18 -0500

OCaml is so insanely easy to refactor in.

Diffstat:
Alearning/bin/basic_functions.ml | 26++++++++++++++++++++++++++
Mlearning/bin/main.ml | 127+++----------------------------------------------------------------------------
Alearning/bin/parser.ml | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Alearning/bin/render.ml | 41+++++++++++++++++++++++++++++++++++++++++
Alearning/bin/shared.ml | 1+
5 files changed, 124 insertions(+), 123 deletions(-)

diff --git a/learning/bin/basic_functions.ml b/learning/bin/basic_functions.ml @@ -0,0 +1,26 @@ +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 + +(* 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 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/bin/main.ml b/learning/bin/main.ml @@ -1,132 +1,13 @@ open Jingoo -let course_dir = "../courses/" - -let countPrefix char data = - let rec go idx = - if String.length data > idx && data.[idx] = char - then - go (idx + 1) - else idx - in go 0 - -type page_line = { - header_level: int; - content: string; - is_embedded_video: bool; - is_embedded_image: bool; - is_preformatted: bool; - is_hidden: bool; -} - -let parse_simple_line line preformatted = - let hidden = if not preformatted && String.length(line) > 2 && line.[0] = '?' && line.[1] = '?' then true else false in - let hash_count = if not preformatted then countPrefix '#' line else 0 in - let embedded_video = if not preformatted && String.length(line) > 3 && line.[0] = '=' && line.[1] = '>' && line.[2] = 'v' then true else false in - let embedded_image = if not preformatted && String.length(line) > 3 && line.[0] = '=' && line.[1] = '>' && line.[2] = 'i' then true else false in - {header_level = hash_count; content = (if embedded_video || embedded_image then (String.sub line 4 (String.length(line) - 4)) else line); is_embedded_image = embedded_image; is_embedded_video = embedded_video; is_preformatted = preformatted; is_hidden = hidden} - -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 readFile fileName = - let ic = open_in fileName in - let rec readAll acc = - try - readAll (acc ^ ((input_line ic)) ^ "\n") - with End_of_file -> - close_in_noerr ic; - acc - in readAll "" - -let remove_file_extension file_name = - let fin_period_pos = String.rindex file_name '.' in - String.sub file_name 0 fin_period_pos - -let to_tval value = - Jg_types.Tobj [("name", Jg_types.Tstr value);] - -let page_line_to_tobj line = - Jg_types.Tobj [("header_level", Jg_types.Tint line.header_level); ("content", Jg_types.Tstr line.content); ("is_embedded_video", Jg_types.Tbool line.is_embedded_video); ("is_embedded_image", Jg_types.Tbool line.is_embedded_image); ("is_preformatted", Jg_types.Tbool line.is_preformatted); ("is_hidden", Jg_types.Tbool line.is_hidden)] - -let get_courses = - let arr = Sys.readdir course_dir in - Array.sort compare arr; - Jg_types.Tlist - (Array.to_list (Array.map to_tval arr)) - -let render_course_list = - (Jg_template.from_file "./templates/course_list.jingoo" ~models:[("courses", get_courses)]) - -let prepend prefix s = - prefix ^ s - -(* 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 get_pages_in_order course_name = - let arr = Sys.readdir (course_dir ^ course_name) in - Array.sort compare arr; - let lst = List.filter has_sl_postfix (Array.to_list arr) in - lst - -let get_pages page_arr = - readFile page_arr.(0) - -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 get_course_home course_name = - List.map to_tval (get_pages_in_order course_name) - -let render_course_home course_name = - (Jg_template.from_file "./templates/course_home.jingoo" ~models:[("pages", (Jg_types.Tlist (get_course_home course_name)))]) - -let get_path_to_page course page = - course_dir ^ course ^ "/" ^ page - -let render_page course page = - let page_lines = read_simple_learning_file @@ get_path_to_page course page in - (Jg_template.from_file "./templates/course_page.jingoo" ~models:(["lines", Jg_types.Tlist (List.map page_line_to_tobj page_lines)])) - - -let render_sidebar s = - (Jg_template.from_file "./templates/sidebar.jingoo" ~models:[("courses", get_courses)]) ^ s - - let () = Dream.run @@ Dream.logger @@ Dream.router [ - Dream.get "/course/:course_name/" (fun request -> Dream.html (render_sidebar @@ render_course_home (Dream.param request "course_name"))); - Dream.get "/course/:course_name/page/:page_name" (fun request -> Dream.html @@ render_sidebar @@ render_page (Dream.param request "course_name") (Dream.param request "page_name")); + Dream.get "/course/:course_name/" (fun request -> Dream.html (Render.render_sidebar @@ Render.render_course_home (Dream.param request "course_name"))); + Dream.get "/course/:course_name/page/:page_name" (fun request -> Dream.html @@ Render.render_sidebar @@ Render.render_page (Dream.param request "course_name") (Dream.param request "page_name")); Dream.get "/static/**" (Dream.static "./static"); - Dream.get "/" (fun _ -> Dream.html (render_sidebar @@ render_course_list)); + Dream.get "/" (fun _ -> Dream.html (Render.render_sidebar @@ Render.render_course_list)); Dream.get "/course/:course_name/page/assets/**" (fun request -> Dream.static - (course_dir ^ (Dream.param request "course_name") ^ "/assets") request); + (Shared.course_dir ^ (Dream.param request "course_name") ^ "/assets") request); ] diff --git a/learning/bin/parser.ml b/learning/bin/parser.ml @@ -0,0 +1,52 @@ +type page_line = { + header_level: int; + content: string; + is_embedded_video: bool; + is_embedded_image: bool; + is_preformatted: bool; + is_hidden: bool; +} + +let parse_simple_line line preformatted = + let hidden = if not preformatted && String.length(line) > 2 && line.[0] = '?' && line.[1] = '?' then true else false in + let hash_count = if not preformatted then Basic_functions.count_prefix '#' line else 0 in + let embedded_video = if not preformatted && String.length(line) > 3 && line.[0] = '=' && line.[1] = '>' && line.[2] = 'v' then true else false in + let embedded_image = if not preformatted && String.length(line) > 3 && line.[0] = '=' && line.[1] = '>' && line.[2] = 'i' then true else false in + {header_level = hash_count; content = (if embedded_video || embedded_image then (String.sub line 4 (String.length(line) - 4)) else line); is_embedded_image = embedded_image; is_embedded_video = embedded_video; is_preformatted = preformatted; is_hidden = hidden} + + +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 readFile fileName = + let ic = open_in fileName in + let rec readAll acc = + try + readAll (acc ^ ((input_line ic)) ^ "\n") + with End_of_file -> + close_in_noerr ic; + acc + in readAll "" + +let get_pages page_arr = + readFile page_arr.(0) diff --git a/learning/bin/render.ml b/learning/bin/render.ml @@ -0,0 +1,41 @@ +open Jingoo + +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 = 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_courses = + let arr = Sys.readdir Shared.course_dir in + Array.sort compare arr; + Jg_types.Tlist + (Array.to_list (Array.map to_tval arr)) + +let render_course_list = + (Jg_template.from_file "./templates/course_list.jingoo" ~models:[("courses", get_courses)]) + + + +let render_sidebar s = + (Jg_template.from_file "./templates/sidebar.jingoo" ~models:[("courses", get_courses)]) ^ s + +let page_line_to_tobj (line : Parser.page_line) = + Jg_types.Tobj [("header_level", Jg_types.Tint line.header_level); ("content", Jg_types.Tstr line.content); ("is_embedded_video", Jg_types.Tbool line.is_embedded_video); ("is_embedded_image", Jg_types.Tbool line.is_embedded_image); ("is_preformatted", Jg_types.Tbool line.is_preformatted); ("is_hidden", Jg_types.Tbool line.is_hidden)] + +let render_course_home course_name = + (Jg_template.from_file "./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 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_file "./templates/course_page.jingoo" ~models:(["lines", Jg_types.Tlist (List.map page_line_to_tobj page_lines)])) + diff --git a/learning/bin/shared.ml b/learning/bin/shared.ml @@ -0,0 +1 @@ +let course_dir = "../courses/"