logarion/src/core/meta.ml

223 lines
7.4 KiB
OCaml

module Date = struct
type t = {
created: Ptime.t option;
published: Ptime.t option;
edited: Ptime.t option;
} [@@deriving lens { submodule = true }]
let rfc_string date = match date with Some t -> Ptime.to_rfc3339 t | None -> ""
let of_string (rfc : string) = match Ptime.of_rfc3339 rfc with
Ok (t,_,_) -> Some t | Error _ -> None
let listing date = match date.published, date.created with
| Some _, _ -> date.published
| None, Some _ -> date.created
| None, None -> None
let compare = compare
let pretty_date = function
| Some t -> Ptime.to_date t |> fun (y, m, d) -> Printf.sprintf "%04d-%02d-%02d" y m d
| None -> ""
end
module Id = struct
let random_state = Random.State.make_self_init ()
type t = Uuidm.t
let compare = Uuidm.compare
let to_string = Uuidm.to_string
let of_string = Uuidm.of_string
let generate ?(random_state=random_state) = Uuidm.v4_gen random_state
end
module Author = struct
type name_t = string
type address_t = Uri.t
type t = {
name: name_t;
address: address_t;
} [@@deriving lens { submodule = true } ]
let empty = { name = ""; address = Uri.empty }
let compare = Pervasives.compare
end
module AuthorSet = struct
include Set.Make(Author)
let to_string authors =
let to_string a = a.Author.name ^ " <" ^ Uri.to_string a.Author.address ^ ">" in
let f elt acc = if String.length acc > 1 then acc ^ ", " ^ to_string elt else to_string elt in
fold f authors ""
let of_string s =
match Emile.List.of_string s with
| Error _ -> prerr_endline @@ "Error parsing: " ^ s; empty
| Ok emails ->
let to_author =
let module L = List in
let open Emile in
function
| `Group _ -> prerr_endline @@ "Can't deal with groups in author: " ^ s; Author.empty
| `Mailbox { name; local; _ } ->
let s_of_phrase = function `Dot -> "" | `Word w -> (match w with `Atom a -> a | `String s -> s) | `Encoded _ -> "" in
let name = match name with None -> "" | Some phrase -> L.fold_left (fun a e -> a ^ s_of_phrase e) "" phrase in
let address =
L.fold_left (fun a e -> a ^ match e with `Atom a -> a | `String s -> s) "" local ^ "@" (* TODO: Author address unimplemented *)
in
Author.{ name; address = Uri.of_string address }
in
of_list @@ List.map to_author emails
end
module Category = struct
type t = Draft | Unlisted | Published | Custom of string
let compare = Pervasives.compare
let of_string = function
| "draft" -> Draft
| "unlisted" -> Unlisted
| "published" -> Published
| c -> Custom c
let to_string = function
| Draft -> "draft"
| Unlisted -> "unlisted"
| Published -> "published"
| Custom c -> c
end
module CategorySet = struct
include Set.Make(Category)
let to_csv set =
let f elt a =
let s = Category.to_string elt in
if a <> "" then a ^ ", " ^ s else s
in
fold f set ""
let categorised categs cs = of_list categs |> (fun s -> subset s cs)
let published = categorised [Category.Published]
let listed cs = not @@ categorised [Category.Unlisted] cs
end
module StringSet = Set.Make(String)
let stringset_csv set =
let f elt a = if a <> "" then a ^ ", " ^ elt else elt in
StringSet.fold f set ""
let string_alias t =
let is_reserved = function
| '!' | '*' | '\'' | '(' | ')' | ';' | ':' | '@' | '&' | '=' | '+' | '$'
| ',' | '/' | '?' | '#' | '[' | ']' | ' ' | '\t' | '\x00' -> true
| _ -> false
in
let b = Buffer.create (String.length t) in
let filter char =
let open Buffer in
if is_reserved char then (try (if nth b (pred (length b)) <> '-' then add_char b '-') with Invalid_argument _ -> prerr_endline "reserved")
else add_char b char
in
String.(iter filter (lowercase_ascii t));
Buffer.contents b
type t = {
title: string;
authors: AuthorSet.t;
date: Date.t;
categories: CategorySet.t;
topics: StringSet.t;
keywords: StringSet.t;
series: StringSet.t;
abstract: string;
uuid: Id.t;
alias: string;
} [@@deriving lens { submodule = true }]
let blank ?(uuid=(Id.generate ())) () = {
title = "";
authors = AuthorSet.empty;
date = Date.({ created = None; edited = None; published = None });
categories = CategorySet.empty;
topics = StringSet.empty;
keywords = StringSet.empty;
series = StringSet.empty;
abstract = "";
uuid;
alias = "";
}
let listed e = CategorySet.listed e.categories
let published e = CategorySet.published e.categories
let unique_topics ts x = StringSet.union ts x.topics
module AliasMap = Map.Make(String)
module IdMap = Map.Make(Id)
let alias meta = if meta.alias = "" then string_alias meta.title else meta.alias
let value_with_name (_meta as m) = function
| "Title" -> m.title
| "Abstract" -> m.abstract
| "Authors" -> AuthorSet.to_string m.authors
| "Date" -> Date.(rfc_string m.date.created)
| "Edited" -> Date.(rfc_string m.date.edited)
| "Published"-> Date.(rfc_string m.date.published)
| "Human" -> Date.(pretty_date @@ listing m.date)
| "Topics" -> stringset_csv m.topics;
| "Categories" -> CategorySet.to_csv m.categories;
| "Keywords" -> stringset_csv m.keywords;
| "Series" -> stringset_csv m.series;
| "ID" -> Id.to_string m.uuid
| "Alias" -> alias m
| e -> invalid_arg e
let with_kv meta (k,v) =
let list_of_csv = Re.Str.(split (regexp " *, *")) in
let trim = String.trim in
match k with
| "Title" -> { meta with title = trim v }
| "Author"
| "Authors" -> { meta with authors = AuthorSet.of_string (trim v)}
| "Abstract" -> { meta with abstract = trim v }
| "Date" -> { meta with date = Date.{ meta.date with created = Date.of_string v }}
| "Published" -> { meta with date = Date.{ meta.date with published = Date.of_string v }}
| "Edited" -> { meta with date = Date.{ meta.date with edited = Date.of_string v }}
| "Topics" -> { meta with topics = trim v |> list_of_csv |> StringSet.of_list }
| "Keywords" -> { meta with keywords = trim v |> list_of_csv |> StringSet.of_list }
| "Categories"->
let categories = trim v |> list_of_csv |> List.map Category.of_string |> CategorySet.of_list in
{ meta with categories }
| "Series" -> { meta with series = trim v |> list_of_csv |> StringSet.of_list }
| "ID" -> (match Id.of_string v with Some id -> { meta with uuid = id } | None -> meta)
| "Alias" -> { meta with alias = v }
| k -> prerr_endline ("Unknown key: " ^ k ^ ", with value: " ^ v ); meta
let to_string (_meta as m) =
let has_len v = String.length v > 0 in
let s field value = if has_len value then field ^ ": " ^ value ^ "\n" else "" in
let a value = if AuthorSet.is_empty value then "" else "Authors: " ^ AuthorSet.to_string value ^ "\n" in
let d field value = match value with
| Some _ -> field ^ ": " ^ Date.rfc_string value ^ "\n" | None -> ""
in
let rows =
[ s "Title" m.title;
a m.authors;
d "Date" m.date.Date.created;
d "Edited" m.date.Date.edited;
d "Published" m.date.Date.published;
s "Topics" (stringset_csv m.topics);
s "Categories" (CategorySet.to_csv m.categories);
s "Keywords" (stringset_csv m.keywords);
s "Series" (stringset_csv m.series);
s "Abstract" m.abstract;
s "ID" (Uuidm.to_string m.uuid);
s "Alias" m.alias
]
in
String.concat "" rows