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
|
---|