1use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use serde_json::json;
8
9use crate::store::Progress;
10use crate::{coach, gate, layout};
11
12const TEMPLATE: &str = include_str!("charts.html");
13const DATA_MARK: &str = "\"__VERY_NORMAL_DATA__\"";
14
15pub fn default_path(progress: &Path) -> PathBuf {
17 progress.with_file_name("charts.html")
18}
19
20pub fn html(progress: &Progress, now_ms: u64) -> String {
23 let stages: Vec<_> = (0..=layout::last())
24 .map(|s| {
25 json!({
26 "name": layout::name(s),
27 "keys": layout::keys_string(s),
28 "focus": layout::focus(s).iter().collect::<String>(),
29 })
30 })
31 .collect();
32 let data = json!({
33 "generated": now_ms,
34 "plan": layout::plan().name,
35 "stages": stages,
36 "baseline": progress.baseline(),
37 "progress": progress,
38 "goal": coach::DAILY_GOAL,
39 "breakMinutes": coach::BREAK,
40 "gate": {
41 "wordsAccuracy": gate::WORDS_ACCURACY,
42 "speedShare": gate::SPEED_SHARE,
43 "realAccuracy": gate::REAL_ACCURACY,
44 },
45 });
46 let data = data.to_string().replace("</", "<\\/");
48 TEMPLATE.replacen(DATA_MARK, &data, 1)
49}
50
51pub fn write(progress: &Progress, path: &Path, now_ms: u64) -> Result<()> {
52 if let Some(dir) = path.parent() {
53 std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
54 }
55 std::fs::write(path, html(progress, now_ms))
56 .with_context(|| format!("writing {}", path.display()))
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn the_data_lands_in_the_page_and_cannot_close_the_script() {
65 let mut p = Progress::default();
66 p.manual_mut(0).wpm = 33.0;
67 p.warmed_up = Some("</script><b>".into());
68 let page = html(&p, 1);
69 assert!(!page.contains(DATA_MARK));
70 assert!(page.contains("\"baseline\":33.0"));
71 assert!(!page.contains("</script><b>"));
72 assert_eq!(page.matches("</script>").count(), 1);
73 }
74}