1use std::path::Path;
9
10pub fn export_file(input: &Path, output: &Path) -> Result<(), String> {
11 let source = std::fs::read_to_string(input)
12 .map_err(|error| format!("could not read session {}: {error}", input.display()))?;
13 if output
14 .extension()
15 .and_then(|extension| extension.to_str())
16 .is_some_and(|extension| extension.eq_ignore_ascii_case("jsonl"))
17 {
18 std::fs::write(output, source.as_bytes())
19 .map_err(|error| format!("could not write export {}: {error}", output.display()))?;
20 return Ok(());
21 }
22
23 let mut body = String::new();
24 for line in source.lines().filter(|line| !line.trim().is_empty()) {
25 let display = serde_json::from_str::<serde_json::Value>(line)
26 .ok()
27 .and_then(|value| serde_json::to_string_pretty(&value).ok())
28 .unwrap_or_else(|| line.to_string());
29 body.push_str("<pre>");
30 body.push_str(&escape_html(&display));
31 body.push_str("</pre>\n");
32 }
33 let html = format!(
34 "<!doctype html><html><head><meta charset=\"utf-8\"><title>Pi session</title><style>body{{font:14px ui-monospace,monospace;background:#111;color:#eee;padding:24px}}pre{{white-space:pre-wrap;border-bottom:1px solid #444;padding:12px 0}}</style></head><body><h1>Pi session</h1>{body}</body></html>"
35 );
36 std::fs::write(output, html.as_bytes())
37 .map_err(|error| format!("could not write export {}: {error}", output.display()))
38}
39
40fn escape_html(value: &str) -> String {
41 value
42 .replace('&', "&")
43 .replace('<', "<")
44 .replace('>', ">")
45 .replace('"', """)
46 .replace('\'', "'")
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn exports_jsonl_as_html_with_escaped_content() {
55 let dir = tempfile::tempdir().unwrap();
56 let input = dir.path().join("session.jsonl");
57 let output = dir.path().join("session.html");
58 std::fs::write(&input, "{\"text\":\"<hello>\"}\n").unwrap();
59 export_file(&input, &output).unwrap();
60 let html = std::fs::read_to_string(output).unwrap();
61 assert!(html.contains("<hello>"));
62 assert!(!html.contains("<hello>"));
63 }
64
65 #[test]
66 fn jsonl_destination_preserves_source() {
67 let dir = tempfile::tempdir().unwrap();
68 let input = dir.path().join("session.jsonl");
69 let output = dir.path().join("copy.jsonl");
70 let source = "not-json-but-valid-session-line\n";
71 std::fs::write(&input, source).unwrap();
72 export_file(&input, &output).unwrap();
73 assert_eq!(std::fs::read_to_string(output).unwrap(), source);
74 }
75}