223 lines
7.4 KiB
OCaml
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
|