adrscope/infrastructure/renderer/
html.rs1use askama::Template;
4use serde::Serialize;
5use time::OffsetDateTime;
6
7use crate::domain::{Adr, Facets, Graph};
8use crate::error::{Error, Result};
9
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12pub enum Theme {
13 Light,
15 Dark,
17 #[default]
19 Auto,
20}
21
22impl Theme {
23 #[must_use]
25 pub const fn as_str(&self) -> &'static str {
26 match self {
27 Self::Light => "light",
28 Self::Dark => "dark",
29 Self::Auto => "auto",
30 }
31 }
32}
33
34impl std::str::FromStr for Theme {
35 type Err = String;
36
37 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
38 match s.to_lowercase().as_str() {
39 "light" => Ok(Self::Light),
40 "dark" => Ok(Self::Dark),
41 "auto" => Ok(Self::Auto),
42 _ => Err(format!("invalid theme: {s}")),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Default)]
49pub struct RenderConfig {
50 pub title: String,
52 pub theme: Theme,
54 pub embed_assets: bool,
56}
57
58impl RenderConfig {
59 #[must_use]
61 pub fn new(title: impl Into<String>) -> Self {
62 Self {
63 title: title.into(),
64 theme: Theme::default(),
65 embed_assets: true,
66 }
67 }
68
69 #[must_use]
71 pub const fn with_theme(mut self, theme: Theme) -> Self {
72 self.theme = theme;
73 self
74 }
75}
76
77#[derive(Debug, Clone, Serialize)]
79pub struct ViewerData {
80 pub meta: ViewerMeta,
82 pub records: Vec<Adr>,
84 pub facets: Facets,
86 pub graph: Graph,
88}
89
90#[derive(Debug, Clone, Serialize)]
92pub struct ViewerMeta {
93 pub generated: String,
95 pub generator: String,
97 pub schema_version: String,
99 pub source_dir: String,
101}
102
103impl ViewerMeta {
104 #[must_use]
106 pub fn new(source_dir: impl Into<String>) -> Self {
107 Self {
108 generated: OffsetDateTime::now_utc()
109 .format(&time::format_description::well_known::Rfc3339)
110 .unwrap_or_else(|_| "unknown".to_string()),
111 generator: format!("adrscope/{}", env!("CARGO_PKG_VERSION")),
112 schema_version: "1.0.0".to_string(),
113 source_dir: source_dir.into(),
114 }
115 }
116}
117
118#[derive(Template)]
120#[template(path = "viewer.html", escape = "none")]
121pub struct ViewerTemplate<'a> {
122 pub title: &'a str,
124 pub theme: &'a str,
126 pub data_json: &'a str,
128 pub css: &'a str,
130 pub js: &'a str,
132}
133
134#[derive(Debug, Clone, Default)]
136pub struct HtmlRenderer;
137
138impl HtmlRenderer {
139 #[must_use]
141 pub const fn new() -> Self {
142 Self
143 }
144
145 pub fn render(
147 &self,
148 adrs: Vec<Adr>,
149 source_dir: &str,
150 config: &RenderConfig,
151 ) -> Result<String> {
152 let data = ViewerData {
154 meta: ViewerMeta::new(source_dir),
155 facets: Facets::from_adrs(&adrs),
156 graph: Graph::from_adrs(&adrs),
157 records: adrs,
158 };
159
160 let data_json =
162 serde_json::to_string(&data).map_err(|e| Error::JsonSerialize(e.to_string()))?;
163
164 let template = ViewerTemplate {
166 title: &config.title,
167 theme: config.theme.as_str(),
168 data_json: &data_json,
169 css: include_str!("../../../templates/styles.css"),
170 js: include_str!("../../../templates/app.js"),
171 };
172
173 template.render().map_err(Error::from)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_theme_from_str() {
183 assert_eq!("light".parse::<Theme>().ok(), Some(Theme::Light));
184 assert_eq!("DARK".parse::<Theme>().ok(), Some(Theme::Dark));
185 assert_eq!("Auto".parse::<Theme>().ok(), Some(Theme::Auto));
186 assert!("invalid".parse::<Theme>().is_err());
187 }
188
189 #[test]
190 fn test_theme_as_str() {
191 assert_eq!(Theme::Light.as_str(), "light");
192 assert_eq!(Theme::Dark.as_str(), "dark");
193 assert_eq!(Theme::Auto.as_str(), "auto");
194 }
195
196 #[test]
197 fn test_render_config_builder() {
198 let config = RenderConfig::new("My ADRs").with_theme(Theme::Dark);
199
200 assert_eq!(config.title, "My ADRs");
201 assert_eq!(config.theme, Theme::Dark);
202 }
203
204 #[test]
205 fn test_viewer_meta_creation() {
206 let meta = ViewerMeta::new("docs/decisions");
207
208 assert!(meta.generated.contains("T")); assert!(meta.generator.starts_with("adrscope/"));
210 assert_eq!(meta.schema_version, "1.0.0");
211 assert_eq!(meta.source_dir, "docs/decisions");
212 }
213}