Samhain 21
Converter - type selection - subdir conversion - htm extension Gemini - index.gmi - topics and latest - gmi.atom feed Add pull (http(s)) operation - peers.pub.conf and peers.priv.conf HTML5 format & fixes by Novaburst Phony target (thanks Gergely) May Basic unit renamed from Note to Text. New modular text-parser, internal to Logarion, for generic notation parsing. The default input format is now a much plainer text. Logarion created texts have part of the UUID in filename. Logarion's index re-written in Messagepack format. Removed `indices` command. They are generated during `convert`. git-svn-id: file:///srv/svn/repo/kosuzu/trunk@2 eb64cd80-c68d-6f47-b6a3-0ada418499da
This commit is contained in:
57
cli/atom.ml
Normal file
57
cli/atom.ml
Normal file
@@ -0,0 +1,57 @@
|
||||
let esc = Converter.Html.esc
|
||||
|
||||
let element tag content = "<" ^ tag ^ ">" ^ content ^ "</" ^ tag ^ ">"
|
||||
|
||||
let opt_element tag_name content =
|
||||
if content <> ""
|
||||
then element tag_name content
|
||||
else ""
|
||||
|
||||
module P = Parsers.Plain_text.Make (Converter.Html)
|
||||
|
||||
let id txt = "<id>urn:uuid:" ^ Logarion.(Id.to_string txt.Text.uuid) ^ "</id>"
|
||||
let title text = "<title>" ^ esc text.Logarion.Text.title ^ "</title>"
|
||||
|
||||
let authors text =
|
||||
let u acc addr = acc ^ element "uri" (Uri.to_string addr) in
|
||||
let open Logarion in
|
||||
let fn txt a =
|
||||
a ^ "<author>" ^ (opt_element "name" @@ esc txt.Person.name)
|
||||
^ (List.fold_left u "" txt.Person.addresses)
|
||||
^ "</author>" in
|
||||
Person.Set.fold fn text.Text.authors ""
|
||||
|
||||
let updated txt = let open Logarion in
|
||||
"<updated>"^ Date.(txt.Text.date |> listing |> rfc_string) ^"</updated>"
|
||||
|
||||
let htm_entry base_url text =
|
||||
let open Logarion in
|
||||
let u = Text.short_id text in
|
||||
"<entry><link rel=\"alternate\" href=\"" ^ base_url ^ "/" ^ u ^ ".htm\" />"
|
||||
^ title text ^ id text ^ updated text ^ authors text
|
||||
^ (opt_element "summary" @@ esc @@ Text.str "abstract" text)
|
||||
^ String_set.fold (fun elt a -> a ^ "<category term=\"" ^ esc elt ^ "\"/>") (Text.set "topics" text) ""
|
||||
^ "<content type=\"xhtml\"><div xmlns=\"http://www.w3.org/1999/xhtml\">"
|
||||
^ P.of_string text.body ""
|
||||
^ "</div></content></entry>\n"
|
||||
|
||||
let gmi_entry base_url text =
|
||||
let open Logarion in
|
||||
let u = Text.short_id text in
|
||||
"<entry><link rel=\"alternate\" href=\"" ^ base_url ^ "/" ^ u ^ ".gmi\" />"
|
||||
^ title text ^ id text ^ updated text ^ authors text
|
||||
^ (opt_element "summary" @@ esc @@ Text.str "abstract" text)
|
||||
^ String_set.fold (fun elt a -> a ^ "<category term=\"" ^ elt ^ "\"/>") (Text.set "topics" text) ""
|
||||
^ "</entry>\n"
|
||||
|
||||
let feed title archive_id base_url alternate_type texts =
|
||||
let entry, self = match alternate_type with
|
||||
| "text/gemini" -> gmi_entry, base_url^"/gmi.atom"
|
||||
| "text/html" | _ -> htm_entry, base_url^"/feed.atom" in
|
||||
{|<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:base="|} ^ base_url ^ {|"><title>|}
|
||||
^ title ^ {|</title><link rel="alternate" type="|} ^ alternate_type ^ {|" href="|}
|
||||
^ base_url ^ {|/" /><link rel="self" type="application/atom+xml" href="|}
|
||||
^ self ^ {|" /><id>urn:uuid:|} ^ Logarion.Id.to_string archive_id ^ "</id><updated>"
|
||||
^ Ptime.to_rfc3339 (Ptime_clock.now ()) ^ "</updated>\n"
|
||||
^ List.fold_left (fun acc t -> acc ^ entry base_url t) "" texts
|
||||
^ "</feed>"
|
||||
142
cli/cli.ml
Normal file
142
cli/cli.ml
Normal file
@@ -0,0 +1,142 @@
|
||||
let version = "%%VERSION%%"
|
||||
|
||||
open Cmdliner
|
||||
open Logarion
|
||||
module A = Logarion.Archive.Make(File_store)
|
||||
|
||||
(* TODO: merge in lib/ so other modules can use (.e.g HTTP pull) *)
|
||||
let text_list order_opt reverse_opt number_opt values_opt authors_opt topics_opt =
|
||||
match A.of_path (Sys.getcwd ()) with
|
||||
| Error msg -> prerr_endline msg
|
||||
| Ok archive ->
|
||||
let predicates = A.predicate A.authored authors_opt @ A.predicate A.topics topics_opt in
|
||||
let predicate text = List.fold_left (fun a e -> a && e text) true predicates in
|
||||
let print_fold ~predicate fn =
|
||||
let ts = A.fold ~predicate fn String_set.empty archive in
|
||||
String_set.iter (print_endline) ts
|
||||
in
|
||||
let list_text (t, fname) = print_endline (Text.short_id t ^ " " ^ t.Text.title ^ "\t" ^ fname) in
|
||||
match values_opt with
|
||||
| Some "topics" -> print_fold ~predicate (fun a (e,_) -> (String_set.union a (Text.set "topics" e)))
|
||||
| Some "authors" ->
|
||||
let s = A.fold ~predicate (fun a (e,_) -> Person.Set.union a e.Text.authors) Person.Set.empty archive in
|
||||
print_endline @@ Person.Set.to_string s
|
||||
| Some x -> prerr_endline @@ "Unrecognised field: " ^ x
|
||||
| None -> match order_opt with
|
||||
| false -> A.iter ~predicate list_text archive
|
||||
| true ->
|
||||
let order = match reverse_opt with true -> A.newest | false -> A.oldest in
|
||||
match number_opt with
|
||||
| Some number -> A.iter ~predicate ~order ~number list_text archive
|
||||
| None -> A.iter ~predicate ~order list_text archive
|
||||
|
||||
let list_term =
|
||||
let reverse = Arg.(value & flag & info ["r"] ~doc:"reverse order") in
|
||||
let time = Arg.(value & flag & info ["t"] ~doc:"Sort by time, newest first") in
|
||||
let number = Arg.(value & opt (some int) None & info ["n"] ~docv:"NUMBER" ~doc:"number of entries to list") in
|
||||
let values = Arg.(value & opt (some string) None & info ["values"] ~docv:"HEADER-FIELD" ~doc:"unique values for header field") in
|
||||
let authed = Arg.(value & opt (some string) None & info ["authored"] ~docv:"AUTHORS" ~doc:"texts by authors") in
|
||||
let topics = Arg.(value & opt (some string) None & info ["topics"] ~docv:"TOPICS" ~doc:"texts with topics") in
|
||||
Term.(const text_list $ time $ reverse $ number $ values $ authed $ topics),
|
||||
Term.info "list" ~doc:"list texts" ~man:[ `S "DESCRIPTION"; `P "List texts" ]
|
||||
|
||||
let print_last search_mine =
|
||||
let last a ((t,_) as pair) = match a with None -> Some pair
|
||||
| Some (t', _) as pair' -> if Text.newest t t' > 0 then Some pair else pair' in
|
||||
match A.of_path (Sys.getcwd ()) with
|
||||
| Error msg -> prerr_endline msg
|
||||
| Ok archive ->
|
||||
let last_mine a ((t,_) as pair) =
|
||||
let open Text in
|
||||
match a with None ->
|
||||
if Person.Set.subset archive.A.archivists t.authors then Some pair else None
|
||||
| Some (t', _) as pair' ->
|
||||
if Text.newest t t' > 0 && Person.Set.subset archive.A.archivists t'.authors
|
||||
then Some pair else pair'
|
||||
in
|
||||
match A.fold (if search_mine then last_mine else last) None archive with
|
||||
| Some (_,f) -> print_endline f | None -> ()
|
||||
|
||||
let last_term =
|
||||
let mine = Arg.(value & flag & info ["mine"] ~doc:"last text authored by me") in
|
||||
Term.(const print_last $ mine),
|
||||
Term.info "last" ~doc:"most recent test" ~man:[ `S "DESCRIPTION"; `P "Print the filename of most recent text" ]
|
||||
|
||||
let split_filetypes files =
|
||||
let acc (dirs, files) x = if Sys.is_directory x then (x::dirs, files) else (dirs, x::files) in
|
||||
List.fold_left acc ([],[]) files
|
||||
|
||||
let file files = match A.of_path "." with
|
||||
| Error msg -> prerr_endline msg
|
||||
| Ok _archive ->
|
||||
let dirs, files = split_filetypes files in
|
||||
let _link_as_named dir file = Unix.link file (dir ^"/"^ file) in
|
||||
let link_with_id dir file =
|
||||
match File_store.to_text file with Error s -> prerr_endline s
|
||||
| Ok t -> Unix.link file (dir ^"/"^ String.sub (Id.to_string (t.Text.uuid)) 0 8 ^".txt")
|
||||
in
|
||||
let link = link_with_id in
|
||||
List.iter (fun d -> List.iter (link d) files) dirs
|
||||
|
||||
let file_term =
|
||||
let files = Arg.(value & pos_all string [] & info [] ~doc:"filenames") in
|
||||
let doc = "file texts in directories" in
|
||||
let man = [ `S "DESCRIPTION"; `P doc ] in
|
||||
Term.(const file $ files), Term.info "file" ~doc ~man
|
||||
|
||||
let unfile files = match A.of_path "." with
|
||||
| Error msg -> prerr_endline msg
|
||||
| Ok _archive ->
|
||||
let dirs, files = split_filetypes files in
|
||||
let unlink dir file = try Unix.unlink (dir ^"/"^ file) with Unix.(Unix_error(ENOENT,_,_))-> () in
|
||||
List.iter (fun d -> List.iter (unlink d) files) dirs
|
||||
|
||||
let unfile_term =
|
||||
let files = Arg.(value & pos_all string [] & info [] ~doc:"filenames") in
|
||||
let doc = "unfile texts from directories" in
|
||||
let man = [ `S "DESCRIPTION"; `P doc ] in
|
||||
Term.(const unfile $ files), Term.info "unfile" ~doc ~man
|
||||
|
||||
let init _force = File_store.init ()
|
||||
|
||||
let init_term =
|
||||
let force = Arg.(value & flag & info ["f"; "force"] ~doc:"Initialise even if directory is not empty") in
|
||||
let doc = "initialise a text repository in present directory" in
|
||||
let man = [ `S "DESCRIPTION"; `P "Start an archive in current directory" ] in
|
||||
Term.(const init $ force), Term.info "init" ~doc ~man
|
||||
|
||||
let new_term =
|
||||
let f title topics_opt interactive =
|
||||
match A.of_path "." with
|
||||
| Error m -> prerr_endline m
|
||||
| Ok archive ->
|
||||
let t = match title with "" -> "Draft" | _ -> title in
|
||||
let authors = archive.archivists in
|
||||
let date = Date.({ created = Some (Ptime_clock.now ()); edited = None }) in
|
||||
let text = { (Text.blank ()) with title = t; authors; date } in
|
||||
let text = try Text.with_str_set text "Topics" (Option.get topics_opt) with _ -> text in
|
||||
match File_store.with_text archive text with
|
||||
| Error s -> prerr_endline s
|
||||
| Ok (filepath, _note) ->
|
||||
match interactive with false -> print_endline filepath
|
||||
| true ->
|
||||
print_endline @@ "Created: " ^ filepath;
|
||||
let _code = Sys.command ("$EDITOR " ^ filepath) in
|
||||
()
|
||||
in
|
||||
let title = Arg.(value & pos 0 string "" & info [] ~docv:"TITLE" ~doc:"Title for new article") in
|
||||
let topics= Arg.(value & opt (some string) None & info ["t"; "topics"] ~docv:"TOPICS" ~doc:"Topics for new article") in
|
||||
let inter = Arg.(value & flag & info ["i"; "interactive"] ~doc:"Prompts through the steps of creation and publication") in
|
||||
let man = [ `S "DESCRIPTION"; `P "Create a new article, with title 'Draft' when none provided"] in
|
||||
Term.(const f $ title $ topics $ inter), Term.info "new" ~doc:"create a new article" ~man
|
||||
|
||||
let default_cmd =
|
||||
let doc = "text archival & publishing" in
|
||||
let man = [ `S "BUGS"; `P "Submit bugs <mailto:logarion@lists.orbitalfox.eu?subject=Issue: " ] in
|
||||
Term.(ret (const (`Help (`Pager, None)))), Term.info "txt" ~version ~doc ~man
|
||||
|
||||
let cmds = [ init_term; new_term; file_term; unfile_term; list_term; last_term; Convert.term; Http.pull_term ]
|
||||
|
||||
let () =
|
||||
Random.self_init();
|
||||
match Term.eval_choice default_cmd cmds with `Error _ -> exit 1 | _ -> exit 0
|
||||
90
cli/convert.ml
Normal file
90
cli/convert.ml
Normal file
@@ -0,0 +1,90 @@
|
||||
open Logarion
|
||||
module A = Archive.Make (Logarion.File_store)
|
||||
|
||||
let convert_modified source dest fn title text =
|
||||
if (try Unix.((stat dest).st_mtime < (stat source).st_mtime) with _-> true)
|
||||
then (File_store.file dest (fn title text); true) else false
|
||||
|
||||
let word_fname dir text = dir ^ "/" ^ Text.alias text
|
||||
let id_fname dir text = dir ^ "/" ^ Text.short_id text
|
||||
|
||||
let writer types dir name (text,store_item) = (* todo: single_parser -> [files] *)
|
||||
(* convert_modified store_item idfilename (fun _title -> Text.to_string) text.title text;*)
|
||||
let h = if "htm" = types || "all" = types then
|
||||
convert_modified store_item (id_fname dir text ^ ".htm") Html.page name text
|
||||
else false in
|
||||
let g = if "gmi" = types || "all" = types then
|
||||
convert_modified store_item (id_fname dir text ^ ".gmi") Gemini.page name text
|
||||
else false in
|
||||
h || g
|
||||
|
||||
let index_writer types noindex dir archive topic_roots topic_map texts =
|
||||
let name = archive.A.name in
|
||||
let file path = File_store.file (dir ^ path) in
|
||||
file "/index.pck" (Header_pack.pack archive texts);
|
||||
if not noindex && ("htm" = types || "all" = types) then (
|
||||
let index_name = try Store.KV.find "HTML-index" archive.File_store.kv
|
||||
with Not_found -> "index.html" in
|
||||
if index_name <> "" then
|
||||
file ("/"^index_name) (Html.topic_main_index name topic_roots texts);
|
||||
file "/index.date.htm" (Html.date_index name texts);
|
||||
List.iter
|
||||
(fun topic -> file ("/index." ^ topic ^ ".htm")
|
||||
(Html.topic_sub_index name topic_map topic texts))
|
||||
topic_roots;
|
||||
let base_url = try Store.KV.find "HTTP-URL" archive.File_store.kv
|
||||
with Not_found -> prerr_endline "Missing `HTTP-URL:` in config"; "" in
|
||||
file "/feed.atom" (Atom.feed archive.A.name archive.A.id base_url "text/html" texts)
|
||||
);
|
||||
if not noindex && ("gmi" = types || "all" = types) then (
|
||||
let index_name = try Store.KV.find "Gemini-index" archive.File_store.kv
|
||||
with Not_found -> "index.gmi" in
|
||||
if index_name <> "" then
|
||||
file ("/"^index_name) (Gemini.topic_main_index name topic_roots texts);
|
||||
file "/index.date.gmi" (Gemini.date_index name texts);
|
||||
List.iter
|
||||
(fun topic -> file ("/index." ^ topic ^ ".gmi")
|
||||
(Gemini.topic_sub_index name topic_map topic texts))
|
||||
topic_roots;
|
||||
let base_url = try Store.KV.find "GEMINI-URL" archive.File_store.kv
|
||||
with Not_found -> prerr_endline "Missing `GEMINI-URL:` in config"; "" in
|
||||
file "/gmi.atom" (Atom.feed archive.A.name archive.A.id base_url "text/gemini" texts)
|
||||
)
|
||||
|
||||
let txt_writer types dir name ((text, _store_item) as r) =
|
||||
match Text.str "Content-Type" text with
|
||||
| "" | "text/plain" -> writer types dir name r
|
||||
| x -> prerr_endline ("Can't convert Content-Type: "^x^" file: " ^text.Text.title); false
|
||||
|
||||
let convert_all types noindex dir archive =
|
||||
let name = archive.A.name in
|
||||
let fn (ts,ls,acc) ((elt,_) as r) =
|
||||
(Topic_set.to_map ts (Text.set "topics" elt)),
|
||||
elt::ls, if txt_writer types dir name r then acc+1 else acc in
|
||||
let empty = Topic_set.Map.empty in
|
||||
let topic_map, texts, count = A.(fold ~order:newest fn (empty,[],0) archive) in
|
||||
let topic_roots = Topic_set.roots topic_map in
|
||||
index_writer types noindex dir archive topic_roots topic_map texts;
|
||||
print_endline @@ "Converted: " ^ string_of_int (count)
|
||||
^ "\nIndexed: " ^ string_of_int (List.length texts);
|
||||
Ok ()
|
||||
|
||||
let convert_dir types noindex cmd_dir =
|
||||
let (>>=) = Result.bind in
|
||||
let with_dir dir =
|
||||
Result.map_error (function `Msg m -> m)
|
||||
Logarion.File_store.Directory.(directory dir |> print ~descr:"export" dir) in
|
||||
(A.of_path "."
|
||||
>>= fun archive -> (match cmd_dir with "" -> Error "unspecified export dir" | x -> Ok x)
|
||||
>>= fun dir -> with_dir dir
|
||||
>>= fun _ -> convert_all types noindex dir { archive with store = dir })
|
||||
|> function Ok () -> () | Error x -> prerr_endline x
|
||||
|
||||
open Cmdliner
|
||||
|
||||
let term =
|
||||
let directory = Arg.(value & pos 0 string "" & info [] ~docv:"target directory" ~doc:"Directory to convert into") in
|
||||
let types = Arg.(value & opt string "all" & info ["t"; "type"] ~docv:"TYPES" ~doc:"Convert to type") in
|
||||
let noindex = Arg.(value & flag & info ["noindex"] ~doc:"don't write an index when converting") in
|
||||
Term.(const convert_dir $ types $ noindex $ directory),
|
||||
Term.info "convert" ~doc:"convert archive" ~man:[ `S "DESCRIPTION"; `P "Convert Logarion archive" ]
|
||||
5
cli/dune
Normal file
5
cli/dune
Normal file
@@ -0,0 +1,5 @@
|
||||
(executable
|
||||
(name cli)
|
||||
(public_name txt)
|
||||
(modules cli convert html atom gemini)
|
||||
(libraries logarion logarion.http re.str cmdliner bos ptime ptime.clock.os text_parse.converter text_parse.parsers msgpck))
|
||||
73
cli/gemini.ml
Normal file
73
cli/gemini.ml
Normal file
@@ -0,0 +1,73 @@
|
||||
let page _archive_title text =
|
||||
let open Logarion.Text in
|
||||
"# " ^ text.title
|
||||
^ "\nAuthors: " ^ Logarion.Person.Set.to_string text.authors
|
||||
^ "\nDate: " ^ Logarion.Date.(pretty_date @@ listing text.date)
|
||||
^ let module T = Parsers.Plain_text.Make (Converter.Gemini) in
|
||||
"\n" ^ T.of_string text.body ""
|
||||
|
||||
let date_index title meta_list =
|
||||
List.fold_left
|
||||
(fun a m ->
|
||||
a ^ "=> " ^ Logarion.Text.short_id m ^ ".gmi " ^
|
||||
Logarion.(Date.(pretty_date (listing m.date)) ^ " " ^ m.title) ^ "\n")
|
||||
("# " ^ title ^ "\n\n## Posts by date\n\n") meta_list
|
||||
|
||||
let to_dated_links ?(limit) meta_list =
|
||||
let meta_list = match limit with
|
||||
| None -> meta_list
|
||||
| Some limit->
|
||||
let rec reduced acc i = function
|
||||
| [] -> acc
|
||||
| h::t -> if i < limit then reduced (h::acc) (i+1) t else acc in
|
||||
List.rev @@ reduced [] 0 meta_list
|
||||
in
|
||||
List.fold_left
|
||||
(fun a m ->
|
||||
a
|
||||
^ "=> " ^ Logarion.Text.short_id m ^ ".gmi "
|
||||
^ Logarion.(Date.(pretty_date (listing m.Text.date))) ^ " "
|
||||
^ m.Logarion.Text.title ^ "\n")
|
||||
"" meta_list
|
||||
|
||||
let topic_link root topic =
|
||||
"=> index." ^ root ^ ".gmi " ^ String.capitalize_ascii topic ^ "\n"
|
||||
|
||||
let text_item path meta =
|
||||
let open Logarion in
|
||||
"=> " ^ path ^ Text.short_id meta ^ ".gmi "
|
||||
^ Date.(pretty_date (listing meta.Text.date)) ^ " "
|
||||
^ meta.Text.title ^ "\n"
|
||||
|
||||
let listing_index topic_map topic_roots path metas =
|
||||
let rec item_group topics =
|
||||
List.fold_left (fun acc topic -> acc ^ sub_groups topic ^ items topic) "" topics
|
||||
and sub_groups topic = match Logarion.Topic_set.Map.find_opt topic topic_map with
|
||||
| None -> ""
|
||||
| Some (_, subtopics) -> item_group (Logarion.String_set.elements subtopics)
|
||||
and items topic =
|
||||
let items =
|
||||
let open Logarion in
|
||||
List.fold_left
|
||||
(fun a e ->
|
||||
if String_set.mem topic (String_set.map (Logarion.Topic_set.topic) (Text.set "Topics" e))
|
||||
then text_item path e ^ a else a) "" metas in
|
||||
match items with
|
||||
| "" -> ""
|
||||
| x -> "## " ^ String.capitalize_ascii topic ^ "\n\n" ^ x
|
||||
in
|
||||
item_group topic_roots
|
||||
|
||||
let fold_topic_roots topic_roots =
|
||||
let list_item root t = topic_link root t in
|
||||
List.fold_left (fun a x -> a ^ list_item x x) "" (List.rev topic_roots)
|
||||
|
||||
let topic_main_index title topic_roots metas =
|
||||
"# " ^ title ^ "\n\n"
|
||||
^ (if topic_roots <> [] then ("## Main topics\n\n" ^ fold_topic_roots topic_roots) else "")
|
||||
^ "\n## Latest\n\n" ^ to_dated_links ~limit:10 metas
|
||||
^ "\n=> index.date.gmi More by date\n"
|
||||
|
||||
let topic_sub_index title topic_map topic_root metas =
|
||||
"# " ^ title ^ "\n\n"
|
||||
^ listing_index topic_map [topic_root] "" metas
|
||||
125
cli/html.ml
Normal file
125
cli/html.ml
Normal file
@@ -0,0 +1,125 @@
|
||||
let wrap (title:string) (subtitle:string) body =
|
||||
{|<!DOCTYPE HTML>|}
|
||||
^ {|<html><head><title>|}
|
||||
^ subtitle ^ " | " ^ title
|
||||
^ {|</title><link rel="stylesheet" href="main.css">|}
|
||||
^ {|<link rel="alternate" href="feed.atom" type="application/atom+xml">|}
|
||||
^ {|<meta charset="utf-8"/>|}
|
||||
^ {|<meta name="viewport" content="width=device-width, initial-scale=1.0">|}
|
||||
^ {|</head><body><header><a href=".">|} ^ title
|
||||
^ {|</a> <nav><a href="feed.atom" id="feed">feed</a></nav></header>|} ^ body
|
||||
^ "</body></html>"
|
||||
|
||||
let topic_link root topic =
|
||||
let replaced_space = String.map (function ' '->'+' | x->x) in
|
||||
{|<a href="index.|} ^ root ^ {|.htm#|} ^ replaced_space topic ^ {|">|}
|
||||
^ String.capitalize_ascii topic ^ "</a>"
|
||||
|
||||
let page archive_title text =
|
||||
let open Logarion in
|
||||
let open Text in
|
||||
let module T = Parsers.Plain_text.Make (Converter.Html) in
|
||||
let sep_append ?(sep=", ") a x = match a,x with "",_ -> x | _, "" -> a | _ -> a ^ sep ^ x in
|
||||
let opt_kv key value = if String.length value > 0 then "<dt>" ^ key ^ "<dd>" ^ value else "" in
|
||||
(* let author acc auth = sep_append acc Person.(auth.name ^ " ") in*)
|
||||
let authors = (Person.Set.to_string text.authors ^ " ") in
|
||||
let keywords = str_set "keywords" text in
|
||||
let header =
|
||||
let time x = {|<time datetime="|} ^ x ^ {|">|} ^ x ^ "</time>" in
|
||||
let topic_links x =
|
||||
let to_linked t a =
|
||||
let ts = Topic_set.of_string t in
|
||||
sep_append a (List.fold_left (fun a t -> sep_append ~sep:" > " a (topic_link (List.hd ts) t)) "" ts) in
|
||||
String_set.fold to_linked x "" in
|
||||
"<article><header><dl>"
|
||||
^ opt_kv "Title:" text.title
|
||||
^ opt_kv "Authors:" authors
|
||||
^ opt_kv "Date: " (time (Date.(pretty_date @@ listing text.date)))
|
||||
^ opt_kv "Series: " (str_set "series" text)
|
||||
^ opt_kv "Topics: " (topic_links (set "topics" text))
|
||||
^ opt_kv "Keywords: " keywords
|
||||
^ opt_kv "Id: " (Id.to_string text.uuid)
|
||||
^ {|</dl></header><pre style="white-space:pre-wrap">|} in
|
||||
wrap archive_title text.title ((T.of_string text.body header) ^ "</pre></article>")
|
||||
|
||||
let to_dated_links ?(limit) meta_list =
|
||||
let meta_list = match limit with
|
||||
| None -> meta_list
|
||||
| Some limit->
|
||||
let rec reduced acc i = function
|
||||
| [] -> acc
|
||||
| h::t -> if i < limit then reduced (h::acc) (i+1) t else acc in
|
||||
List.rev @@ reduced [] 0 meta_list
|
||||
in
|
||||
List.fold_left
|
||||
(fun a m ->
|
||||
a ^ Logarion.(Date.(pretty_date (listing m.Text.date)) ^ " ")
|
||||
^ {|<a href="|} ^ Logarion.Text.short_id m ^ {|.htm">|} ^ m.Logarion.Text.title ^ "</a><br>")
|
||||
"" meta_list
|
||||
|
||||
let date_index ?(limit) title meta_list =
|
||||
match limit with
|
||||
| Some limit -> wrap title "Index" (to_dated_links ~limit meta_list)
|
||||
| None -> wrap title "Index" (to_dated_links meta_list)
|
||||
|
||||
let fold_topic_roots topic_roots =
|
||||
let list_item root t = "<li>" ^ topic_link root t in
|
||||
"<nav><h2>Main topics</h2>"
|
||||
^ List.fold_left (fun a x -> a ^ list_item x x) "<ul>" (List.rev topic_roots)
|
||||
^ "</ul></nav>"
|
||||
|
||||
let fold_topics topic_map topic_roots metas =
|
||||
let open Logarion in
|
||||
let rec unordered_list root topic =
|
||||
List.fold_left (fun a x -> a ^ list_item root x) "<ul>" topic
|
||||
^ "</ul>"
|
||||
and sub_items root topic = match Topic_set.Map.find_opt topic topic_map with
|
||||
| None -> ""
|
||||
| Some (_, subtopics) -> unordered_list root (String_set.elements subtopics)
|
||||
and list_item root t =
|
||||
let item =
|
||||
if List.exists (fun x -> String_set.mem t (String_set.map Topic_set.topic (Text.set "topics" x))) metas
|
||||
then topic_link root t else String.capitalize_ascii t
|
||||
in
|
||||
"<li>" ^ item ^ sub_items root t
|
||||
in
|
||||
"<nav><h2>Topics</h2>"
|
||||
^ List.fold_left (fun a x -> a ^ list_item x x) "<ul>" (List.rev topic_roots)
|
||||
^ "</ul></nav>"
|
||||
|
||||
let text_item path meta =
|
||||
let open Logarion in
|
||||
"<time>" ^ Date.(pretty_date (listing meta.Text.date))
|
||||
^ {|</time> <a href="|} ^ path ^ Text.short_id meta ^ {|.htm">|} ^ meta.Text.title
|
||||
^ "</a><br>"
|
||||
|
||||
let listing_index topic_map topic_roots path metas =
|
||||
let rec item_group topics =
|
||||
List.fold_left (fun acc topic -> acc ^ sub_groups topic ^ items topic) "" topics
|
||||
and sub_groups topic = match Logarion.Topic_set.Map.find_opt topic topic_map with
|
||||
| None -> ""
|
||||
| Some (_, subtopics) -> item_group (Logarion.String_set.elements subtopics)
|
||||
and items topic =
|
||||
let items =
|
||||
let open Logarion in
|
||||
List.fold_left
|
||||
(fun a e ->
|
||||
if String_set.mem topic (String_set.map (Logarion.Topic_set.topic) (Text.set "Topics" e))
|
||||
then text_item path e ^ a else a) "" metas in
|
||||
match items with
|
||||
| "" -> ""
|
||||
| x -> {|<h2 id="|} ^ topic ^ {|">|} ^ String.capitalize_ascii topic ^ "</h2>" ^ x
|
||||
in
|
||||
"<nav><h1>Texts</h1>" ^ item_group topic_roots ^ "</nav>"
|
||||
|
||||
let topic_main_index title topic_roots metas =
|
||||
wrap title "Topics"
|
||||
(fold_topic_roots topic_roots
|
||||
^ "<nav><h1>Latest</h1>" ^ to_dated_links ~limit:10 metas
|
||||
^ {|<a href="index.date.htm">More by date</a></nav>|} )
|
||||
|
||||
let topic_sub_index title topic_map topic_root metas =
|
||||
wrap title topic_root
|
||||
(fold_topics topic_map [topic_root] metas
|
||||
(* ^ {|<a href=".atom" id="feed">|}^ String.capitalize_ascii topic_root ^{| feed </a>|}*)
|
||||
^ listing_index topic_map [topic_root] "" metas)
|
||||
Reference in New Issue
Block a user