Skip to main content

rpi_cli/
export.rs

1//! Session file export helpers.
2//!
3//! Native Pi accepts a JSONL session file and writes an HTML transcript. The
4//! Rust session format is already JSONL, so this module keeps the source lines
5//! intact for `.jsonl` destinations and provides a dependency-free HTML view
6//! for browser inspection.
7
8use 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('&', "&amp;")
43        .replace('<', "&lt;")
44        .replace('>', "&gt;")
45        .replace('"', "&quot;")
46        .replace('\'', "&#39;")
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("&lt;hello&gt;"));
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}