A fork of mtelver's day10 project
at main2 208 lines 8.5 kB view raw
1(* Generate an atom feed from compiled odocl blog posts. *) 2 3let id = Uri.of_string "https://jon.recoil.org/atom.xml" 4let title : Syndic.Atom.text_construct = Syndic.Atom.Text "Jon's blog" 5 6let author = 7 Syndic.Atom.author "Jon Ludlam" ~uri:(Uri.of_string "https://jon.recoil.org/") 8 9let updated = Unix.gettimeofday () |> Ptime.of_float_s |> Option.get 10 11(** Extract the text content from a custom tag's payload. 12 Custom tags like [@published 2026-03-02] are stored in Comment.docs 13 as [`Tag (`Custom ("published", elements))]. The elements are 14 nestable block elements — typically a single paragraph containing 15 words and spaces. *) 16let text_of_tag_payload elements = 17 let buf = Buffer.create 32 in 18 List.iter 19 (fun (el : Odoc_model.Comment.nestable_block_element Odoc_model.Location_.with_location) -> 20 match el.Odoc_model.Location_.value with 21 | `Paragraph inlines -> 22 List.iter 23 (fun (il : Odoc_model.Comment.inline_element Odoc_model.Location_.with_location) -> 24 match il.value with 25 | `Word w -> Buffer.add_string buf w 26 | `Space -> Buffer.add_char buf ' ' 27 | _ -> ()) 28 inlines 29 | _ -> ()) 30 elements; 31 String.trim (Buffer.contents buf) 32 33(** Find a custom tag by name in the page's content elements. *) 34let find_custom_tag name (docs : Odoc_model.Comment.docs) = 35 List.find_map 36 (fun (el : Odoc_model.Comment.block_element Odoc_model.Location_.with_location) -> 37 match el.value with 38 | `Tag (`Custom (n, payload)) when n = name -> 39 Some (text_of_tag_payload payload) 40 | _ -> None) 41 docs.elements 42 43let entry_of_mld odoc_file = 44 let report_error during msg = 45 Format.eprintf "Error processing file '%s' while %s: %s\n%!" 46 (Fpath.to_string odoc_file) 47 during msg; 48 None 49 in 50 let unit = 51 match Odoc_odoc.Odoc_file.load odoc_file with 52 | Ok f -> Some f 53 | Error (`Msg m) -> 54 ignore (report_error "loading file" m); 55 None 56 in 57 match unit with 58 | None -> None 59 | Some unit -> ( 60 let page = 61 match unit.content with 62 | Odoc_odoc.Odoc_file.Page_content page -> Some page 63 | _ -> None 64 in 65 match page with 66 | None -> None 67 | Some page -> ( 68 let document = 69 Odoc_document.Renderer.document_of_page ~syntax:OCaml page 70 in 71 let published = find_custom_tag "published" page.content in 72 match published with 73 | None -> None (* Skip posts without published date *) 74 | Some published -> ( 75 match document with 76 | Odoc_document.Types.Document.Source_page _ -> None 77 | Odoc_document.Types.Document.Page p -> 78 let first_heading = 79 List.find_map 80 (function 81 | Odoc_document.Types.Item.Heading h -> Some h 82 | _ -> None) 83 p.preamble 84 in 85 match first_heading with 86 | None -> 87 ignore (report_error "parsing title" "No heading found"); 88 None 89 | Some first_heading -> 90 let title = 91 List.filter_map 92 (function 93 | Odoc_document.Types.Inline.{ desc = Text t; _ } -> Some t 94 | _ -> None) 95 first_heading.title 96 in 97 let title = String.concat "" title in 98 if title = "" then None 99 else 100 let resolve = Odoc_html.Link.Current p.url in 101 let config = 102 Odoc_html.Config.v ~semantic_uris:false ~indent:false 103 ~flat:false ~open_details:false ~as_json:false ~remap:[] () 104 in 105 let url = Odoc_html.Generator.filepath p.url ~config in 106 let url = 107 Format.asprintf "https://jon.recoil.org/%s" 108 (Fpath.to_string url) 109 in 110 (* Generate full content: preamble + items *) 111 let all_items = p.preamble @ p.items in 112 let html = Odoc_html.Generator.items ~config ~resolve all_items in 113 let content_fmt = Fmt.list (Tyxml.Html.pp_elt ()) in 114 let content = Format.asprintf "%a" content_fmt html in 115 (* Extract first paragraph for summary *) 116 let summary = 117 let first_text = 118 List.find_map 119 (function 120 | Odoc_document.Types.Item.Text blocks -> 121 List.find_map 122 (function 123 | { Odoc_document.Types.Block.desc = 124 Odoc_document.Types.Block.Paragraph inline; 125 _ 126 } -> 127 let text = 128 List.filter_map 129 (function 130 | Odoc_document.Types.Inline. 131 { desc = Text t; _ } -> 132 Some t 133 | _ -> None) 134 inline 135 in 136 if text = [] then None 137 else Some (String.concat "" text) 138 | _ -> None) 139 blocks 140 | _ -> None) 141 p.preamble 142 in 143 match first_text with 144 | Some t -> 145 if String.length t > 200 then 146 String.sub t 0 200 ^ "..." 147 else t 148 | None -> title 149 in 150 let published = 151 try 152 ISO8601.Permissive.date published |> Ptime.of_float_s 153 with _ -> 154 Format.eprintf "Error parsing date '%s' for %s\n%!" 155 published (Fpath.to_string odoc_file); 156 None 157 in 158 match published with 159 | None -> None 160 | Some published -> 161 Some 162 (Syndic.Atom.entry ~id:(Uri.of_string url) 163 ~title:(Syndic.Atom.Text title) 164 ~published ~updated:published 165 ~summary:(Syndic.Atom.Text summary) 166 ~content:(Syndic.Atom.Html (None, content)) 167 ~links: 168 [ 169 Syndic.Atom.link ~rel:Syndic.Atom.Alternate 170 (Uri.of_string url); 171 ] 172 ~authors:(author, []) ())))) 173 174let is_blog_post path = 175 let basename = Fpath.basename path in 176 Fpath.has_ext "odocl" path 177 && String.length basename > 5 178 && String.sub basename 0 5 = "page-" 179 && basename <> "page-index.odocl" 180 181let entries = 182 let mlds = 183 Bos.OS.Dir.fold_contents 184 (fun path acc -> if is_blog_post path then path :: acc else acc) 185 [] 186 (Fpath.v "_build/default/site/_odoc/blog") 187 in 188 match mlds with 189 | Ok mlds -> 190 let entries = List.filter_map entry_of_mld mlds in 191 (* Sort by published date, newest first *) 192 List.sort Syndic.Atom.descending entries 193 | Error (`Msg m) -> 194 Format.eprintf "Error finding blog posts: %s\n%!" m; 195 [] 196 197let self_link = 198 Syndic.Atom.link ~rel:Self (Uri.of_string "https://jon.recoil.org/atom.xml") 199 200let alt_link = 201 Syndic.Atom.link ~rel:Alternate (Uri.of_string "https://jon.recoil.org/blog/") 202 203let feed = 204 Syndic.Atom.feed ~id ~title ~updated ~links:[ self_link; alt_link ] entries 205 206let _ = 207 Syndic.Atom.write feed "atom.xml"; 208 Format.printf "Generated atom.xml with %d entries\n%!" (List.length entries)