blob: 67f3fe888c62096b54eff48891bdd3f7d490251f [file] [log] [blame]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::types::{Capability, FileInfo, OutputSummary, PackageContents, ProtocolToClientMap};
use anyhow::{bail, Context, Result};
use argh::FromArgs;
use camino::Utf8PathBuf;
use fuchsia_url::Hash;
use handlebars::{
handlebars_helper, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError,
};
use rayon::prelude::*;
use serde::Serialize;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::sync::{LazyLock, Mutex};
use std::time::Instant;
#[derive(FromArgs)]
#[argh(subcommand, name = "html")]
/// generate an HTML report from output data
pub struct HtmlCommand {
/// input file generated using "process" command
#[argh(option)]
input: Utf8PathBuf,
/// output directory for HTML, must not exist
#[argh(option)]
output: Utf8PathBuf,
}
impl HtmlCommand {
pub fn execute(self) -> Result<()> {
if !self.input.is_file() {
bail!("{:?} is not a file", self.input);
} else if self.output.exists() {
bail!("{:?} must not exist, it will be created by this tool", self.output);
}
let start = Instant::now();
let input: OutputSummary = serde_json::from_reader(&mut File::open(&self.input)?)?;
std::fs::create_dir_all(&self.output)?;
let mut hb = Handlebars::new();
hb.set_strict_mode(true);
handlebars_helper!(capability_str: |capability: Capability| {
capability.to_string()
});
handlebars_helper!(capability_target_list: |capability: Capability, map: ProtocolToClientMap, packages: HashMap<Hash, PackageContents>| {
let Capability::Protocol(protocol_name) = capability;
let mut result = Vec::new();
write!(&mut result, r#"<ul class="capability-targets">"#).unwrap();
if let Some(hash_to_coverage) = map.get(&protocol_name) {
for (hash, component_to_coverage) in hash_to_coverage.iter() {
let package_url = packages.get(hash).unwrap().url.to_string();
for component in component_to_coverage.iter() {
let uri = package_page_url(&hash.to_string());
write!(&mut result, "<li><a href='{uri}'>{package_url}#meta/{component}</a></li>").unwrap();
}
}
}
write!(&mut result, "</ul>").unwrap();
String::from_utf8(result).unwrap()
});
hb.register_template_string("base", include_str!("../templates/base_template.html.hbs"))?;
hb.register_template_string("index", include_str!("../templates/index.html.hbs"))?;
hb.register_template_string("package", include_str!("../templates/package.html.hbs"))?;
hb.register_template_string("content", include_str!("../templates/content.html.hbs"))?;
hb.register_helper("package_link", Box::new(package_link_helper));
hb.register_helper("content_link", Box::new(content_link_helper));
hb.register_helper("capability_str", Box::new(capability_str));
hb.register_helper("capability_target_list", Box::new(capability_target_list));
render_page(
&hb,
BaseTemplateArgs {
page_title: "Home",
css_path: "style.css",
root_link: "",
body_content: &render_index_contents(&hb, &input)?,
},
self.output.join("index.html"),
)?;
*RENDER_PATH.lock().unwrap() = "../".to_string();
std::fs::create_dir_all(self.output.join("packages"))?;
input
.packages
.par_iter()
.map(|(package_hash, package)| -> Result<()> {
let data =
(package.url.clone(), package, &input.protocol_to_client, &input.packages);
let body_content = hb.render("package", &data).context("rendering package")?;
render_page(
&hb,
BaseTemplateArgs {
page_title: &format!("Package: {}", package.url),
css_path: "../style.css",
root_link: "../",
body_content: &body_content,
},
self.output.join("packages").join(format!(
"{}.html",
simplify_name_for_linking(&package_hash.to_string())
)),
)?;
Ok(())
})
.collect::<Result<Vec<_>>>()?;
std::fs::create_dir_all(self.output.join("contents"))?;
input
.contents
.par_iter()
.map(|item| -> Result<()> {
let mut files = match item.1 {
FileInfo::Elf(elf) => elf
.source_file_references
.iter()
.map(|idx| input.files[idx].source_path.clone())
.collect::<Vec<_>>(),
_ => vec![],
};
files.sort();
let with_files = (item.0, item.1, files);
let body_content =
hb.render("content", &with_files).context("rendering content")?;
render_page(
&hb,
BaseTemplateArgs {
page_title: &format!("File content: {}", item.0),
css_path: "../style.css",
root_link: "../",
body_content: &body_content,
},
self.output
.join("contents")
.join(format!("{}.html", simplify_name_for_linking(&item.0.to_string()))),
)?;
Ok(())
})
.collect::<Result<Vec<_>>>()?;
*RENDER_PATH.lock().unwrap() = "".to_string();
File::create(self.output.join("style.css"))
.context("open style.css")?
.write_all(include_bytes!("../templates/style.css"))
.context("write style.css")?;
println!("Created site at {:?} in {:?}", self.output, Instant::now() - start);
Ok(())
}
}
#[derive(Serialize)]
struct BaseTemplateArgs<'a> {
page_title: &'a str,
css_path: &'a str,
root_link: &'a str,
body_content: &'a str,
}
fn render_page(
hb: &Handlebars<'_>,
args: BaseTemplateArgs<'_>,
out_path: impl AsRef<Path> + std::fmt::Debug,
) -> Result<()> {
println!("..rendering {:?}", out_path);
hb.render_to_write("base", &args, File::create(out_path)?)?;
Ok(())
}
fn render_index_contents(hb: &Handlebars<'_>, output: &OutputSummary) -> Result<String> {
Ok(hb.render("index", output)?)
}
static RENDER_PATH: LazyLock<Mutex<String>> = LazyLock::new(|| Mutex::new("".to_string()));
fn simplify_name_for_linking(name: &str) -> String {
let mut ret = String::with_capacity(name.len());
let replace_chars = ".-/\\:";
let mut replaced = false;
for ch in name.chars() {
if replace_chars.contains(ch) {
if replaced {
continue;
}
ret.push('_');
replaced = true;
} else {
replaced = false;
ret.push(ch);
}
}
ret
}
fn package_link_helper(
h: &Helper<'_, '_>,
_: &Handlebars<'_>,
_: &handlebars::Context,
_: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
) -> HelperResult {
let input_name = if let Some(name) = h.param(0) {
name.value().as_str().ok_or_else(|| RenderError::new("Value is not a non-empty string"))?
} else {
return Err(RenderError::new("Helper requires one param"));
};
out.write(&package_page_url(input_name))?;
Ok(())
}
fn package_page_url(package_hash: impl AsRef<str>) -> String {
format!(
"{}packages/{}.html",
*RENDER_PATH.lock().unwrap(),
simplify_name_for_linking(package_hash.as_ref())
)
}
fn content_link_helper(
h: &Helper<'_, '_>,
_: &Handlebars<'_>,
_: &handlebars::Context,
_: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
) -> HelperResult {
let input_name = if let Some(name) = h.param(0) {
name.value().as_str().ok_or_else(|| RenderError::new("Value is not a non-empty string"))?
} else {
return Err(RenderError::new("Helper requires one param"));
};
out.write(&format!(
"{}contents/{}.html",
*RENDER_PATH.lock().unwrap(),
simplify_name_for_linking(input_name)
))?;
Ok(())
}