use std::path::{Path, PathBuf};
use beecast_page::{build_page, inspect, CastError, PageMeta};
use crate::annotate::CastAnnotation;
#[derive(Debug)]
pub enum Sidecar {
Absent,
Found(CastAnnotation),
Malformed(PathBuf),
}
pub fn load_sidecar(cast_path: &Path) -> Sidecar {
let Some(sidecar) = crate::daemon::chapters_sidecar_path(&cast_path.to_string_lossy()) else {
return Sidecar::Absent;
};
let Ok(text) = std::fs::read_to_string(&sidecar) else { return Sidecar::Absent };
match crate::annotate::parse_annotation(&text) {
Some(a) => Sidecar::Found(a),
None => Sidecar::Malformed(sidecar),
}
}
pub fn cast_stem(cast_path: &Path) -> String {
match cast_path.file_stem() {
Some(stem) => stem.to_string_lossy().into_owned(),
None => cast_path.to_string_lossy().into_owned(),
}
}
pub fn default_output_path(cast_path: &Path) -> PathBuf {
match cast_path.file_name().and_then(|n| n.to_str()).and_then(|n| n.strip_suffix(".cast")) {
Some(stem) => cast_path.with_file_name(format!("{stem}.html")),
None => PathBuf::from(format!("{}.html", cast_path.display())),
}
}
pub fn render_page(cast_ndjson: &str, stem: &str, annotation: Option<&CastAnnotation>) -> Result<String, CastError> {
inspect(cast_ndjson)?;
let chapters: Vec<(f64, &str)> =
annotation.map(|a| a.chapters.iter().map(|c| (c.t, c.title.as_str())).collect()).unwrap_or_default();
let meta = PageMeta { title: Some(stem), summary: annotation.map(|a| a.summary.as_str()), chapters: &chapters };
Ok(build_page(cast_ndjson, &meta, stem))
}
pub fn render_page_from_texts(cast_ndjson: &str, sidecar_json: Option<&str>, stem: &str) -> Result<String, CastError> {
let annotation = sidecar_json.and_then(crate::annotate::parse_annotation);
render_page(cast_ndjson, stem, annotation.as_ref())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::annotate::Chapter;
const CAST: &str =
"{\"version\":2,\"width\":80,\"height\":24}\n[0.5,\"o\",\"hello\\r\\n\"]\n[2.0,\"o\",\"done\\r\\n\"]\n";
fn annotation() -> CastAnnotation {
CastAnnotation {
summary: "Ran the demo.".into(),
chapters: vec![Chapter { t: 0.0, title: "Start".into() }, Chapter { t: 1.5, title: "Finish".into() }],
}
}
#[test]
fn render_page_maps_the_sidecar_onto_page_meta() {
let a = annotation();
let page = render_page(CAST, "rec-042", Some(&a)).unwrap();
assert!(page.contains("<title>rec-042</title>"), "stem is the title; got no such <title>");
assert!(page.contains("Ran the demo."), "summary rendered");
assert!(page.contains("\"chapters\":[{\"t\":0.0,\"title\":\"Start\"},{\"t\":1.5,\"title\":\"Finish\"}]"));
}
#[test]
fn render_page_without_a_sidecar_falls_back_to_the_stem_title() {
let page = render_page(CAST, "bare-rec", None).unwrap();
assert!(page.contains("<title>bare-rec</title>"));
assert!(page.contains("<p id=\"summary\" hidden></p>"), "no summary stays hidden");
assert!(!page.contains("\"chapters\":["), "no chapters without a sidecar");
}
#[test]
fn render_page_rejects_a_non_asciicast() {
assert_eq!(render_page("not a recording at all", "x", None), Err(CastError::NotAsciicast));
}
#[test]
fn render_page_from_texts_parses_the_sidecar_and_shrugs_off_junk() {
let page = render_page_from_texts(CAST, Some(&annotation().to_sidecar_json()), "rec-042").unwrap();
assert!(page.contains("\"chapters\":[{\"t\":0.0,\"title\":\"Start\"},{\"t\":1.5,\"title\":\"Finish\"}]"));
for sidecar in [Some("{ not json"), None] {
let page = render_page_from_texts(CAST, sidecar, "rec-042").unwrap();
assert!(page.contains("<title>rec-042</title>"));
assert!(!page.contains("\"chapters\":["), "malformed/absent sidecar exports without chapters");
}
}
#[test]
fn load_sidecar_reports_absent_found_and_malformed() {
let dir = std::env::temp_dir().join(format!("scsh-export-test-{}", crate::runtime::random_nonce_6()));
std::fs::create_dir_all(&dir).unwrap();
let cast = dir.join("rec.cast");
std::fs::write(&cast, CAST).unwrap();
assert!(matches!(load_sidecar(&cast), Sidecar::Absent));
std::fs::write(dir.join("rec.chapters.json"), annotation().to_sidecar_json()).unwrap();
match load_sidecar(&cast) {
Sidecar::Found(a) => {
assert_eq!(a.summary, "Ran the demo.");
assert_eq!(a.chapters.len(), 2);
}
other => panic!("expected Found, got {other:?}"),
}
std::fs::write(dir.join("rec.chapters.json"), "{ not json").unwrap();
match load_sidecar(&cast) {
Sidecar::Malformed(p) => assert!(p.ends_with("rec.chapters.json"), "got: {}", p.display()),
other => panic!("expected Malformed, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn default_output_path_sits_next_to_the_cast() {
assert_eq!(default_output_path(Path::new("/a/b/foo.cast")), PathBuf::from("/a/b/foo.html"));
assert_eq!(default_output_path(Path::new("/a/b/session.rec")), PathBuf::from("/a/b/session.rec.html"));
}
}