rfc4287.ml 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Web.
  10. *
  11. * Copyright (C) The #Seppo contributors. All rights reserved.
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *)
  26. let ( >>= ) = Result.bind
  27. let ( let* ) = Result.bind
  28. let defa = Uri.make ~path:"o/p/" ()
  29. let tagu = Uri.make ~path:"o/t/" ()
  30. (** map until the first Error *)
  31. let list f xs =
  32. let it xs x =
  33. let* xs = xs in
  34. let* x = f x in
  35. Ok (List.cons x xs)
  36. in
  37. xs |> List.fold_left it (Ok [])
  38. type single = Single of string
  39. type multi = Multi of string
  40. type rfc4646 = Rfc4646 of string (* bcp47 *)
  41. (* See also Cohttp.Link.Rel *)
  42. module Link = struct
  43. type rel = Rel of single
  44. type t = {
  45. href : Uri.t;
  46. rel : rel option;
  47. title : string option;
  48. rfc7033 : string option; (* Webfinger.Client.t is a cycle *)
  49. }
  50. let self = Rel (Single "self")
  51. let last = Rel (Single "last")
  52. let first = Rel (Single "first")
  53. let next = Rel (Single "next")
  54. let prev = Rel (Single "previous")
  55. let edit = Rel (Single "edit")
  56. let notify= Rel (Single "notify") (* idiosyncratic *)
  57. let subscribed= Rel (Single "subscribed") (* idiosyncratic *)
  58. let make ?(title = None) ?(rfc7033 = None) ?(rel = None) href = { href; rel; title; rfc7033; }
  59. let encode (r : t) =
  60. let open Csexp in
  61. let str n v l = Atom n :: Atom v :: l in
  62. let uri n v l = l |> str n (v |> Uri.to_string) in
  63. let opt f n v l = match v with | None -> l
  64. | Some u -> l |> f n u in
  65. let sing f n v l = match v with | None -> l
  66. | Some (Rel (Single u)) -> l |> f n u in
  67. List ([]
  68. |> sing str "rel" r.rel
  69. |> opt str "title" r.title
  70. |> opt str "rfc7033" r.rfc7033
  71. |> uri "href" r.href)
  72. let decode s =
  73. let open Csexp in
  74. let rec pairs xs r =
  75. match xs with
  76. | Atom "rel" :: Atom x :: tl -> pairs tl {r with rel=Some (Rel (Single x))}
  77. | Atom "title" :: Atom x :: tl -> pairs tl {r with title=Some x}
  78. | Atom "rfc7033":: Atom x :: tl -> pairs tl {r with rfc7033=Some x}
  79. | [] -> Ok r
  80. | _ -> Error "unexpected field"
  81. in
  82. match s with
  83. | List ( Atom "href" :: Atom href :: tl ) ->
  84. href
  85. |> Uri.of_string
  86. |> make
  87. |> pairs tl
  88. | _ -> Error "unexpected field"
  89. let to_atom ?(base = Uri.empty) li =
  90. let href = match li.href |> Http.reso ~base |> Uri.to_string with
  91. | "" -> "."
  92. | s -> s in
  93. let attr = [] in
  94. let attr = match li.rfc7033 with
  95. | None -> attr
  96. | Some x -> (("","rfc7033"), x) :: attr in
  97. let attr = match li.title with
  98. | None -> attr
  99. | Some x -> (("","title"), x) :: attr in
  100. let attr = (("","href"), href) :: attr in
  101. let attr = match li.rel with
  102. | None -> attr
  103. | Some Rel Single x -> (("","rel"), x) :: attr in
  104. `El (((Xml.ns_a,"link"),attr),[])
  105. let link ~rfc7033 ~title ~rel ~href : t =
  106. let rel = Some rel in
  107. make ~rfc7033 ~title ~rel href
  108. end
  109. module Inreplyto = struct
  110. type t = {
  111. ref_ : Uri.t;
  112. href : Uri.t option;
  113. source : Uri.t option;
  114. type_ : string option;
  115. }
  116. let make ?(href = None) ?(source = None) ?(type_ = None) ref_ = { ref_; href; source; type_; }
  117. let encode (r : t) =
  118. let open Csexp in
  119. let str n v l = Atom n :: Atom v :: l in
  120. let uri n v l = l |> str n (v |> Uri.to_string) in
  121. let opt f n v l = match v with | None -> l
  122. | Some u -> l |> f n u in
  123. List ([]
  124. |> opt str "type" r.type_
  125. |> opt uri "source" r.source
  126. |> opt uri "href" r.href
  127. |> uri "ref" r.ref_)
  128. let decode s =
  129. let open Csexp in
  130. let rec pairs xs r =
  131. match xs with
  132. | Atom "href" :: Atom x :: tl -> pairs tl {r with href=Some (Uri.of_string x)}
  133. | Atom "source":: Atom x :: tl -> pairs tl {r with source=Some (Uri.of_string x)}
  134. | Atom "type" :: Atom x :: tl -> pairs tl {r with type_=Some x}
  135. | [] -> Ok r
  136. | _ -> Error "unexpected field"
  137. in
  138. match s with
  139. | List ( Atom "ref" :: Atom ref_ :: tl ) ->
  140. ref_
  141. |> Uri.of_string
  142. |> make
  143. |> pairs tl
  144. | _ -> Error "unexpected field"
  145. end
  146. module Category = struct
  147. type label = Label of single
  148. type term = Term of single
  149. type t = label * term * Uri.t
  150. let encode (Label (Single l), Term (Single t), u) =
  151. Csexp.(List [
  152. Atom "label"; Atom l;
  153. Atom "term"; Atom t;
  154. Atom "scheme"; Atom (u |> Uri.to_string)
  155. ])
  156. let decode s =
  157. match s with
  158. | Csexp.(List [
  159. Atom "label"; Atom l;
  160. Atom "term"; Atom t;
  161. Atom "scheme"; Atom u
  162. ]) -> Ok (Label (Single l), Term (Single t), u |> Uri.of_string)
  163. | _ -> Error "expected category but found none"
  164. let to_atom ?(base = Uri.empty) (Label (Single lbl),Term (Single trm),sch) =
  165. let sch = sch |> Http.reso ~base |> Uri.to_string in
  166. `El (((Xml.ns_a,"category"),[
  167. (("","label"),lbl);
  168. (("","term"),trm);
  169. (("","scheme"),sch);
  170. ]),[])
  171. end
  172. type id = Id of string
  173. (* Being "super-careful" https://code.mro.name/mro/ProgrammableWebSwartz2013/src/master/content/pages/2-building-for-users.md
  174. *
  175. * geohash uses a base 32 https://codeberg.org/mro/geohash/src/commit/ed8e71a03e377b472054a3468979a1cd77fc090d/lib/geohash.ml#L73
  176. *
  177. * See also https://opam.ocaml.org/packages/base32/ for int (we need more bits)
  178. *)
  179. module Base24 = struct
  180. open Optint.Int63
  181. let alphabet = Bytes.of_string "23456789abcdefghkrstuxyz"
  182. let base = 24 |> of_int
  183. (* encode the n right chars of x *)
  184. let encode chars x =
  185. let int_to_char i = i |> Bytes.get alphabet in
  186. let rec f i x' b =
  187. match i with
  188. | -1 -> b
  189. | _ ->
  190. rem x' base |> to_int |> int_to_char |> Bytes.set b i;
  191. f (i - 1) (div x' base) b
  192. in
  193. chars |> Bytes.create |> f (chars - 1) x |> Bytes.to_string
  194. let decode hash =
  195. let int_of_char c =
  196. (* if we want it fast, either do binary search or construct a sparse LUT from chars 0-z -> int *)
  197. match c |> Bytes.index_opt alphabet with
  198. | None -> Error c
  199. | Some i -> Ok i
  200. and len = hash |> String.length in
  201. match len <= 7 with
  202. | false -> Error '_'
  203. | true ->
  204. let rec f idx x =
  205. match len - idx with
  206. | 0 -> Ok x
  207. | _ ->
  208. let* v = hash.[idx] |> int_of_char in
  209. v |> of_int
  210. |> add (mul x base)
  211. |> f (idx + 1)
  212. in
  213. f 0 zero
  214. end
  215. let mk_auth ~base a =
  216. let host = Uri.host base |> Option.value ~default:"-" in
  217. let userinfo = Uri.user a |> Option.value ~default:"-" in
  218. let s = Uri.make ~host ~userinfo () |> Uri.to_string in
  219. let le = s |> String.length in
  220. "@" ^ String.sub s 2 (le-2)
  221. (* RFC7565 *)
  222. let mk_auth_acct ~base a =
  223. let host = Uri.host base |> Option.value ~default:"-" in
  224. let userinfo = Uri.user a |> Option.value ~default:"-" in
  225. let s = Uri.make ~host ~userinfo () |> Uri.to_string in
  226. let le = s |> String.length in
  227. "acct:" ^ String.sub s 2 (le-2)
  228. open Xml
  229. let sep n = `Data ("\n" ^ String.make (2*n) ' ')
  230. let xsl fn_xsl fn_xml =
  231. let x = fn_xml
  232. |> String.split_on_char '/'
  233. |> List.tl
  234. |> List.fold_left (fun a _ -> ".." :: a) ["themes";"current";fn_xsl]
  235. |> String.concat "/" in
  236. Some x
  237. module Entry = struct
  238. type t = {
  239. id : Uri.t;
  240. in_reply_to: Inreplyto.t list;
  241. (* assumes an antry has one language for title, tags, content. *)
  242. lang : rfc4646;
  243. author : Uri.t;
  244. title : string;
  245. published : Rfc3339.t;
  246. updated : Rfc3339.t;
  247. links : Link.t list;
  248. categories : Category.t list;
  249. content : string;
  250. }
  251. (** inspired by https://code.mro.name/mro/ShaarliGo/src/cb798ebfae17431732e37a94ee80b29bd3b78911/atom.go#L302 *)
  252. let id_make t =
  253. let secs_since_epoch t : Optint.Int63.t =
  254. let (d',ps') = Ptime.epoch |> Ptime.diff t |> Ptime.Span.to_d_ps in
  255. let open Optint.Int63 in
  256. let ( +. ) = add
  257. and ( *. ) = mul
  258. and s = Int64.div ps' 1_000_000_000_000L |> of_int64
  259. and day_s = 24 * 60 * 60 |> of_int
  260. and d' = d' |> of_int in
  261. d' *. day_s +. s
  262. in
  263. let path = t |> secs_since_epoch |> Base24.encode 7 in
  264. Logr.debug (fun m -> m "id_make %s" path);
  265. Uri.make ~path ()
  266. let compare_published_desc a b =
  267. let Rfc3339.T a' = a.published
  268. and Rfc3339.T b' = b.published in
  269. let r = String.compare b' a' in
  270. Logr.debug (fun m -> m "%s.%s %s %s = %d" "Rfc4287" "compare_published_desc" b' a' r);
  271. r
  272. let encode e =
  273. let Rfc4646 lang = e.lang
  274. and Rfc3339.T published = e.published
  275. and Rfc3339.T updated = e.updated
  276. in
  277. Csexp.(
  278. List [
  279. Atom "id"; Atom (e.id |> Uri.to_string);
  280. Atom "in-reply-to";List (e.in_reply_to |> List.map Inreplyto.encode);
  281. Atom "lang"; Atom lang;
  282. Atom "title"; Atom e.title;
  283. Atom "author"; Atom (e.author |> Uri.to_string);
  284. Atom "published"; Atom published;
  285. Atom "updated"; Atom updated;
  286. Atom "links"; List (e.links |> List.map Link.encode);
  287. Atom "categories"; List (e.categories |> List.map Category.encode);
  288. Atom "content"; Atom e.content;
  289. ] )
  290. (* I am unsure if similar to https://opam.ocaml.org/packages/decoders-sexplib/
  291. * could help.
  292. *)
  293. let decode s =
  294. match s with
  295. | Csexp.(List [Atom _; Atom _]) -> Error "deleted"
  296. | Csexp.(List [
  297. Atom "id"; Atom id;
  298. Atom "in-reply-to";List in_reply_to;
  299. Atom "lang"; Atom lang;
  300. Atom "title"; Atom title;
  301. Atom "author"; Atom author;
  302. Atom "published"; Atom published;
  303. Atom "updated"; Atom updated;
  304. Atom "links"; List links;
  305. Atom "categories"; List categories;
  306. Atom "content"; Atom content;
  307. ]) ->
  308. let id = id |> Uri.of_string
  309. and lang = Rfc4646 lang
  310. and author = author |> Uri.of_string
  311. and published = Rfc3339.T published
  312. and updated = Rfc3339.T updated in
  313. let* in_reply_to = in_reply_to|> list Inreplyto.decode in
  314. let* links = links |> list Link.decode in
  315. let* categories = categories |> list Category.decode in
  316. Ok { id; in_reply_to; lang; author; title; published; updated; links; categories; content }
  317. | _ -> Error ("can't decode '" ^ (Csexp.to_string s) ^ "'")
  318. let decode_channel ic =
  319. let* lst = ic |> Csexp.input_many in
  320. let* lst = lst |> list decode in
  321. Ok lst
  322. let one_from_channel ic =
  323. let* r = ic |> Csexp.input in
  324. r |> decode
  325. let from_text_plain ~published ~author ~lang ~uri title content =
  326. Logr.debug (fun m -> m "new note %s\n%s" title content);
  327. let in_reply_to = [] in
  328. let links = [] in
  329. let categories = []
  330. and links = (if uri |> Uri.host |> Option.is_none
  331. then links
  332. else (uri |> Link.make) :: links)
  333. and updated = published in
  334. let* t = published |> Rfc3339.to_ptime in
  335. let id = t |> id_make in
  336. (*
  337. * - add attributedTo, id
  338. * - extract microformats (tags, mentions)
  339. * - via and thanks -> link via
  340. * - emojis -> tags
  341. *)
  342. Ok { id; in_reply_to; lang; author; published; updated; links; title; categories; content }
  343. let from_channel ?(published = Ptime_clock.now ()) ?(author = Uri.make ()) ~lang ~tz ic =
  344. Logr.debug (fun m -> m "Rfc4287.from_channel");
  345. let l1 = input_line ic
  346. and buf = Buffer.create 512
  347. and published = published |> Rfc3339.of_ptime ~tz in
  348. let uri = l1 |> Uri.of_string in
  349. let l1,uri = (if uri |> Uri.host |> Option.is_none
  350. then (l1, Uri.empty)
  351. else
  352. let l1 = try
  353. input_line ic
  354. with End_of_file -> "" in
  355. (l1,uri) ) in
  356. (try
  357. while true do
  358. ic
  359. |> input_line
  360. |> Buffer.add_string buf;
  361. Buffer.add_char buf '\n'
  362. done
  363. with End_of_file -> ());
  364. buf
  365. |> Buffer.contents
  366. |> from_text_plain ~published ~author ~lang ~uri l1
  367. let save _ =
  368. (*
  369. * - apend to storage csexp (tag feed
  370. * - update indices (id & url cdbs)
  371. * - recreate recent pages
  372. * - queue subscriber notification (aka followers)
  373. *)
  374. Error ("not implemented yet " ^ __LOC__)
  375. let to_atom ?(attr = []) ~base e : _ Xmlm.frag =
  376. let Rfc4646 lang = e.lang in
  377. let self = e.id |> Http.reso ~base in
  378. let id = self |> Uri.to_string in
  379. let lifo init item = sep 2 :: Link.to_atom ~base item :: init in
  380. let cafo init item = sep 2 :: Category.to_atom ~base item :: init in
  381. `El (((ns_a,"entry"),
  382. ((Xmlm.ns_xml,"lang"),lang)
  383. :: attr),
  384. sep 2 :: `El (((ns_a,"id"),[]),[`Data id])
  385. :: sep 2 :: `El (((ns_a,"title"),[(("","type"),"text")]),[`Data e.title])
  386. :: sep 2 :: Rfc3339.to_xml "updated" e.updated
  387. :: sep 2 :: Rfc3339.to_xml "published" e.published
  388. :: sep 2 :: `El (((ns_a,"author"),[]),
  389. sep 3 :: `El (((ns_a,"name"),[]),[`Data (e.author |> mk_auth ~base)])
  390. :: sep 3 :: `El (((ns_rfc7033,"uri"),[]),[`Data (e.author |> mk_auth_acct ~base)])
  391. :: sep 3 :: `El (((ns_a,"uri"),[]),[`Data (base |> Uri.to_string)])
  392. :: []
  393. )
  394. :: sep 2 :: (Link.link ~rfc7033:None ~title:None ~href:self ~rel:Link.self |> Link.to_atom)
  395. :: (e.links |> List.fold_left lifo (
  396. e.categories |> List.fold_left cafo
  397. (sep 2
  398. :: `El (((ns_a,"content"),[(("","type"),"text")]),[`Data (match e.content with "" -> " " | c -> c)])
  399. :: sep 1
  400. :: [])))
  401. )
  402. let to_atom' ~base e = Ok (to_atom ~base e)
  403. end
  404. module Feed = struct
  405. let compute_links ?(min = 0) ~max ~base (a,b : string * int) =
  406. let j = ["";"-";"/index.xml"] in
  407. let compute_self ~base j v =
  408. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  409. let p0 = p0 |> St.before ~suffix:"/index.xml" |> Option.value ~default:"" in
  410. let path = base |> Uri.path in
  411. Uri.with_path base (path ^ p0 ^ "/")
  412. in
  413. let compute_first ~base j v =
  414. let v = match v with
  415. | [x;_] -> [x;"dirt"]
  416. | x -> x in
  417. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  418. let p0 = p0 |> St.before ~suffix:"-dirt/index.xml" |> Option.value ~default:"" in
  419. let path = base |> Uri.path in
  420. Uri.with_path base (path ^ p0 ^ "/")
  421. in
  422. let compute_last ~base j v =
  423. let v = match v with
  424. | [x;_] -> [x;"0"]
  425. | x -> x in
  426. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  427. let p0 = p0 |> St.before ~suffix:"/index.xml" |> Option.value ~default:"" in
  428. let path = base |> Uri.path in
  429. Uri.with_path base (path ^ p0 ^ "/")
  430. in
  431. let compute_prev ~max ~base j v =
  432. match v with
  433. | [a;b] -> let b = b |> int_of_string in
  434. if b <= max
  435. then
  436. let v = [a;b + 1 |> string_of_int] in
  437. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  438. let p0 = p0 |> St.before ~suffix:"/index.xml" |> Option.value ~default:"" in
  439. let path = base |> Uri.path in
  440. Some (Uri.with_path base (path ^ p0 ^ "/"))
  441. else None
  442. | _ -> None
  443. in
  444. let compute_next ?(min = 0) ~base j v =
  445. match v with
  446. | [a;b] -> let b = b |> int_of_string in
  447. if b > min
  448. then
  449. let v = [a;b - 1 |> string_of_int] in
  450. let p0 = v |> Make.Jig.paste j |> Option.value ~default:"-" in
  451. let p0 = p0 |> St.before ~suffix:"/index.xml" |> Option.value ~default:"" in
  452. let path = base |> Uri.path in
  453. Some (Uri.with_path base (path ^ p0 ^ "/"))
  454. else None
  455. | _ -> None
  456. in
  457. assert (Uri.empty |> Uri.equal base || base |> Uri.to_string |> St.ends_with ~suffix:"/");
  458. let v = [a;b |> string_of_int] in
  459. compute_self ~base j v,
  460. compute_first ~base j v,
  461. compute_last ~base j v,
  462. compute_prev ~max ~base j v,
  463. compute_next ~min ~base j v
  464. let head_to_atom
  465. ~base
  466. ~(self : Uri.t)
  467. ~prev
  468. ~next
  469. ~first
  470. ~last
  471. ~title
  472. ~updated
  473. ~lang
  474. ~(author : Uri.t)
  475. (init : _ Xmlm.frag list) : _ Xmlm.frag =
  476. let _ = author in
  477. let id = self |> Http.reso ~base |> Uri.to_string in
  478. let Rfc4646 lang = lang in
  479. let uri_to_page_num u =
  480. let u = u |> Uri.to_string in
  481. try Scanf.sscanf u "%[^-]-%d/" (fun _ num -> Some (string_of_int (num + 1)))
  482. with | _ -> None
  483. in
  484. let init = match next with
  485. | None -> init
  486. | Some href -> (Link.link ~rfc7033:None ~title:(uri_to_page_num href) ~href ~rel:Link.next |> Link.to_atom) :: sep 1 :: init in
  487. let init = match prev with
  488. | None -> init
  489. | Some href -> (Link.link ~rfc7033:None ~title:(uri_to_page_num href) ~href ~rel:Link.prev |> Link.to_atom) :: sep 1 :: init in
  490. `El (((ns_a,"feed"),[
  491. ((Xmlm.ns_xmlns,"xmlns"),ns_a);
  492. ((Xmlm.ns_xmlns,"wf"),ns_rfc7033);
  493. ((Xmlm.ns_xml,"lang"),lang);
  494. ((Xmlm.ns_xml,"base"),base |> Uri.to_string);
  495. ]),
  496. sep 1 :: `El (((ns_a,"id"),[]),[`Data id])
  497. :: sep 1 :: `El (((ns_a,"title"),[(("","type"),"text")]),[`Data title])
  498. :: sep 1 :: Rfc3339.to_xml "updated" updated
  499. :: sep 1 :: `El (((ns_a,"generator"),[ (("","uri"),St.seppo_s) ]),
  500. `Data St.seppo_c :: [] )
  501. :: sep 1 :: (Link.link ~rfc7033:None ~title:(uri_to_page_num self) ~href:self ~rel:Link.self |> Link.to_atom)
  502. :: sep 1 :: (Link.link ~rfc7033:None ~title:(Some "last") ~href:first ~rel:Link.first |> Link.to_atom)
  503. :: sep 1 :: (Link.link ~rfc7033:None ~title:(Some "1") ~href:last ~rel:Link.last |> Link.to_atom)
  504. :: sep 1 :: init)
  505. let to_atom
  506. ~base
  507. ~self
  508. ~prev
  509. ~next
  510. ~first
  511. ~last
  512. ~title
  513. ~updated
  514. ~lang
  515. ~author
  516. entries : _ Xmlm.frag =
  517. let entry init item = Entry.to_atom ~base item :: sep 1 :: init in
  518. entries |> List.fold_left entry []
  519. |> head_to_atom
  520. ~base
  521. ~self
  522. ~prev
  523. ~next
  524. ~first
  525. ~last
  526. ~title
  527. ~updated
  528. ~lang
  529. ~author
  530. let to_atom_
  531. ~base
  532. ~self
  533. ~prev
  534. ~next
  535. ~first
  536. ~last
  537. ~title
  538. ~updated
  539. ~lang
  540. ~author
  541. _dst (es : (Entry.t,string) result list) : _ Xmlm.frag =
  542. es |> List.fold_left (fun a e ->
  543. match e with
  544. | Error e ->
  545. Logr.warn (fun m -> m "%s.%s ignore broken entry: %s" "Rfc4287.Feed" "to_atom_" e);
  546. a
  547. | Ok e -> e :: a) []
  548. |> to_atom
  549. ~base
  550. ~self
  551. ~prev
  552. ~next
  553. ~first
  554. ~last
  555. ~title
  556. ~updated
  557. ~lang
  558. ~author
  559. let to_file fn (x : _ Xmlm.frag) =
  560. let xsl = fn |> xsl "posts.xsl" in
  561. File.out_channel fn (Xml.to_chan ~xsl x);
  562. Ok fn
  563. end