[1] | 1 | module Date = struct
|
---|
| 2 | type t = {
|
---|
| 3 | created: Ptime.t option;
|
---|
| 4 | published: Ptime.t option;
|
---|
| 5 | edited: Ptime.t option;
|
---|
| 6 | } [@@deriving lens { submodule = true }]
|
---|
| 7 |
|
---|
| 8 | let rfc_string date = match date with Some t -> Ptime.to_rfc3339 t | None -> ""
|
---|
| 9 |
|
---|
| 10 | let of_string (rfc : string) = match Ptime.of_rfc3339 rfc with
|
---|
| 11 | Ok (t,_,_) -> Some t | Error _ -> None
|
---|
| 12 |
|
---|
| 13 | let listing date = match date.published, date.created with
|
---|
| 14 | | Some _, _ -> date.published
|
---|
| 15 | | None, Some _ -> date.created
|
---|
| 16 | | None, None -> None
|
---|
| 17 |
|
---|
| 18 | let compare = compare
|
---|
| 19 |
|
---|
| 20 | let pretty_date = function
|
---|
| 21 | | Some t -> Ptime.to_date t |> fun (y, m, d) -> Printf.sprintf "%04d-%02d-%02d" y m d
|
---|
| 22 | | None -> ""
|
---|
| 23 | end
|
---|
| 24 |
|
---|
| 25 | module Id = struct
|
---|
| 26 | let random_state = Random.State.make_self_init ()
|
---|
| 27 | type t = Uuidm.t
|
---|
| 28 | let compare = Uuidm.compare
|
---|
| 29 | let to_string = Uuidm.to_string
|
---|
| 30 | let of_string = Uuidm.of_string
|
---|
| 31 | let generate ?(random_state=random_state) = Uuidm.v4_gen random_state
|
---|
| 32 | end
|
---|
| 33 |
|
---|
| 34 | module Author = struct
|
---|
| 35 | type name_t = string
|
---|
| 36 | type address_t = Uri.t
|
---|
| 37 | type t = {
|
---|
| 38 | name: name_t;
|
---|
| 39 | address: address_t;
|
---|
| 40 | } [@@deriving lens { submodule = true } ]
|
---|
| 41 |
|
---|
| 42 | let empty = { name = ""; address = Uri.empty }
|
---|
| 43 |
|
---|
| 44 | let compare = Pervasives.compare
|
---|
| 45 |
|
---|
| 46 | end
|
---|
| 47 |
|
---|
| 48 | module AuthorSet = struct
|
---|
| 49 | include Set.Make(Author)
|
---|
| 50 |
|
---|
| 51 | let to_string authors =
|
---|
| 52 | let to_string a = a.Author.name ^ " <" ^ Uri.to_string a.Author.address ^ ">" in
|
---|
| 53 | let f elt acc = if String.length acc > 1 then acc ^ ", " ^ to_string elt else to_string elt in
|
---|
| 54 | fold f authors ""
|
---|
| 55 |
|
---|
| 56 | let of_string s =
|
---|
| 57 | match Emile.List.of_string s with
|
---|
| 58 | | Error _ -> prerr_endline @@ "Error parsing: " ^ s; empty
|
---|
| 59 | | Ok emails ->
|
---|
| 60 | let to_author =
|
---|
| 61 | let module L = List in
|
---|
| 62 | let open Emile in
|
---|
| 63 | function
|
---|
| 64 | | `Group _ -> prerr_endline @@ "Can't deal with groups in author: " ^ s; Author.empty
|
---|
| 65 | | `Mailbox { name; local; _ } ->
|
---|
| 66 | let s_of_phrase = function `Dot -> "" | `Word w -> (match w with `Atom a -> a | `String s -> s) | `Encoded _ -> "" in
|
---|
| 67 | let name = match name with None -> "" | Some phrase -> L.fold_left (fun a e -> a ^ s_of_phrase e) "" phrase in
|
---|
| 68 | let address =
|
---|
| 69 | L.fold_left (fun a e -> a ^ match e with `Atom a -> a | `String s -> s) "" local ^ "@" (* TODO: Author address unimplemented *)
|
---|
| 70 | in
|
---|
| 71 | Author.{ name; address = Uri.of_string address }
|
---|
| 72 | in
|
---|
| 73 | of_list @@ List.map to_author emails
|
---|
| 74 | end
|
---|
| 75 |
|
---|
| 76 | module Category = struct
|
---|
| 77 | type t = Draft | Unlisted | Published | Custom of string
|
---|
| 78 |
|
---|
| 79 | let compare = Pervasives.compare
|
---|
| 80 |
|
---|
| 81 | let of_string = function
|
---|
| 82 | | "draft" -> Draft
|
---|
| 83 | | "unlisted" -> Unlisted
|
---|
| 84 | | "published" -> Published
|
---|
| 85 | | c -> Custom c
|
---|
| 86 |
|
---|
| 87 | let to_string = function
|
---|
| 88 | | Draft -> "draft"
|
---|
| 89 | | Unlisted -> "unlisted"
|
---|
| 90 | | Published -> "published"
|
---|
| 91 | | Custom c -> c
|
---|
| 92 | end
|
---|
| 93 |
|
---|
| 94 | module CategorySet = struct
|
---|
| 95 | include Set.Make(Category)
|
---|
| 96 | let to_csv set =
|
---|
| 97 | let f elt a =
|
---|
| 98 | let s = Category.to_string elt in
|
---|
| 99 | if a <> "" then a ^ ", " ^ s else s
|
---|
| 100 | in
|
---|
| 101 | fold f set ""
|
---|
| 102 | let categorised categs cs = of_list categs |> (fun s -> subset s cs)
|
---|
| 103 | let published = categorised [Category.Published]
|
---|
| 104 | let listed cs = not @@ categorised [Category.Unlisted] cs
|
---|
| 105 | end
|
---|
| 106 |
|
---|
| 107 | module StringSet = Set.Make(String)
|
---|
| 108 |
|
---|
| 109 | let stringset_csv set =
|
---|
| 110 | let f elt a = if a <> "" then a ^ ", " ^ elt else elt in
|
---|
| 111 | StringSet.fold f set ""
|
---|
| 112 |
|
---|
| 113 | let string_alias t =
|
---|
| 114 | let is_reserved = function
|
---|
| 115 | | '!' | '*' | '\'' | '(' | ')' | ';' | ':' | '@' | '&' | '=' | '+' | '$'
|
---|
| 116 | | ',' | '/' | '?' | '#' | '[' | ']' | ' ' | '\t' | '\x00' -> true
|
---|
| 117 | | _ -> false
|
---|
| 118 | in
|
---|
| 119 | let b = Buffer.create (String.length t) in
|
---|
| 120 | let filter char =
|
---|
| 121 | let open Buffer in
|
---|
| 122 | if is_reserved char then (try (if nth b (pred (length b)) <> '-' then add_char b '-') with Invalid_argument _ -> prerr_endline "reserved")
|
---|
| 123 | else add_char b char
|
---|
| 124 | in
|
---|
| 125 | String.(iter filter (lowercase_ascii t));
|
---|
| 126 | Buffer.contents b
|
---|
| 127 |
|
---|
| 128 | type t = {
|
---|
| 129 | title: string;
|
---|
| 130 | authors: AuthorSet.t;
|
---|
| 131 | date: Date.t;
|
---|
| 132 | categories: CategorySet.t;
|
---|
| 133 | topics: StringSet.t;
|
---|
| 134 | keywords: StringSet.t;
|
---|
| 135 | series: StringSet.t;
|
---|
| 136 | abstract: string;
|
---|
| 137 | uuid: Id.t;
|
---|
| 138 | alias: string;
|
---|
| 139 | } [@@deriving lens { submodule = true }]
|
---|
| 140 |
|
---|
| 141 | let blank ?(uuid=(Id.generate ())) () = {
|
---|
| 142 | title = "";
|
---|
| 143 | authors = AuthorSet.empty;
|
---|
| 144 | date = Date.({ created = None; edited = None; published = None });
|
---|
| 145 | categories = CategorySet.empty;
|
---|
| 146 | topics = StringSet.empty;
|
---|
| 147 | keywords = StringSet.empty;
|
---|
| 148 | series = StringSet.empty;
|
---|
| 149 | abstract = "";
|
---|
| 150 | uuid;
|
---|
| 151 | alias = "";
|
---|
| 152 | }
|
---|
| 153 |
|
---|
| 154 | let listed e = CategorySet.listed e.categories
|
---|
| 155 | let published e = CategorySet.published e.categories
|
---|
| 156 | let unique_topics ts x = StringSet.union ts x.topics
|
---|
| 157 |
|
---|
| 158 | module AliasMap = Map.Make(String)
|
---|
| 159 | module IdMap = Map.Make(Id)
|
---|
| 160 |
|
---|
| 161 | let alias meta = if meta.alias = "" then string_alias meta.title else meta.alias
|
---|
| 162 |
|
---|
| 163 | let value_with_name (_meta as m) = function
|
---|
| 164 | | "Title" -> m.title
|
---|
| 165 | | "Abstract" -> m.abstract
|
---|
| 166 | | "Authors" -> AuthorSet.to_string m.authors
|
---|
| 167 | | "Date" -> Date.(rfc_string m.date.created)
|
---|
| 168 | | "Edited" -> Date.(rfc_string m.date.edited)
|
---|
| 169 | | "Published"-> Date.(rfc_string m.date.published)
|
---|
| 170 | | "Human" -> Date.(pretty_date @@ listing m.date)
|
---|
| 171 | | "Topics" -> stringset_csv m.topics;
|
---|
| 172 | | "Categories" -> CategorySet.to_csv m.categories;
|
---|
| 173 | | "Keywords" -> stringset_csv m.keywords;
|
---|
| 174 | | "Series" -> stringset_csv m.series;
|
---|
| 175 | | "ID" -> Id.to_string m.uuid
|
---|
| 176 | | "Alias" -> alias m
|
---|
| 177 | | e -> invalid_arg e
|
---|
| 178 |
|
---|
| 179 | let with_kv meta (k,v) =
|
---|
| 180 | let list_of_csv = Re.Str.(split (regexp " *, *")) in
|
---|
| 181 | let trim = String.trim in
|
---|
| 182 | match k with
|
---|
| 183 | | "Title" -> { meta with title = trim v }
|
---|
| 184 | | "Author"
|
---|
| 185 | | "Authors" -> { meta with authors = AuthorSet.of_string (trim v)}
|
---|
| 186 | | "Abstract" -> { meta with abstract = trim v }
|
---|
| 187 | | "Date" -> { meta with date = Date.{ meta.date with created = Date.of_string v }}
|
---|
| 188 | | "Published" -> { meta with date = Date.{ meta.date with published = Date.of_string v }}
|
---|
| 189 | | "Edited" -> { meta with date = Date.{ meta.date with edited = Date.of_string v }}
|
---|
| 190 | | "Topics" -> { meta with topics = trim v |> list_of_csv |> StringSet.of_list }
|
---|
| 191 | | "Keywords" -> { meta with keywords = trim v |> list_of_csv |> StringSet.of_list }
|
---|
| 192 | | "Categories"->
|
---|
| 193 | let categories = trim v |> list_of_csv |> List.map Category.of_string |> CategorySet.of_list in
|
---|
| 194 | { meta with categories }
|
---|
| 195 | | "Series" -> { meta with series = trim v |> list_of_csv |> StringSet.of_list }
|
---|
| 196 | | "ID" -> (match Id.of_string v with Some id -> { meta with uuid = id } | None -> meta)
|
---|
| 197 | | "Alias" -> { meta with alias = v }
|
---|
| 198 | | k -> prerr_endline ("Unknown key: " ^ k ^ ", with value: " ^ v ); meta
|
---|
| 199 |
|
---|
| 200 | let to_string (_meta as m) =
|
---|
| 201 | let has_len v = String.length v > 0 in
|
---|
| 202 | let s field value = if has_len value then field ^ ": " ^ value ^ "\n" else "" in
|
---|
| 203 | let a value = if AuthorSet.is_empty value then "" else "Authors: " ^ AuthorSet.to_string value ^ "\n" in
|
---|
| 204 | let d field value = match value with
|
---|
| 205 | | Some _ -> field ^ ": " ^ Date.rfc_string value ^ "\n" | None -> ""
|
---|
| 206 | in
|
---|
| 207 | let rows =
|
---|
| 208 | [ s "Title" m.title;
|
---|
| 209 | a m.authors;
|
---|
| 210 | d "Date" m.date.Date.created;
|
---|
| 211 | d "Edited" m.date.Date.edited;
|
---|
| 212 | d "Published" m.date.Date.published;
|
---|
| 213 | s "Topics" (stringset_csv m.topics);
|
---|
| 214 | s "Categories" (CategorySet.to_csv m.categories);
|
---|
| 215 | s "Keywords" (stringset_csv m.keywords);
|
---|
| 216 | s "Series" (stringset_csv m.series);
|
---|
| 217 | s "Abstract" m.abstract;
|
---|
| 218 | s "ID" (Uuidm.to_string m.uuid);
|
---|
| 219 | s "Alias" m.alias
|
---|
| 220 | ]
|
---|
| 221 | in
|
---|
| 222 | String.concat "" rows
|
---|