Changeset 3 in code
- Timestamp:
- Apr 15, 2022, 1:17:01 PM (3 years ago)
- Location:
- trunk
- Files:
-
- 10 added
- 3 deleted
- 21 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Makefile
r2 r3 3 3 4 4 cli: 5 dune build cli/ cli.exe5 dune build cli/txt.exe 6 6 7 7 clean: … … 11 11 dune subst 12 12 dune build 13 cp _build/default/cli/ cli.exe txt13 cp _build/default/cli/txt.exe txt 14 14 strip txt 15 tar czvf "logarion-$(shell date -r _build/default/cli/cli.exe "+%y-%m-%d")-$(shell uname -s)-$(shell uname -m)-$(shell git rev-parse --short HEAD).tar.gz" txt ReadMe15 tar czvf "logarion-$(shell date -r _build/default/cli/cli.exe "+%y-%m-%d")-$(shell uname -s)-$(shell uname -m)-$(shell git rev-parse --short HEAD).tar.gz" txt readme 16 16 rm txt 17 17 -
trunk/cli/atom.ml
r2 r3 10 10 module P = Parsers.Plain_text.Make (Converter.Html) 11 11 12 let id txt = "<id>urn:uuid:" ^ Logarion.( Id.to_string txt.Text.uuid) ^ "</id>"12 let id txt = "<id>urn:uuid:" ^ Logarion.(txt.Text.id) ^ "</id>" 13 13 let title text = "<title>" ^ esc text.Logarion.Text.title ^ "</title>" 14 14 15 15 let authors text = 16 let u acc addr = acc ^ element "uri" (Uri.to_string addr)in16 let u acc addr = acc ^ element "uri" addr in 17 17 let open Logarion in 18 18 let fn txt a = … … 52 52 ^ title ^ {|</title><link rel="alternate" type="|} ^ alternate_type ^ {|" href="|} 53 53 ^ base_url ^ {|/" /><link rel="self" type="application/atom+xml" href="|} 54 ^ self ^ {|" /><id>urn:uuid:|} ^ Logarion.Id.to_stringarchive_id ^ "</id><updated>"55 ^ Ptime.to_rfc3339 (Ptime_clock.now ()) ^ "</updated>\n"54 ^ self ^ {|" /><id>urn:uuid:|} ^ archive_id ^ "</id><updated>" 55 ^ Logarion.Date.now () ^ "</updated>\n" 56 56 ^ List.fold_left (fun acc t -> acc ^ entry base_url t) "" texts 57 57 ^ "</feed>" -
trunk/cli/convert.ml
r2 r3 1 1 open Logarion 2 module A = Archive.Make (Logarion.File_store)3 2 4 let convert_modified source dest fn title text = 5 if (try Unix.((stat dest).st_mtime < (stat source).st_mtime) with _-> true) 6 then (File_store.file dest (fn title text); true) else false 3 let is_older source dest = try 4 Unix.((stat dest).st_mtime < (stat source).st_mtime) with _-> true 7 5 8 let word_fname dir text = dir ^ "/" ^ Text.alias text 9 let id_fname dir text = dir ^ "/" ^ Text.short_id text 6 let convert cs r (text, files) = match Text.str "Content-Type" text with 7 | "" | "text/plain" -> 8 let source = List.hd files in 9 let dest = Filename.concat r.Conversion.dir (Text.short_id text) in 10 List.fold_left 11 (fun a f -> 12 let dest = dest ^ f.Conversion.ext in 13 if is_older source dest then (File_store.file dest (f.Conversion.page r text); true) else false 14 || a) 15 false cs 16 | x -> Printf.eprintf "Can't convert Content-Type: %s file: %s" x text.Text.title; false 10 17 11 let writer types dir name (text,store_item) = (* todo: single_parser -> [files] *)12 (* convert_modified store_item idfilename (fun _title -> Text.to_string) text.title text;*) 13 let h = if "htm" = types || "all" = typesthen14 convert_modified store_item (id_fname dir text ^ ".htm") Html.page name text15 else false in16 let g = if "gmi" = types || "all" = types then17 convert_modified store_item (id_fname dir text ^ ".gmi") Gemini.page name text18 else falsein19 h || g18 let converters types kv = 19 let t = [] in 20 let t = if ("htm" = types || "all" = types) then 21 (let htm = Html.init kv in 22 Conversion.{ ext = Html.ext; page = Html.page htm; indices = Html.indices htm })::t 23 else t in 24 let t = if ("gmi" = types || "all" = types) then 25 Conversion.{ ext = Gemini.ext; page = Gemini.page; indices = Gemini.indices}::t else t in 26 t 20 27 21 let index_writer types noindex dir archive topic_roots topic_map texts = 22 let name = archive.A.name in 23 let file path = File_store.file (dir ^ path) in 24 file "/index.pck" (Header_pack.pack archive texts); 25 if not noindex && ("htm" = types || "all" = types) then ( 26 let index_name = try Store.KV.find "HTML-index" archive.File_store.kv 27 with Not_found -> "index.html" in 28 if index_name <> "" then 29 file ("/"^index_name) (Html.topic_main_index name topic_roots texts); 30 file "/index.date.htm" (Html.date_index name texts); 31 List.iter 32 (fun topic -> file ("/index." ^ topic ^ ".htm") 33 (Html.topic_sub_index name topic_map topic texts)) 34 topic_roots; 35 let base_url = try Store.KV.find "HTTP-URL" archive.File_store.kv 36 with Not_found -> prerr_endline "Missing `HTTP-URL:` in config"; "" in 37 file "/feed.atom" (Atom.feed archive.A.name archive.A.id base_url "text/html" texts) 38 ); 39 if not noindex && ("gmi" = types || "all" = types) then ( 40 let index_name = try Store.KV.find "Gemini-index" archive.File_store.kv 41 with Not_found -> "index.gmi" in 42 if index_name <> "" then 43 file ("/"^index_name) (Gemini.topic_main_index name topic_roots texts); 44 file "/index.date.gmi" (Gemini.date_index name texts); 45 List.iter 46 (fun topic -> file ("/index." ^ topic ^ ".gmi") 47 (Gemini.topic_sub_index name topic_map topic texts)) 48 topic_roots; 49 let base_url = try Store.KV.find "GEMINI-URL" archive.File_store.kv 50 with Not_found -> prerr_endline "Missing `GEMINI-URL:` in config"; "" in 51 file "/gmi.atom" (Atom.feed archive.A.name archive.A.id base_url "text/gemini" texts) 52 ) 28 let convert_all converters noindex dir id kv = 29 let empty = Topic_set.Map.empty in 30 let repo = Conversion.{ id; dir; kv; topic_roots = []; topics = empty; texts = [] } in 31 let fn (ts,ls,acc) ((elt,_) as r) = 32 (Topic_set.to_map ts (Text.set "topics" elt)), elt::ls, 33 if convert converters repo r then acc+1 else acc in 34 let topics, texts, count = File_store.(fold ~dir ~order:newest fn (empty,[],0)) in 35 let topic_roots = try List.rev @@ String_set.list_of_csv (Store.KV.find "Topics" kv) 36 with Not_found -> Topic_set.roots topics in 37 let repo = Conversion.{ repo with topic_roots; topics; texts } in 38 if not noindex then List.iter (fun c -> c.Conversion.indices repo) converters; 39 Printf.printf "Converted: %d Indexed: %d\n" count (List.length texts) 53 40 54 let txt_writer types dir name ((text, _store_item) as r) = 55 match Text.str "Content-Type" text with 56 | "" | "text/plain" -> writer types dir name r 57 | x -> prerr_endline ("Can't convert Content-Type: "^x^" file: " ^text.Text.title); false 58 59 let convert_all types noindex dir archive = 60 let name = archive.A.name in 61 let fn (ts,ls,acc) ((elt,_) as r) = 62 (Topic_set.to_map ts (Text.set "topics" elt)), 63 elt::ls, if txt_writer types dir name r then acc+1 else acc in 64 let empty = Topic_set.Map.empty in 65 let topic_map, texts, count = A.(fold ~order:newest fn (empty,[],0) archive) in 66 let topic_roots = Topic_set.roots topic_map in 67 index_writer types noindex dir archive topic_roots topic_map texts; 68 print_endline @@ "Converted: " ^ string_of_int (count) 69 ^ "\nIndexed: " ^ string_of_int (List.length texts); 70 Ok () 71 72 let convert_dir types noindex cmd_dir = 73 let (>>=) = Result.bind in 74 let with_dir dir = 75 Result.map_error (function `Msg m -> m) 76 Logarion.File_store.Directory.(directory dir |> print ~descr:"export" dir) in 77 (A.of_path "." 78 >>= fun archive -> (match cmd_dir with "" -> Error "unspecified export dir" | x -> Ok x) 79 >>= fun dir -> with_dir dir 80 >>= fun _ -> convert_all types noindex dir { archive with store = dir }) 81 |> function Ok () -> () | Error x -> prerr_endline x 41 let convert_dir types noindex dir = 42 match dir with "" -> prerr_endline "unspecified dir" 43 | dir -> 44 let fname = Filename.concat dir "index.pck" in 45 match Header_pack.of_string @@ File_store.to_string fname with 46 | Error s -> prerr_endline s 47 | Ok { info; _ } -> 48 let kv = let f = Filename.concat dir ".convert.conf" in (* TODO: better place to store convert conf? *) 49 if Sys.file_exists f then File_store.of_kv_file f else Store.KV.empty in 50 let kv = if Store.KV.mem "Title" kv then kv 51 else Store.KV.add "Title" info.Header_pack.title kv in 52 let kv = Store.KV.add "Locations" (String.concat ";\n" info.Header_pack.locations) kv in 53 let cs = converters types kv in 54 convert_all cs noindex dir info.Header_pack.id kv 82 55 83 56 open Cmdliner 84 85 57 let term = 86 let directory = Arg.(value & pos 0 string "" & info [] ~docv:"target directory" ~doc:"Directory to convert into") in 87 let types = Arg.(value & opt string "all" & info ["t"; "type"] ~docv:"TYPES" ~doc:"Convert to type") in 88 let noindex = Arg.(value & flag & info ["noindex"] ~doc:"don't write an index when converting") in 58 let directory = Arg.(value & pos 0 string "" & info [] ~docv:"target directory" 59 ~doc:"Directory to convert") in 60 let types = Arg.(value & opt string "all" & info ["t"; "type"] ~docv:"TYPES" 61 ~doc:"Convert to type") in 62 let noindex = Arg.(value & flag & info ["noindex"] 63 ~doc:"don't create indices in target format") in 89 64 Term.(const convert_dir $ types $ noindex $ directory), 90 Term.info "convert" ~doc:"convert archive" ~man:[ `S "DESCRIPTION"; `P "Convert Logarion archive" ] 65 Term.info "convert" ~doc:"convert txts" 66 ~man:[ `S "DESCRIPTION"; `P "Convert texts within a directory to another format. 67 Directory must contain an index.pck. Run `txt index` first." ] -
trunk/cli/dune
r2 r3 1 1 (executable 2 (name cli)2 (name txt) 3 3 (public_name txt) 4 (modules cli convert html atom gemini)5 (libraries logarion logarion.http re.str cmdliner bos ptime ptime.clock.os text_parse.converter text_parse.parsers msgpck))4 (modules txt authors convert conversion file index last listing new topics html atom gemini pull) 5 (libraries text_parse.converter text_parse.parsers logarion msgpck curl str cmdliner)) -
trunk/cli/gemini.ml
r2 r3 1 let page _archive_title text = 1 let ext = ".gmi" 2 3 module GeminiConverter = struct 4 include Converter.Gemini 5 let angled_uri u a = if String.sub u 0 10 <> "urn:txtid:" then 6 angled_uri u a else angled_uri (String.(sub u 10 (length u - 10)) ^ ext) a 7 end 8 9 let page _conversion text = 2 10 let open Logarion.Text in 3 11 "# " ^ text.title 4 12 ^ "\nAuthors: " ^ Logarion.Person.Set.to_string text.authors 5 13 ^ "\nDate: " ^ Logarion.Date.(pretty_date @@ listing text.date) 6 ^ let module T = Parsers.Plain_text.Make ( Converter.Gemini) in14 ^ let module T = Parsers.Plain_text.Make (GeminiConverter) in 7 15 "\n" ^ T.of_string text.body "" 8 16 … … 31 39 "" meta_list 32 40 33 let topic_link root topic = 34 "=> index." ^ root ^ ".gmi " ^ String.capitalize_ascii topic ^ "\n" 41 let topic_link root topic = 42 let replaced_space = String.map (function ' '->'+' | x->x) in 43 "=> index." ^ replaced_space root ^ ".gmi " ^ String.capitalize_ascii topic ^ "\n" 35 44 36 45 let text_item path meta = … … 72 81 "# " ^ title ^ "\n\n" 73 82 ^ listing_index topic_map [topic_root] "" metas 83 84 let indices r = 85 let open Logarion in 86 let file name = File_store.file (Filename.concat r.Conversion.dir name) in 87 let index_name = try Store.KV.find "Gemini-index" r.kv with Not_found -> "index.gmi" in 88 let title = try Store.KV.find "Title" r.Conversion.kv with Not_found -> "" in 89 90 if index_name <> "" then 91 file index_name (topic_main_index title r.topic_roots r.texts); 92 93 file "index.date.gmi" (date_index title r.texts); 94 95 List.iter 96 (fun topic -> file ("index." ^ topic ^ ".gmi") 97 (topic_sub_index title r.topics topic r.texts)) 98 r.topic_roots; 99 100 let base_url = try 101 let _i = Str.(search_forward (regexp "gemini?://[^;]*") (Store.KV.find "Locations" r.kv) 0) in 102 Str.(matched_string (Store.KV.find "Locations" r.kv)) 103 with Not_found -> prerr_endline "Missing location for Gemini"; "" in 104 file "gmi.atom" (Atom.feed title r.id base_url "text/gemini" r.texts) -
trunk/cli/html.ml
r2 r3 1 let wrap (title:string) (subtitle:string) body = 2 {|<!DOCTYPE HTML>|} 3 ^ {|<html><head><title>|} 4 ^ subtitle ^ " | " ^ title 5 ^ {|</title><link rel="stylesheet" href="main.css">|} 6 ^ {|<link rel="alternate" href="feed.atom" type="application/atom+xml">|} 7 ^ {|<meta charset="utf-8"/>|} 8 ^ {|<meta name="viewport" content="width=device-width, initial-scale=1.0">|} 9 ^ {|</head><body><header><a href=".">|} ^ title 10 ^ {|</a> <nav><a href="feed.atom" id="feed">feed</a></nav></header>|} ^ body 11 ^ "</body></html>" 1 type templates_t = { header: string option; footer: string option } 2 type t = { templates : templates_t } 3 4 let ext = ".htm" 5 let empty_templates = { header = None; footer = None } 6 let default_opts = { templates = empty_templates } 7 8 let init kv = 9 let open Logarion in 10 let header = match Store.KV.find "HTM-header" kv with 11 | fname -> Some (File_store.to_string fname) 12 | exception Not_found -> None in 13 let footer = match Store.KV.find "HTM-footer" kv with 14 | fname -> Some (File_store.to_string fname) 15 | exception Not_found -> None in 16 { templates = { header; footer} } 17 18 let wrap c htm text_title body = 19 let site_title = try Logarion.Store.KV.find "Title" c.Conversion.kv 20 with Not_found -> "" in 21 let replace x = let open Str in 22 global_replace (regexp "{{archive-title}}") site_title x 23 |> global_replace (regexp "{{text-title}}") text_title 24 in 25 let header = match htm.templates.header with 26 | Some x -> replace x 27 | None -> "<header><a href='.'>" ^ site_title ^ 28 "</a><nav><a href='feed.atom' id='feed'>feed</a></nav></header>" 29 in 30 let footer = match htm.templates.footer with None -> "" | Some x -> replace x in 31 "<!DOCTYPE HTML><html><head><title>" ^ text_title ^ " • " ^ site_title ^ "</title>\n\ 32 <link rel='stylesheet' href='main.css'>\ 33 <link rel='alternate' href='feed.atom' type='application/atom+xml'>\ 34 <meta charset='utf-8'/><meta name='viewport' content='width=device-width, initial-scale=1.0'>\ 35 </head><body>\n" ^ header ^ body ^ footer ^ "</body></html>" 12 36 13 37 let topic_link root topic = 14 38 let replaced_space = String.map (function ' '->'+' | x->x) in 15 {|<a href="index.|} ^ root ^ {|.htm#|} ^ replaced_space topic ^ {|">|}39 "<a href='index." ^ replaced_space root ^ ".htm#" ^ replaced_space topic ^ "'>" 16 40 ^ String.capitalize_ascii topic ^ "</a>" 17 41 18 let page archive_title text = 42 module HtmlConverter = struct 43 include Converter.Html 44 let angled_uri u a = if String.sub u 0 10 <> "urn:txtid:" then 45 angled_uri u a else angled_uri (String.(sub u 10 (length u - 10)) ^ ext) a 46 end 47 48 let page htm conversion text = 19 49 let open Logarion in 20 50 let open Text in 21 let module T = Parsers.Plain_text.Make ( Converter.Html) in51 let module T = Parsers.Plain_text.Make (HtmlConverter) in 22 52 let sep_append ?(sep=", ") a x = match a,x with "",_ -> x | _, "" -> a | _ -> a ^ sep ^ x in 23 let opt_kv key value = if String.length value > 0 then "<dt>" ^ key ^ "<dd>" ^ value else "" in 53 let opt_kv key value = if String.length value > 0 54 then "<dt>" ^ key ^ "<dd>" ^ value else "" in 24 55 (* let author acc auth = sep_append acc Person.(auth.name ^ " ") in*) 25 56 let authors = (Person.Set.to_string text.authors ^ " ") in … … 39 70 ^ opt_kv "Topics: " (topic_links (set "topics" text)) 40 71 ^ opt_kv "Keywords: " keywords 41 ^ opt_kv "Id: " (Id.to_string text.uuid)72 ^ opt_kv "Id: " text.id 42 73 ^ {|</dl></header><pre style="white-space:pre-wrap">|} in 43 wrap archive_titletext.title ((T.of_string text.body header) ^ "</pre></article>")74 wrap conversion htm text.title ((T.of_string text.body header) ^ "</pre></article>") 44 75 45 76 let to_dated_links ?(limit) meta_list = … … 58 89 "" meta_list 59 90 60 let date_index ?(limit) titlemeta_list =91 let date_index ?(limit) conv htm meta_list = 61 92 match limit with 62 | Some limit -> wrap title"Index" (to_dated_links ~limit meta_list)63 | None -> wrap title"Index" (to_dated_links meta_list)93 | Some limit -> wrap conv htm "Index" (to_dated_links ~limit meta_list) 94 | None -> wrap conv htm "Index" (to_dated_links meta_list) 64 95 65 96 let fold_topic_roots topic_roots = … … 113 144 "<nav><h1>Texts</h1>" ^ item_group topic_roots ^ "</nav>" 114 145 115 let topic_main_index titletopic_roots metas =116 wrap title"Topics"146 let topic_main_index conv htm topic_roots metas = 147 wrap conv htm "Topics" 117 148 (fold_topic_roots topic_roots 118 149 ^ "<nav><h1>Latest</h1>" ^ to_dated_links ~limit:10 metas 119 150 ^ {|<a href="index.date.htm">More by date</a></nav>|} ) 120 151 121 let topic_sub_index titletopic_map topic_root metas =122 wrap titletopic_root152 let topic_sub_index conv htm topic_map topic_root metas = 153 wrap conv htm topic_root 123 154 (fold_topics topic_map [topic_root] metas 124 155 (* ^ {|<a href=".atom" id="feed">|}^ String.capitalize_ascii topic_root ^{| feed </a>|}*) 125 156 ^ listing_index topic_map [topic_root] "" metas) 157 158 open Logarion 159 let indices htm c = 160 let file name = Logarion.File_store.file (Filename.concat c.Conversion.dir name) in 161 let index_name = try Store.KV.find "HTM-index" c.Conversion.kv with Not_found -> "index.html" in 162 let title = try Store.KV.find "Title" c.Conversion.kv with Not_found -> "" in 163 164 if index_name <> "" then 165 file index_name (topic_main_index c htm c.topic_roots c.texts); 166 167 file "index.date.htm" (date_index c htm c.texts); 168 169 List.iter 170 (fun root -> file ("index." ^ root ^ ".htm") (topic_sub_index c htm c.topics root c.texts)) 171 c.topic_roots; 172 173 let base_url = try 174 let _i = Str.(search_forward (regexp "https?://[^;]*") (Store.KV.find "Locations" c.kv) 0) in 175 Str.(matched_string (Store.KV.find "Locations" c.kv)) 176 with Not_found -> prerr_endline "Missing location for HTTP(S)"; "" in 177 file "feed.atom" (Atom.feed title c.id base_url "text/html" c.texts) -
trunk/dune-project
r2 r3 1 1 (lang dune 2.0) 2 2 (name logarion) 3 (homepage "https://logarion.orbitalfox.eu")4 5 (source (uri git://orbitalfox.eu/logarion))6 3 (license EUPL-1.2) 7 ( authors "orbifx")8 ( maintainers "fox@orbitalfox.eu")9 ( bug_reports "mailto:logarion@lists.orbitalfox.eu?subject=Issue:")4 (maintainers "orbifx <fox@orbitalfox.eu>") 5 (homepage "http://logarion.orbitalfox.eu") 6 (source (uri git+https://git.disroot.org/orbifx/logarion.git)) 10 7 11 8 (generate_opam_files true) … … 14 11 (name logarion) 15 12 (synopsis "Texts archival and exchange") 16 (depends re cmdliner bos ptime uuidm uri text_parse msgpck cohttp-lwt-unix tls))13 (depends text_parse (cmdliner (<= 1.0.4)) msgpck ocurl)) -
trunk/lib/archive.ml
r2 r3 1 (*let module S = Set.Make (Text) in*) 2 (*let module M = Map.Make (String) in*) 3 (*let module I = Map.Make (Id) in*) 4 (*let aggr = I.empty, M.empty, M.empty, M.empty in*) 5 (*let fn (id, a, t, k) (n,_) =*) 6 (* let id = I.add n.Text.uuid n id in*) 7 (* let a =*) 8 (* let f e a = M.update (e.Person.name) (function Some v -> Some (S.add n v) | None -> Some (S.singleton n)) a in*) 9 (* Person.Set.fold f n.Text.authors a in*) 10 (* let t =*) 11 (* let f e a = M.update e (function Some v -> Some (S.add n v) | None -> Some (S.singleton n)) a in*) 12 (* String_set.fold f (Text.set "Topics" n) t in*) 13 (* let k =*) 14 (* let f e a = M.update e (function Some v -> Some (S.add n v) | None -> Some (S.singleton n)) a in*) 15 (* String_set.fold f (Text.set "Keywords" n) k in*) 16 (* (id, a, t, k)*) 1 let predicate fn opt = Option.(to_list @@ map fn opt) 17 2 18 module Make (Store : Store.T) = struct 19 include Store20 let predicate fn opt = Option.(to_list @@ map fn opt)3 let authored query_string = 4 let q = Person.Set.of_query @@ String_set.query query_string in 5 fun n -> Person.Set.predicate q n.Text.authors 21 6 22 let authored query_string =23 let q = Person.Set.of_query @@String_set.query query_string in24 fun n -> Person.Set.predicate q n.Text.authors7 let keyworded query_string = 8 let q = String_set.query query_string in 9 fun n -> String_set.(predicate q (Text.set "Keywords" n)) 25 10 26 let keyworded query_string = 27 let q = String_set.query query_string in 28 fun n -> String_set.(predicate q (Text.set "Keywords" n)) 29 30 let topics query_string = 31 let q = String_set.query query_string in 32 fun n -> String_set.(predicate q (Text.set "Topics" n)) 33 end 11 let topics query_string = 12 let q = String_set.query query_string in 13 fun n -> String_set.(predicate q (Text.set "Topics" n)) -
trunk/lib/date.ml
r2 r3 1 type t = { created: Ptime.t option; edited: Ptime.t option}1 type t = { created: string; edited: string } 2 2 let compare = compare 3 let rfc_string date = match date with Some t -> Ptime.to_rfc3339 t | None -> "" 4 let of_string (rfc : string) = match Ptime.of_rfc3339 rfc with Ok (t,_,_) -> Some t | Error _ -> None 5 let listing date = if Option.is_some date.edited then date.edited else date.created 6 let pretty_date = function 7 | Some t -> Ptime.to_date t |> fun (y, m, d) -> Printf.sprintf "%04d-%02d-%02d" y m d 8 | None -> "" 3 let rfc_string date = date 4 let of_string (rfc : string) = rfc 5 let listing date = if date.edited <> "" then date.edited else date.created 6 let pretty_date date = 7 try Scanf.sscanf date "%4s-%2s-%2s" (fun y m d -> Printf.sprintf "%s %s %s" y m d) 8 with Scanf.Scan_failure s as e -> Printf.fprintf stderr "%s for %s\n" s date; raise e 9 let now () = Unix.time () |> Unix.gmtime |> 10 (fun t -> Printf.sprintf "%4d-%02d-%02dT%02d:%02d:%02dZ" 11 (t.tm_year+1900) (t.tm_mon+1) t.tm_mday t.tm_hour t.tm_min t.tm_sec) 12 let to_secs date = 13 Scanf.sscanf date "%4d-%02d-%02dT%02d:%02d:%02d" 14 (fun y mo d h mi s -> (y-1970)*31557600 + mo*2629800 + d*86400 + h*3600 + mi*60 + s) -
trunk/lib/dune
r2 r3 2 2 (name logarion) 3 3 (public_name logarion) 4 (libraries ptime uuidm uri re.str bos text_parse text_parse.parsersmsgpck))4 (libraries text_parse text_parse.parsers unix str msgpck)) -
trunk/lib/file_store.ml
r2 r3 1 1 type t = string 2 type item_t = string 3 type archive_t = { 4 name: string; archivists: Person.Set.t; id: Id.t; 5 kv: string Store.KV.t; store: t } 2 type item_t = t list 6 3 type record_t = Text.t * item_t 7 4 8 5 let extension = ".txt" 6 let def_dir = try Sys.getenv "LOGARION_DIR" with Not_found -> "." 9 7 10 8 let to_string f = 11 9 let ic = open_in f in 12 let n = in_channel_length ic in 13 let s = Bytes.create n in 14 really_input ic s 0 n; 10 let s = really_input_string ic (in_channel_length ic) in 15 11 close_in ic; 16 Bytes.to_strings12 s 17 13 18 let file path content = let out = open_out path in 19 output_string out content; close_out out 14 let fold_file_line fn init file = match open_in file with 15 | exception (Sys_error msg) -> prerr_endline msg; init 16 | file -> 17 let rec read acc = match input_line file with 18 | "" as s | s when String.get s 0 = '#' -> read acc 19 | s -> read (fn s acc) 20 | exception End_of_file -> close_in file; acc 21 in read init 20 22 21 let (//) a b = a ^ "/" ^ b23 let file path str = let o = open_out path in output_string o str; close_out o 22 24 23 25 let to_text path = 24 26 if Filename.extension path = extension then 25 27 (to_string path |> Text.of_string |> Result.map_error (fun m -> path ^": "^ m)) 26 else Error "Not txt"28 else Error (Printf.sprintf "Not txt: %s" path) 27 29 28 30 let newest (a,_pa) (b,_pb) = Text.newest a b 29 31 let oldest (a,_pa) (b,_pb) = Text.oldest a b 30 32 31 let list_iter fn {store;_}paths =32 let link f = match to_text (Filename.concat store f)33 with Ok t -> fn storet f | Error s -> prerr_endline s in33 let list_iter fn dir paths = 34 let link f = match to_text (Filename.concat dir f) with 35 | Ok t -> fn dir t f | Error s -> prerr_endline s in 34 36 List.iter link paths 35 37 36 let iter_valid_text pred fn p = 37 match to_text p with Error _ -> () | Ok t -> if pred t then fn (t, p) 38 module TextMap = Map.Make(Text) 38 39 39 let fold_valid_text pred fn acc p = 40 match to_text p with Error _ -> acc | Ok t -> if pred t then fn acc (t, p) else acc 40 type iteration_t = item_t TextMap.t 41 let new_iteration = TextMap.empty 41 42 42 let list_fs dir = 43 (*let iter_valid_text pred fn path =*) 44 (* match to_text path with Error _ -> () | Ok t -> if pred t then fn (t, p)*) 45 46 let fold_valid_text pred it path = 47 match to_text path with Error _ -> it 48 | Ok t -> if pred t then (TextMap.update t 49 (function None -> Some [path] | Some ps -> Some (path::ps)) it 50 ) else it 51 52 (* Compare file system nodes to skip reparsing? *) 53 let list_fs ?(r=false) dir = 54 let valid_dir f = r && String.get f 0 <> '.' && Sys.is_directory f in 55 let expand_dir d = Array.(to_list @@ map (Filename.concat d) (Sys.readdir d)) in 43 56 let rec loop result = function 44 | [] -> result 45 | f::fs when Sys.is_directory f -> 46 Array.map (Filename.concat f) (Sys.readdir f) 47 |> Array.to_list |> List.append fs |> loop result 57 | f::fs when valid_dir f -> expand_dir f |> List.append fs |> loop result 48 58 | f::fs -> loop (f::result) fs 49 in loop [] [dir] 59 | [] -> result in 60 let dirs = if dir = "." then Array.to_list (Sys.readdir dir) else 61 if not r then expand_dir dir else [dir] in 62 loop [] dirs 50 63 51 64 let list_take n = … … 55 68 in take [] n 56 69 57 let iter ?(predicate=fun _ -> true) ?order ?number fn {store;_} = 58 match order with 59 | None -> List.iter (iter_valid_text predicate fn) @@ list_fs store 60 | Some comp -> 61 List.iter fn 62 @@ (match number with None -> (fun x -> x) | Some n -> list_take n) 63 @@ List.fast_sort comp 64 @@ List.fold_left (fold_valid_text predicate (fun a e -> List.cons e a)) [] 65 @@ list_fs store 70 let fold_sort_take ?(predicate=fun _ -> true) ?(number=None) comp flist = 71 (match number with None -> (fun x -> x) | Some n -> list_take n) 72 @@ List.fast_sort comp @@ TextMap.bindings 73 @@ List.fold_left (fold_valid_text predicate) new_iteration flist 66 74 67 let fold ?(predicate=fun _ -> true) ?order ?number fn acc {store;_} = 68 match order with 69 | None -> List.fold_left (fold_valid_text predicate fn) acc @@ list_fs store 70 | Some comp -> 71 List.fold_left fn acc 72 @@ (match number with None -> (fun x -> x) | Some n -> list_take n) 73 @@ List.fast_sort comp 74 @@ List.fold_left (fold_valid_text predicate (fun a e -> List.cons e a)) [] 75 @@ list_fs store 75 let iter ?(r=false) ?(dir=def_dir) ?(predicate=fun _ -> true) ?order ?number fn = 76 let flist = list_fs ~r dir in match order with 77 | Some comp -> List.iter fn @@ fold_sort_take ~predicate ~number comp flist 78 | None -> List.iter fn @@ TextMap.bindings @@ 79 List.fold_left (fold_valid_text predicate) new_iteration flist 76 80 77 let with_id { store; _ } id = 81 let fold ?(r=false) ?(dir=def_dir) ?(predicate=fun _ -> true) ?order ?number fn acc = 82 let flist = list_fs ~r dir in match order with 83 | Some comp -> List.fold_left fn acc @@ fold_sort_take ~predicate ~number comp flist 84 | None -> List.fold_left fn acc @@ TextMap.bindings @@ 85 List.fold_left (fold_valid_text predicate) new_iteration flist 86 87 let with_id ?(r=false) ?(dir=def_dir) id = 78 88 let matched acc path = 79 89 match to_text path with 80 90 | Error x -> prerr_endline x; acc 81 | Ok text when text.Text. uuid <> id -> acc91 | Ok text when text.Text.id <> id -> acc 82 92 | Ok text -> 83 93 match acc with … … 85 95 | Ok (Some prev) -> if prev = text then acc else Error [text; prev] 86 96 | Error x -> Error (text :: x) 87 in List.fold_left matched (Ok None) (list_fs store)97 in List.fold_left matched (Ok None) (list_fs ~r dir) 88 98 89 module Directory = struct 90 let print ?(descr="") dir result = 91 let () = match result with 92 | Ok true -> print_endline ("Created " ^ descr ^ " directory " ^ dir) 93 | Ok false -> print_endline ("Using existing " ^ descr ^ " directory " ^ dir) 94 | Error (`Msg msg) -> prerr_endline @@ "Failed making " ^ descr ^ ". " ^ msg 95 in 96 result 99 let with_dir ?(descr="") ?(perm=0o740) dir = 100 let mkdir dir = match Unix.mkdir dir perm with 101 | exception Unix.Unix_error (EEXIST, _, _) -> () 102 | exception Unix.Unix_error (code, _fn, arg) -> 103 failwith @@ Printf.sprintf "Error %s making %s dir: %s" 104 (Unix.error_message code) descr arg 105 | _ -> () in 106 let rec mkeach path = function [] | [""] -> () | ""::t -> mkeach path t 107 | hd::t -> let d = Filename.concat path hd in mkdir d; mkeach d t in 108 mkeach 109 (if Filename.is_relative dir then "" else "/") 110 (String.split_on_char '/' dir) 97 111 98 let directory dir = Result.bind (Fpath.of_string dir) Bos.OS.Dir.create 99 100 let rec directories = function 101 | [] -> Ok () 102 | (d, descr)::tl -> 103 match directory d |> print ~descr d with 104 | Ok _ -> directories tl 105 | Error _ -> Error (d, descr) 106 end 107 108 let copy ?(recursive = false) src dst = 109 Bos.OS.Cmd.run (Bos.Cmd.(v "cp" %% (on recursive @@ v "-r") % src % dst)) 112 let rec with_dirs = function [] -> () | (d, descr)::tl -> with_dir ~descr d; with_dirs tl 110 113 111 114 let versioned_basename_of_title ?(version=0) repo extension (title : string) = 112 115 let basename = Text.string_alias title in 113 116 let rec next version = 114 let candidate = repo // basename ^ "." ^ string_of_int version ^ extension in 117 let candidate = Filename.concat repo 118 (basename ^ "." ^ string_of_int version ^ extension) in 115 119 if Sys.file_exists candidate then next (succ version) else candidate 116 120 in 117 121 next version 118 122 119 let uuid_filename repo extension text =123 let id_filename repo extension text = 120 124 let basename = Text.alias text in 121 let candidate = repo // String.sub (Id.to_string text.uuid) 0 6 ^ "." ^ basename ^ extensionin125 let candidate = Filename.concat repo (text.id ^ "." ^ basename ^ extension) in 122 126 if Sys.file_exists candidate then Error "Name clash, try again" else Ok candidate 123 127 124 let with_text {store;_} new_text = 125 Result.bind (uuid_filename store extension new_text) @@ 126 fun path -> 127 try file path (Text.to_string new_text); Ok (path, new_text) with Sys_error s -> Error s 128 129 let basic_config () = 130 "Archive-Name: " 131 ^ "\nArchive-ID: " ^ Id.(generate () |> to_string) 132 ^ "\nArchivists: " ^ Bos.OS.Env.opt_var "USER" ~absent:"" 133 |> Bytes.of_string 134 135 let init ?(dotdir=".logarion/") () = 136 match Directory.directories [dotdir, "dotdir"] with 137 | Error (_dir, _desc) -> () 138 | Ok () -> 139 let config_file = 140 open_out_gen [Open_creat; Open_excl; Open_wronly] 141 0o700 (dotdir // "config") in 142 output_bytes config_file (basic_config ()); 143 close_out config_file 128 let with_text ?(dir=def_dir) new_text = 129 match id_filename dir extension new_text with 130 | Error _ as e -> e 131 | Ok path -> 132 try file path (Text.to_string new_text); Ok (path, new_text) 133 with Sys_error s -> Error s 144 134 145 135 module Config = struct 146 type t = archive_t 147 let key_value k v a = match k with 148 | "Archive-Name" -> { a with name = String.trim v } 149 | "Archive-ID" -> { a with id = Option.get (Id.of_string (String.trim v)) } 150 | "Archivists" -> { a with archivists = Person.Set.of_string v } 151 | _ -> { a with kv = Store.KV.add k (String.trim v) a.kv } 136 type t = string Store.KV.t 137 let key_value k v a = Store.KV.add k (String.trim v) a 152 138 end 153 139 154 let of_ path store=140 let of_kv_file path = 155 141 let open Text_parse in 156 let subsyntaxes = [| (module Parsers.Key_value.Make (Config) : Parser.S with type t = Config.t); (module Parsers.Key_value.Make (Config)); |] in 157 let of_string text acc = Parser.parse subsyntaxes { text; pos = 0; right_boundary = String.length text - 1 } acc in 158 Ok ( 159 of_string (to_string @@ store ^ "/.logarion/config") { 160 name = ""; 161 archivists = Person.Set.empty; 162 id = Id.nil; 163 kv = Store.KV.empty; 164 store = try Sys.getenv "LOGARION_DIR" with Not_found -> "." 165 } 166 ) 142 let subsyntaxes = Parsers.Key_value.[| 143 (module Make (Config) : Parser.S with type t = Config.t); (module Make (Config)); |] in 144 let of_string text acc = 145 Parser.parse subsyntaxes { text; pos = 0; right_boundary = String.length text - 1 } acc in 146 of_string (to_string @@ path) Store.KV.empty -
trunk/lib/header_pack.ml
r2 r3 1 type info_t = { version: int; name: string; archivists: string list } 2 type text_t = { id: Msgpck.t; time: Msgpck.t; title: Msgpck.t; authors: Msgpck.t }3 type t = { info: info_t; fields: string list; texts: Msgpck.t; peers: Msgpck.t }1 let version = 0 2 type info_t = { version: int; id: string; title: string; people: string list; locations: string list } 3 type t = { info: info_t; fields: Msgpck.t; texts: Msgpck.t; peers: Msgpck.t } 4 4 5 let of_id id = Msgpck. Bytes (Id.to_bytes id)6 let to_id pck_id = Id.of_bytes Msgpck.(to_bytes pck_id)5 let of_id id = Msgpck.of_string id 6 let to_id = Msgpck.to_string 7 7 8 8 let person p = Msgpck.String (Person.to_string p) 9 let persons ps = List.rev @@ Person.Set.fold (fun p a -> person p :: a) ps [] 9 let persons ps = Msgpck.of_list @@ List.rev @@ Person.Set.fold (fun p a -> person p :: a) ps [] 10 11 let str = Msgpck.of_string 12 let str_list ls = Msgpck.of_list @@ List.map str ls 13 let to_str_list x = List.map Msgpck.to_string (Msgpck.to_list x) 10 14 11 15 let of_set field t = 12 16 List.rev @@ String_set.fold (fun s a -> Msgpck.String s :: a) (Text.set field t) [] 13 17 14 let date = function 15 | None -> Int32.zero 16 | Some date -> 17 let days, ps = Ptime.Span.to_d_ps (Ptime.to_span date) in 18 Int32.add Int32.(mul (of_int days) 86400l) Int64.(to_int32 (div ps 1000000000000L)) 18 let date = function "" -> Int32.zero | date -> Int32.of_int (Date.to_secs date) 19 19 20 let to_sec = function 21 Msgpck.Int i -> Int32.of_int i | Msgpck.Uint32 i -> i | x -> Msgpck.to_uint32 x 22 23 let public_peers () = 24 Peers.fold_file (fun x a -> Msgpck.String x :: a) [] Peers.public_fname 20 let to_sec = function Msgpck.Int i -> Int32.of_int i | Msgpck.Uint32 i -> i | x -> Msgpck.to_uint32 x 25 21 26 22 let fields = Msgpck.(List [String "id"; String "time"; String "title"; String "authors"; String "topics"]) 27 23 let to_fields fieldpack = List.map Msgpck.to_string (Msgpck.to_list fieldpack) 28 24 29 let to_pack a t = 25 let to_info = function 26 | Msgpck.List (v::id::n::a::ls::[]) -> 27 let people = to_str_list a in 28 let locations = to_str_list ls in 29 Msgpck.({version = to_int v; id = to_string id; title = to_string n; people; locations}) 30 | _ -> invalid_arg "Pack header" 31 32 let of_info i = let open Msgpck in 33 List [Int i.version; String i.id; String i.title; str_list i.people; str_list i.locations] 34 35 let of_text a t = 30 36 let open Text in 31 37 Msgpck.(List [ 32 Bytes (Id.to_bytes t.uuid); of_uint32 (date (Date.listing t.date));33 String t.title; List (persons t.authors); List (of_set "topics" t)38 of_id t.id; of_uint32 (date (Date.listing t.date)); 39 String t.title; persons t.authors; List (of_set "topics" t) 34 40 ]) :: a 35 41 36 let pack_filename ?(filename="index.pck") archive = 37 let dir = Store.KV.find "Export-Dir" archive.File_store.kv in (*raises Not_found*) 38 dir ^ "/" ^ filename 42 let of_text_list l = Msgpck.List l 39 43 40 let to_info = function 41 | Msgpck.List (v::n::a::[]) -> 42 let archivists = List.map Msgpck.to_string (Msgpck.to_list a) in 43 Msgpck.({version = to_int v; name = to_string n; archivists}) 44 | _ -> invalid_arg "Pack header" 44 let pack p = Msgpck.List [of_info p.info; p.fields; p.texts; p.peers] 45 let string p = Bytes.to_string @@ Msgpck.Bytes.to_string @@ pack p 45 46 46 47 let unpack = function 47 | Msgpck.List (i::f::texts::[]) -> 48 Some { info = to_info i; fields = to_fields f; texts; peers = Msgpck.List [] } 49 | Msgpck.List (i::f::texts::peers::[]) -> 50 Some { info = to_info i; fields = to_fields f; texts; peers } 51 | _ -> None 48 | Msgpck.List (i::fields::texts::[]) -> 49 Ok { info = to_info i; fields; texts; peers = Msgpck.List [] } 50 | Msgpck.List (i::fields::texts::peers::[]) -> 51 Ok { info = to_info i; fields; texts; peers } 52 | _ -> Error "format mismatch" 53 54 let of_string s = unpack @@ snd @@ Msgpck.StringBuf.read s 52 55 53 56 let list filename = try … … 61 64 let contains text = function 62 65 | Msgpck.List (id::_time::title::_authors::_topics::[]) -> 63 (match Id.of_bytes (Msgpck.to_bytes id)with64 | None-> prerr_endline ("Invalid id for " ^ Msgpck.to_string title); false65 | Some id -> text.Text.uuid = id)66 (match to_id id with 67 | "" -> prerr_endline ("Invalid id for " ^ Msgpck.to_string title); false 68 | id -> text.Text.id = id) 66 69 | _ -> prerr_endline ("Invalid record pattern"); false 67 70 68 let pack archive records =69 let header_pack = List.fold_left to_pack [] records in70 let info = Msgpck.(List [Int 0; String archive.File_store.name; List (persons archive.archivists)]) in71 Bytes.to_string @@ Msgpck.Bytes.to_string72 (List [info; fields; Msgpck.List header_pack; Msgpck.List (public_peers ())])73 71 74 let add archive records = 75 let fname = pack_filename archive in 76 let append published (t, _f) = if List.exists (contains t) published then published else to_pack published t in 77 match list fname with Error e -> prerr_endline e | Ok published_list -> 78 let header_pack = List.fold_left append published_list records in 79 let archive = Msgpck.(List [Int 0; String archive.File_store.name; 80 List (persons archive.archivists)]) in 81 File_store.file fname @@ Bytes.to_string 82 @@ Msgpck.Bytes.to_string (List [archive; fields; Msgpck.List header_pack]) 72 (*let pack_filename ?(filename="index.pck") archive =*) 73 (* let dir = Store.KV.find "Export-Dir" archive.File_store.kv in (*raises Not_found*)*) 74 (* dir ^ "/" ^ filename*) 83 75 84 let unpublish _archive _records = () 76 (*let add archive records =*) 77 (* let fname = pack_filename archive in*) 78 (* let append published (t, _f) = if List.exists (contains t) published then published else to_pack published t in*) 79 (* match list fname with Error e -> prerr_endline e | Ok published_list ->*) 80 (* let header_pack = List.fold_left append published_list records in*) 81 (* let archive = Msgpck.(List [*) 82 (* Int 0; String archive.File_store.name; persons archive.people]) in*) 83 (* File_store.file fname @@ Bytes.to_string*) 84 (* @@ Msgpck.Bytes.to_string (List [archive; fields; Msgpck.List header_pack])*) -
trunk/lib/id.ml
r2 r3 1 let random_state = Random.State.make_self_init () 2 type t = Uuidm.t 3 let compare = Uuidm.compare 4 let to_string = Uuidm.to_string 5 let of_string = Uuidm.of_string 6 let to_bytes = Uuidm.to_bytes 7 let of_bytes = Uuidm.of_bytes 8 let generate ?(random_state=random_state) = Uuidm.v4_gen random_state 9 let nil = Uuidm.nil 1 let random_state = Random.State.make_self_init 2 3 (*module UUID = struct*) 4 (*type t = Uuidm.t*) 5 (*let compare = Uuidm.compare*) 6 (*let to_string = Uuidm.to_string*) 7 (*let of_string = Uuidm.of_string*) 8 (*let to_bytes = Uuidm.to_bytes*) 9 (*let of_bytes = Uuidm.of_bytes*) 10 (*let generate ?(random_state=random_state ()) = Uuidm.v4_gen random_state*) 11 (*let nil = Uuidm.nil*) 12 (*end*) 13 14 type t = string 15 let compare = String.compare 16 let nil = "" 17 18 let short ?(len) id = 19 let id_len = String.length id in 20 let l = match len with Some l -> l | None -> if id_len = 36 then 8 else 6 in 21 String.sub id 0 (min l id_len) 22 23 let generate ?(len=6) ?(seed=random_state ()) () = 24 let b32 i = char_of_int @@ 25 if i < 10 then i+48 else 26 if i < 18 then i+87 else 27 if i < 20 then i+88 else 28 if i < 22 then i+89 else 29 if i < 27 then i+90 else 30 if i < 32 then i+91 else 31 (invalid_arg ("id.char" ^ string_of_int i)) in 32 let c _ = b32 (Random.State.int seed 31) in 33 String.init len c -
trunk/lib/peers.ml
r2 r3 1 let public_fname = "peers.pub.conf" 2 let private_fname = "peers.priv.conf" 1 let text_dir = Filename.concat (Sys.getenv "HOME") ".local/share/texts" 3 2 4 let fold_file fn init file = match open_in file with 5 | exception (Sys_error msg) -> prerr_endline msg; init 6 | file -> 7 let rec read acc = try read (fn (input_line file) acc) 8 with End_of_file -> close_in file; acc in 9 read init 3 let fold fn init = match Sys.readdir text_dir with 4 | exception (Sys_error msg) -> prerr_endline msg 5 | dirs -> 6 let read_pack path = 7 let pack_path = Filename.(concat text_dir @@ concat path "index.pck") in 8 match Sys.file_exists pack_path with false -> () | true -> 9 match Header_pack.of_string (File_store.to_string pack_path) with 10 | Error s -> Printf.eprintf "%s %s\n" s pack_path 11 | Ok p -> ignore @@ List.fold_left fn init Header_pack.(p.info.locations) 12 in 13 Array.iter read_pack dirs 14 15 let scheme url = 16 let colon_idx = String.index_from url 0 ':' in 17 let scheme = String.sub url 0 colon_idx in 18 (* let remain = String.(sub url (colon_idx+1) (length url - length scheme - 1)) in*) 19 scheme -
trunk/lib/person.ml
r2 r3 1 1 module Person = struct 2 2 type name_t = string 3 type address_t = Uri.t3 type address_t = string 4 4 type t = { name: name_t; addresses: address_t list } 5 5 let empty = { name = ""; addresses = [] } 6 6 let compare = Stdlib.compare 7 let to_string p = List.fold_left (fun a e -> a^" <"^Uri.to_string e^">") p.name p.addresses 7 let name_to_string p = p.name 8 let to_string p = List.fold_left (fun a e -> Printf.sprintf "%s <%s>" a e) p.name p.addresses 8 9 let of_string s = match String.trim s with "" -> empty | s -> 9 match Re.Str.(split (regexp " *< *") s) with10 match Str.(split (regexp " *< *") s) with 10 11 | [] -> empty 11 12 | [n] -> let name = String.trim n in { empty with name } 12 13 | n::adds -> 13 14 let name = String.trim n in 14 let addresses = List.map (fun f -> Uri.of_string @@String.(sub f 0 (length f -1))) adds in15 let addresses = List.map (fun f -> String.(sub f 0 (length f -1))) adds in 15 16 { name; addresses } 16 17 end … … 20 21 module Set = struct 21 22 include Set.Make(Person) 22 let to_string ?( pre="") ?(sep=", ") s =23 let str = Person.to_string in23 let to_string ?(names_only=false) ?(pre="") ?(sep=", ") s = 24 let str = if names_only then Person.name_to_string else Person.to_string in 24 25 let j x a = match a, x with "",_ -> str x | _,x when x = Person.empty -> a | _ -> a^sep^str x in 25 26 fold j s pre -
trunk/lib/store.ml
r2 r3 4 4 type t 5 5 type item_t 6 type archive_t = { name: string; archivists: Person.Set.t; id: Id.t; kv: string KV.t; store: t }6 type archive_t = { id: Id.t; name: string; archivists: Person.Set.t; kv: string KV.t; store: t } 7 7 type record_t = Text.t * item_t 8 8 val of_path: string -> (archive_t, string) result -
trunk/lib/string_set.ml
r2 r3 1 1 include Set.Make(String) 2 2 3 let list_of_csv x = Re.Str.(split (regexp " *, *")) (String.trim x)3 let list_of_csv x = Str.(split (regexp " *, *")) (String.trim x) 4 4 let of_string x = of_list (list_of_csv x) 5 5 let to_string ?(pre="") ?(sep=", ") s = -
trunk/lib/text.ml
r2 r3 1 1 module String_map = Map.Make (String) 2 2 type t = { 3 title: string;4 uuid: Id.t;5 6 7 8 9 10 3 id: Id.t; 4 title: string; 5 authors: Person.Set.t; 6 date: Date.t; 7 string_map: string String_map.t; 8 stringset_map: String_set.t String_map.t; 9 body: string; 10 } 11 11 12 let blank ?( uuid=(Id.generate ())) () = {13 title = "";14 uuid;15 16 date = Date.({ created = None; edited = None});17 18 19 20 12 let blank ?(id=(Id.generate ())) () = { 13 id; 14 title = ""; 15 authors = Person.Set.empty; 16 date = Date.({ created = now (); edited = ""}); 17 string_map = String_map.empty; 18 stringset_map = String_map.empty; 19 body = ""; 20 } 21 21 22 22 let compare = Stdlib.compare … … 29 29 30 30 let with_kv x (k,v) = 31 32 33 34 35 | "id" -> (match Id.of_string v with Some id -> { x with uuid = id } | None -> x)36 37 38 | "date" -> { x with date = Date.{ x.date with created= Date.of_string v }}39 | "date-edited"-> { x with date = Date.{ x.date with edited= Date.of_string v }}40 41 31 let trim = String.trim in 32 match String.lowercase_ascii k with 33 | "body" -> { x with body = String.trim v } 34 | "title"-> { x with title = trim v } 35 | "id" -> (match v with "" -> x | s -> { x with id = s }) 36 | "author" 37 | "authors" -> { x with authors = Person.Set.of_string (trim v)} 38 | "date" -> { x with date = Date.{ x.date with created = Date.of_string v }} 39 | "date-edited"-> { x with date = Date.{ x.date with edited = Date.of_string v }} 40 | "licences" | "topics" | "keywords" | "series" as k -> with_str_set x k v 41 | k -> { x with string_map = String_map.add k (trim v) x.string_map } 42 42 43 let kv_of_string line = match Re.Str.(bounded_split (regexp ": *")) line 2 with44 | [ key; value ] -> Re.Str.(replace_first (regexp "^#\\+") "" key), value45 | [ key ] -> Re.Str.(replace_first (regexp "^#\\+") "" key), ""46 43 let kv_of_string line = match Str.(bounded_split (regexp ": *")) line 2 with 44 | [ key; value ] -> Str.(replace_first (regexp "^#\\+") "" key), value 45 | [ key ] -> Str.(replace_first (regexp "^#\\+") "" key), "" 46 | _ -> "","" 47 47 48 48 let of_header front_matter = 49 let fields = List.map kv_of_string (Re.Str.(split (regexp "\n")) front_matter) in50 List.fold_left with_kv (blank ~uuid:Id.nil ()) fields49 let fields = List.map kv_of_string (Str.(split (regexp "\n")) front_matter) in 50 List.fold_left with_kv (blank ~id:Id.nil ()) fields 51 51 52 52 let front_matter_body_split s = 53 if Re.Str.(string_match (regexp ".*:.*")) s 054 then match Re.Str.(bounded_split (regexp "^$")) s 2 with55 56 57 53 if Str.(string_match (regexp ".*:.*")) s 0 54 then match Str.(bounded_split (regexp "^$")) s 2 with 55 | front::body::[] -> (front, body) 56 | _ -> ("", s) 57 else ("", s) 58 58 59 59 let of_string s = 60 61 62 63 if note.uuid <> Id.nil then Ok note else Error "Missing ID header"64 60 let front_matter, body = front_matter_body_split s in 61 try 62 let note = { (of_header front_matter) with body } in 63 if note.id <> Id.nil then Ok note else Error "Missing ID header" 64 with _ -> Error ("Failed parsing" ^ s) 65 65 66 66 let to_string x = 67 68 69 70 let d field value = match value with Some _ -> field ^ ": " ^ Date.rfc_string value ^ "\n" | None -> "" in71 let rows = 72 [ s "Title" x.title;73 a x.authors;74 d "Date" x.date.Date.created;75 d "Edited" x.date.Date.edited;76 s "Licences" (str_set "licences" x);77 s "Topics" (str_set "topics" x);78 s "Keywords" (str_set "keywords" x);79 s "Series" (str_set "series" x);80 s "Abstract" (str "abstract" x);81 s "ID" (Uuidm.to_string x.uuid);82 s "Alias" (str "Alias" x) ] 83 84 67 let has_len v = String.length v > 0 in 68 let s field value = if has_len value then field ^ ": " ^ value ^ "\n" else "" in 69 let a value = if Person.Set.is_empty value then "" else "Authors: " ^ Person.Set.to_string value ^ "\n" in 70 let d field value = match value with "" -> "" | s -> field ^ ": " ^ Date.rfc_string s ^ "\n" in 71 let rows = [ 72 s "ID" x.id; 73 d "Date" x.date.Date.created; 74 d "Edited" x.date.Date.edited; 75 s "Title" x.title; 76 a x.authors; 77 s "Licences" (str_set "licences" x); 78 s "Topics" (str_set "topics" x); 79 s "Keywords" (str_set "keywords" x); 80 s "Series" (str_set "series" x); 81 s "Abstract" (str "abstract" x); 82 s "Alias" (str "Alias" x) 83 ] in 84 String.concat "" rows ^ "\n" ^ x.body 85 85 86 86 let string_alias t = 87 88 89 90 91 92 93 94 95 96 97 98 99 87 let is_reserved = function 88 | '!' | '*' | '\'' | '(' | ')' | ';' | ':' | '@' | '&' | '=' | '+' | '$' 89 | ',' | '/' | '?' | '#' | '[' | ']' | ' ' | '\t' | '\x00' -> true 90 | _ -> false 91 in 92 let b = Buffer.create (String.length t) in 93 let filter char = 94 let open Buffer in 95 if is_reserved char then (try (if nth b (pred (length b)) <> '-' then add_char b '-') with Invalid_argument _ -> prerr_endline "reserved") 96 else add_char b char 97 in 98 String.(iter filter (lowercase_ascii t)); 99 Buffer.contents b 100 100 101 101 let alias t = match str "alias" t with "" -> string_alias t.title | x -> x 102 let short_id ?(len=8) t = String.sub (Id.to_string t.uuid) 0 len102 let short_id t = Id.short t.id -
trunk/lib/topic_set.ml
r2 r3 1 let of_string x = Re.Str.(split (regexp " *> *")) (String.trim x)1 let of_string x = Str.(split (regexp " *> *")) (String.trim x) 2 2 3 3 let topic x = -
trunk/logarion.opam
r2 r3 2 2 opam-version: "2.0" 3 3 synopsis: "Texts archival and exchange" 4 maintainer: ["fox@orbitalfox.eu"] 5 authors: ["orbifx"] 4 maintainer: ["orbifx <fox@orbitalfox.eu>"] 6 5 license: "EUPL-1.2" 7 homepage: "https://logarion.orbitalfox.eu" 8 bug-reports: "mailto:logarion@lists.orbitalfox.eu?subject=Issue:" 6 homepage: "http://logarion.orbitalfox.eu" 9 7 depends: [ 10 8 "dune" {>= "2.0"} 11 "re"12 "cmdliner"13 "bos"14 "ptime"15 "uuidm"16 "uri"17 9 "text_parse" 10 "cmdliner" {<= "1.0.4"} 18 11 "msgpck" 19 "cohttp-lwt-unix" 20 "tls" 12 "ocurl" 21 13 ] 22 14 build: [ … … 34 26 ] 35 27 ] 36 dev-repo: "git ://orbitalfox.eu/logarion"28 dev-repo: "git+https://git.disroot.org/orbifx/logarion.git" -
trunk/readme
r2 r3 1 Logarion is a free and open-source text archive system. A blog-wiki hybrid.1 Logarion is a text header-format and suite of tools, for discovering, collecting & exchanging texts. 2 2 3 Download: <https://logarion.orbitalfox.eu/downloads/> 4 EUPL licence: <https://joinup.ec.europa.eu/software/page/eupl> 3 Guide: <http://texts.orbitalfox.eu/11bcd8e9.htm> 4 Source: <http://git.disroot.org/orbifx/logarion> 5 IRC: <irc://tilde.chat/#logarion> 6 EUPL licence: <http://joinup.ec.europa.eu/software/page/eupl> 5 7 6 8 7 Start 9 Header fields 8 10 9 Create a folder and run `logarion init` from within it to produce `.logarion/config` configuration file. 10 Run `logarion --help` for more options. 11 ID: unique identifier 12 Date: of creation, ISO8601 formatted 13 Topics: comma seperated list of topic names & phrases 14 Title: 15 Authors:list of name with optional set of <address> 16 17 A blank line must seperarate the header from the body. 11 18 12 19 13 Community & support 20 Build development version 14 21 15 * Website: <https://logarion.orbitalfox.eu> 16 * Report an issue: <mailto:logarion@lists.orbitalfox.eu?subject=Issue:> 17 * Discussion: <https://lists.orbitalfox.eu/listinfo/logarion> 18 or join via <mailto:logarion-join@lists.orbitalfox.eu> 22 Install `ocaml` and `opam`. Then build and install Logarion using opam's pin function: 19 23 20 21 Install development version 22 23 opam pin add text_parse git://orbitalfox.eu/text-parse-ml 24 opam pin add logarion git://orbitalfox.eu/logarion 25 opam install logarion 24 ``` 25 opam pin add text_parse https://git.disroot.org/orbifx/text-parse-ml.git 26 opam pin add logarion https://git.disroot.org/orbifx/logarion.git 27 opam install logarion 28 ```
Note:
See TracChangeset
for help on using the changeset viewer.