(* emit_csv.ml *) module T = Template let escape_field s = let needs = String.exists (fun c -> c = ',' || c = '"' || c = '\n' || c = '\r') s in if not needs then s else begin let b = Buffer.create (String.length s + 8) in Buffer.add_char b '"'; String.iter (fun c -> if c = '"' then Buffer.add_string b "\"\"" else Buffer.add_char b c) s; Buffer.add_char b '"'; Buffer.contents b end let get v k = match v with T.Obj kvs -> List.assoc_opt k kvs | _ -> None let s v k = match get v k with Some (T.Str x) -> x | _ -> "" (* [name] is now the single RESOLVED display string (view.ml), not a lang-keyed object -- the [name_la]/[name_en] pair this replaces carried the kernel's own [Celebration.names], a different, unlocalised source. Machine formats still carry [slug] alongside it, so a script keeps the stable key and a human reads the name. *) let columns = [ "date"; "rite"; "season"; "season_name"; "week"; "slug"; "name"; "weekday"; "rank"; "rank_name"; "colour"; "colour_name"; "subject"; "first"; "gospel"; "comms" ] let row ~rite d = let comms = match get d "comms" with | Some (T.List l) -> String.concat " " (List.map (fun c -> s c "slug") l) | _ -> "" in String.concat "," (List.map escape_field [ s d "iso"; rite; s d "season"; s d "season_name"; s d "week"; s d "slug"; s d "name"; s d "weekday"; s d "rank"; s d "rank_name"; s d "colour"; s d "colour_name"; s d "subject"; s d "first"; s d "gospel"; comms ]) let year v = let rite = s v "rite" in let days = match get v "days" with Some (T.List l) -> l | _ -> [] in let b = Buffer.create (64 * 400) in Buffer.add_string b (String.concat "," columns); Buffer.add_char b '\n'; List.iter (fun d -> Buffer.add_string b (row ~rite d); Buffer.add_char b '\n') days; Buffer.contents b