1use std::borrow::Cow;
2use std::fmt::Write;
3use std::fs::File;
4use std::io::BufWriter;
5use std::path::{Component, Path, Prefix, MAIN_SEPARATOR};
6
7use anyhow::Context;
8use askama::Template;
9use html_escape::{encode_safe, encode_safe_to_string};
10use rayon::iter::IntoParallelIterator;
11use rayon::prelude::*;
12use rust_embed::RustEmbed;
13
14use crate::remark::{Line, Location, MessagePart, Remark};
15use crate::utils::callback::LoadCallback;
16use crate::utils::data_structures::{Map, Set};
17
18pub const INDEX_FILE_PATH: &str = "index.html";
19const REMARK_LIST_FILE_PATH: &str = "remarks.html";
20
21const SRC_DIR_NAME: &str = "src";
24
25#[derive(RustEmbed)]
26#[folder = "templates/assets"]
27struct StaticAssets;
28
29#[derive(serde::Serialize)]
30struct RemarkIndexEntry<'a> {
31 name: &'a str,
32 location: Option<String>,
33 function: Cow<'a, str>,
34 message: String,
35 hotness: Option<i32>,
36}
37
38#[derive(serde::Serialize, PartialEq, Eq, Hash)]
39struct RemarkSourceEntry<'a> {
40 name: &'a str,
41 function: &'a str,
42 line: Line,
43 message: String,
44 hotness: Option<i32>,
45}
46
47#[derive(Template)]
48#[template(path = "remark-list.jinja")]
49pub struct RemarkListTemplate {
50 remarks_json: String,
51}
52
53#[derive(serde::Serialize)]
54struct SourceFileLink<'a> {
55 name: &'a str,
56 file: String,
57 remark_count: u64,
58}
59
60#[derive(Template)]
61#[template(path = "index.jinja")]
62pub struct IndexTemplate<'a> {
63 source_links: Vec<SourceFileLink<'a>>,
64}
65
66#[derive(Template)]
67#[template(path = "source-file.jinja")]
68pub struct SourceFileTemplate<'a> {
69 path: &'a str,
70 remarks: Set<RemarkSourceEntry<'a>>,
71 file_content: String,
72}
73
74pub fn render_remarks(
75 remarks: Vec<Remark>,
76 source_dir: &Path,
77 output_dir: &Path,
78 callback: Option<&(dyn LoadCallback + Sync)>,
79) -> anyhow::Result<()> {
80 let _ = std::fs::remove_dir_all(output_dir);
81 std::fs::create_dir_all(output_dir).context("Cannot create output directory")?;
82
83 for asset_path in StaticAssets::iter() {
85 let data = StaticAssets::get(&asset_path).unwrap().data;
86 let path = output_dir.join("assets").join(asset_path.as_ref());
87 if let Some(parent) = path.parent() {
88 std::fs::create_dir_all(parent).context("Cannot create output asset directory")?;
89 }
90 std::fs::write(path, data).context("Cannot copy asset file to output directory")?;
91 }
92
93 let mut file_to_remarks: Map<&str, Set<RemarkSourceEntry>> = Map::default();
94
95 let remark_entries = remarks
97 .iter()
98 .map(|r| {
99 let Remark {
100 pass: _,
101 name,
102 function,
103 message,
104 hotness,
105 } = r;
106
107 let entry = RemarkIndexEntry {
108 name,
109 location: function.location.as_ref().map(|location| {
110 let mut buffer = String::new();
111 render_remark_link(&mut buffer, location, Some(SRC_DIR_NAME), None);
112 buffer
113 }),
114 function: encode_safe(&function.name),
115 message: format_message(message, Some(SRC_DIR_NAME)),
116 hotness: *hotness,
117 };
118 if let Some(ref location) = function.location {
119 file_to_remarks
120 .entry(&location.file)
121 .or_default()
122 .insert(RemarkSourceEntry {
123 name,
124 function: &function.name,
125 line: location.line,
126 message: format_message(message, None),
128 hotness: *hotness,
129 });
130 }
131 for msg_part in &r.message {
134 if let MessagePart::AnnotatedString { location, .. } = msg_part {
135 file_to_remarks.entry(&location.file).or_default();
136 }
137 }
138 entry
139 })
140 .collect::<Vec<_>>();
141
142 let serialized_remarks = serde_json::to_string(&remark_entries)?;
143 let remark_list_page = RemarkListTemplate {
144 remarks_json: serialized_remarks,
145 };
146 render_to_file(&remark_list_page, &output_dir.join(REMARK_LIST_FILE_PATH))?;
147
148 let mut source_links: Vec<SourceFileLink> = file_to_remarks
149 .iter()
150 .filter(|(_, remarks)| !remarks.is_empty())
151 .map(|(name, remarks)| {
152 let mut file = String::new();
153 path_to_relative_url(&mut file, Some(SRC_DIR_NAME), name);
154 SourceFileLink {
155 name,
156 file,
157 remark_count: remarks.len() as u64,
158 }
159 })
160 .collect();
161
162 source_links.sort_by_key(|link| (link.name.starts_with('/'), -(link.remark_count as i64)));
164
165 let index_page = IndexTemplate { source_links };
166 render_to_file(&index_page, &output_dir.join(INDEX_FILE_PATH))?;
167
168 if let Some(callback) = callback {
169 callback.start(file_to_remarks.len() as u64);
170 }
171
172 let results = file_to_remarks
174 .into_par_iter()
175 .map(|(source_file, remarks)| -> anyhow::Result<()> {
176 let original_path = resolve_path(source_dir, Path::new(source_file));
177 let file_content = std::fs::read_to_string(&original_path)
178 .with_context(|| format!("Cannot read source file {}", original_path.display()))?;
179
180 if let Some(callback) = callback {
181 callback.advance();
182 }
183
184 let mut buffer = String::new();
186 path_to_relative_url(&mut buffer, Some(SRC_DIR_NAME), source_file);
187 let output_path = output_dir.join(buffer);
188 let source_file_page = SourceFileTemplate {
189 path: source_file,
190 remarks,
191 file_content,
192 };
193 render_to_file(&source_file_page, Path::new(&output_path))
194 .with_context(|| anyhow::anyhow!("Failed to render {source_file}"))?;
195 Ok(())
196 })
197 .collect::<Vec<anyhow::Result<()>>>();
198
199 let failed = results.into_iter().filter(|r| r.is_err()).count();
200 if failed > 0 {
201 log::warn!("Failed to write {failed} source file(s)");
202 }
203
204 if let Some(callback) = callback {
205 callback.finish();
206 }
207
208 Ok(())
209}
210
211fn format_message(parts: &[MessagePart], prefix: Option<&str>) -> String {
212 let mut buffer = String::with_capacity(32);
213 for part in parts {
214 match part {
215 MessagePart::String(string) => {
216 encode_safe_to_string(string, &mut buffer);
217 }
218 MessagePart::AnnotatedString { message, location } => {
219 render_remark_link(&mut buffer, location, prefix, Some(message));
220 }
221 }
222 }
223 buffer
224}
225
226fn render_remark_link(
227 buffer: &mut String,
228 location: &Location,
229 prefix: Option<&str>,
230 label: Option<&str>,
231) {
232 buffer.push_str("<a href='");
233 path_to_relative_url(buffer, prefix, &location.file);
234 buffer.push_str("#L");
235 buffer.write_fmt(format_args!("{}", location.line)).unwrap();
236 buffer.push_str("'>");
237
238 let label = label.map(Cow::from).unwrap_or_else(|| {
239 format!("{}:{}:{}", location.file, location.line, location.column).into()
240 });
241 encode_safe_to_string(label, buffer);
242
243 buffer.push_str("</a>");
244}
245
246fn path_to_relative_url(buffer: &mut String, prefix: Option<&str>, path: &str) {
249 if let Some(prefix) = prefix {
250 buffer.push_str(prefix);
251 buffer.push(MAIN_SEPARATOR);
252 }
253
254 let path = Path::new(path);
255 let mut first = true;
256 for component in path.components() {
257 if !first {
258 buffer.push('_');
259 }
260 first = false;
261
262 match component {
263 Component::Prefix(prefix) => {
264 match prefix.kind() {
265 Prefix::Disk(disk) => buffer.push(disk as char),
266 _ => buffer.push_str("_prefix_"),
267 };
268 first = true;
269 }
270 Component::CurDir | Component::ParentDir => buffer.push('_'),
271 Component::Normal(component) => {
272 buffer.push_str(&component.to_string_lossy());
273 }
274 Component::RootDir => {}
275 }
276 }
277 buffer.push_str(".html");
278}
279
280fn resolve_path<'a>(root_dir: &Path, path: &'a Path) -> Cow<'a, Path> {
281 if path.is_absolute() {
282 path.into()
283 } else {
284 root_dir.join(path).into()
285 }
286}
287
288fn render_to_file<T: askama::Template>(template: &T, path: &Path) -> anyhow::Result<()> {
289 if let Some(parent) = path.parent() {
290 std::fs::create_dir_all(parent)
291 .context("Cannot create directory for storing rendered file")?;
292 }
293
294 let file = File::create(path)
295 .with_context(|| format!("Cannot create template file {}", path.display()))?;
296 let mut writer = BufWriter::new(file);
297 template
298 .write_into(&mut writer)
299 .with_context(|| format!("Cannot render template into {}", path.display()))?;
300 Ok(())
301}
302
303#[cfg(test)]
304mod tests {
305 use crate::render::path_to_relative_url;
306
307 #[cfg(windows)]
308 #[test]
309 fn relative_path_c_prefix() {
310 check_path(r#"C:\foo\bar"#, "C_foo_bar.html");
311 }
312
313 #[test]
314 fn relative_path_absolute() {
315 check_path("/tmp/foo/bar", "_tmp_foo_bar.html");
316 }
317
318 #[test]
319 fn relative_path_relative() {
320 check_path("foo/bar", "foo_bar.html");
321 }
322
323 fn check_path(path: &str, expected: &str) {
324 let mut buffer = String::new();
325 path_to_relative_url(&mut buffer, None, path);
326 assert_eq!(buffer, expected);
327 }
328}