My aggregated monorepo of OCaml code, automaintained
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** Paper rendering for Arod webserver *)
7
8open Htmlit
9open Printf
10
11module MP = Arod_model.Paper
12module MC = Arod_model.Contact
13
14(** Author name with text-wrap:nowrap *)
15let author_name name =
16 El.span ~at:[At.style "text-wrap:nowrap"] [El.txt name]
17
18(** Render one author - link to their best URL if available *)
19let one_author author_name_str =
20 match Arod_model.lookup_by_name author_name_str with
21 | None ->
22 El.span ~at:[At.class' "author"] [author_name author_name_str]
23 | Some contact ->
24 let name = MC.name contact in
25 match MC.best_url contact with
26 | None ->
27 El.span ~at:[At.class' "author"] [author_name name]
28 | Some url ->
29 El.a ~at:[At.href url] [author_name name]
30
31(** Render all authors with proper comma and "and" formatting *)
32let authors p =
33 let author_names = MP.authors p in
34 let author_els = List.map one_author author_names in
35 match author_els with
36 | [] -> El.splice []
37 | [a] -> a
38 | els ->
39 let rec make_list = function
40 | [] -> []
41 | [x] -> [El.txt " and "; x]
42 | x :: xs -> x :: El.txt ", " :: make_list xs
43 in
44 El.splice (make_list els)
45
46(** Generate publication info based on bibtype *)
47let paper_publisher p =
48 let bibty = MP.bibtype p in
49 let ourl l = function
50 | None -> l
51 | Some u -> sprintf {|<a href="%s">%s</a>|} u l
52 in
53 let string_of_vol_issue p =
54 match (MP.volume p), (MP.number p) with
55 | Some v, Some n -> sprintf " (vol %s issue %s)" v n
56 | Some v, None -> sprintf " (vol %s)" v
57 | None, Some n -> sprintf " (issue %s)" n
58 | _ -> ""
59 in
60 let result = match String.lowercase_ascii bibty with
61 | "misc" ->
62 sprintf {|Working paper at %s|} (ourl (MP.publisher p) (MP.url p))
63 | "inproceedings" ->
64 sprintf {|Paper in the %s|} (ourl (MP.booktitle p) (MP.url p))
65 | "proceedings" ->
66 sprintf {|%s|} (ourl (MP.title p) (MP.url p))
67 | "abstract" ->
68 sprintf {|Abstract in the %s|} (ourl (MP.booktitle p) (MP.url p))
69 | "article" | "journal" ->
70 sprintf {|Journal paper in %s%s|} (ourl (MP.journal p) (MP.url p)) (string_of_vol_issue p)
71 | "book" ->
72 sprintf {|Book published by %s|} (ourl (MP.publisher p) (MP.url p))
73 | "techreport" ->
74 sprintf {|Technical report%s at %s|}
75 (match MP.number p with None -> "" | Some n -> " (" ^ n ^ ")")
76 (ourl (MP.institution p) (MP.url p))
77 | _ -> sprintf {|Publication in %s|} (ourl (MP.publisher p) (MP.url p))
78 in
79 El.unsafe_raw result
80
81(** Extract host without www prefix *)
82let host_without_www u =
83 match Uri.host (Uri.of_string u) with
84 | None -> ""
85 | Some h ->
86 if String.starts_with ~prefix:"www." h then
87 String.sub h 4 (String.length h - 4)
88 else h
89
90(** Render the links bar (URL, DOI, BIB, PDF) *)
91let paper_bar_for_feed ?(nopdf=false) p =
92 let cfg = Arod_model.get_config () in
93 let pdf =
94 let pdf_path = Filename.concat cfg.paths.static_dir (sprintf "papers/%s.pdf" (MP.slug p)) in
95 if Sys.file_exists pdf_path && not nopdf then
96 Some (El.a ~at:[At.href (sprintf "/papers/%s.pdf" (MP.slug p))] [
97 El.span ~at:[At.class' "nobreak"] [
98 El.txt "PDF";
99 El.img ~at:[At.class' "inline-icon"; At.alt "pdf"; At.src "/assets/pdf.svg"] ()
100 ]
101 ])
102 else None
103 in
104 let bib =
105 if nopdf then None
106 else Some (El.a ~at:[At.href (sprintf "/papers/%s.bib" (MP.slug p))] [El.txt "BIB"])
107 in
108 let url =
109 match MP.url p with
110 | None -> None
111 | Some u ->
112 Some (El.splice [
113 El.a ~at:[At.href u] [El.txt "URL"];
114 El.txt " ";
115 El.unsafe_raw (sprintf {|<i style="color: #666666">(%s)</i>|} (host_without_www u))
116 ])
117 in
118 let doi =
119 match MP.doi p with
120 | None -> None
121 | Some d ->
122 Some (El.a ~at:[At.href ("https://doi.org/" ^ d)] [El.txt "DOI"])
123 in
124 let bits = [url; doi; bib; pdf] |> List.filter_map Fun.id in
125 El.splice ~sep:(El.unsafe_raw " ") bits
126
127(** Render paper for feed/listing (blockquote style) *)
128let paper_for_feed p =
129 let title_el = El.p ~at:[At.class' "paper-title"] [
130 El.a ~at:[At.href (Arod_model.Entry.site_url (`Paper p))] [El.txt (MP.title p)]
131 ] in
132 (El.blockquote ~at:[At.class' "paper noquote"] [
133 El.div ~at:[At.class' "paper-info"] [
134 title_el;
135 El.p [authors p; El.txt "."];
136 El.p [paper_publisher p; El.txt "."];
137 El.p [paper_bar_for_feed p]
138 ]
139 ], None)
140
141(** Render paper for entry listing *)
142let paper_for_entry ?nopdf p =
143 (El.div ~at:[At.class' "paper"] [
144 El.div ~at:[At.class' "paper-info"] [
145 El.p ~at:[At.class' "paper-title"] [
146 El.a ~at:[At.href (Arod_model.Entry.site_url (`Paper p))] [El.txt (MP.title p)]
147 ];
148 El.p [authors p; El.txt "."];
149 El.p [paper_publisher p; El.txt "."];
150 El.p [paper_bar_for_feed ?nopdf p]
151 ]
152 ], None)
153
154(** Render older versions section for a paper *)
155let one_paper_extra p =
156 let entries = Arod_model.get_entries () in
157 let all = Arod_model.Entry.old_papers entries
158 |> List.filter (fun op -> MP.slug op = MP.slug p)
159 in
160 match all with
161 | [] -> El.splice []
162 | all ->
163 let older_versions = List.map (fun op ->
164 let (paper_html, _) = paper_for_entry ~nopdf:true op in
165 El.splice [
166 El.hr ();
167 El.p [
168 El.txt ("This is " ^ op.Arod_model.Paper.ver ^ " of the publication from " ^
169 Arod_view.ptime_date ~with_d:false (MP.date op) ^ ".")
170 ];
171 El.blockquote ~at:[At.class' "noquote"] [
172 paper_html
173 ];
174 Arod_view.tags_meta (`Paper op)
175 ]
176 ) all in
177 El.splice [
178 El.h1 [El.txt "Older versions"];
179 El.p [
180 El.txt "There are earlier revisions of this paper available below for historical reasons. ";
181 El.txt "Please cite the latest version of the paper above instead of these."
182 ];
183 El.splice older_versions
184 ]
185
186(** Render full paper page *)
187let one_paper_full p =
188 let img_el =
189 match Arod_model.lookup_image (MP.slug p) with
190 | Some img ->
191 El.p [
192 El.a ~at:[At.href (Option.value ~default:"#" (MP.best_url p))] [
193 Arod_view.img ~cl:"image-center" img
194 ]
195 ]
196 | None -> El.splice []
197 in
198 let abstract_html =
199 let abstract = MP.abstract p in
200 if abstract <> "" then
201 El.p [El.unsafe_raw (Arod_view.md_to_html abstract)]
202 else
203 El.splice []
204 in
205 El.div ~at:[At.class' "paper"] [
206 El.div ~at:[At.class' "paper-info"] [
207 El.h2 [El.txt (MP.title p)];
208 El.p [authors p; El.txt "."];
209 El.p [paper_publisher p; El.txt "."];
210 El.p [paper_bar_for_feed p]
211 ];
212 img_el;
213 abstract_html
214 ]