diff --git a/resources/select.html.jinja b/resources/select.html.jinja new file mode 100644 index 0000000000..8d6dfd0bed --- /dev/null +++ b/resources/select.html.jinja @@ -0,0 +1,48 @@ +
+ + +
+ + diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index d75178608e..4831d40390 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -8,16 +8,18 @@ publish = false anyhow = "1.0.93" basic-toml = "0.1.9" chrono = "0.4.38" -clap = { version = "4.5.20", features = ["derive", "wrap_help"] } +clap = { version = "4.5.20", features = ["derive", "wrap_help"] } console = "0.15.10" csv = "1.3.1" env_logger = "0.11.5" esp-metadata = { path = "../esp-metadata", features = ["clap"] } +kuchikiki = "0.8.2" log = "0.4.22" minijinja = "2.5.0" -semver = { version = "1.0.23", features = ["serde"] } +reqwest = { version = "0.12.12", features = ["blocking", "json", "native-tls-vendored"] } +semver = { version = "1.0.23", features = ["serde"] } serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.70" -strum = { version = "0.26.3", features = ["derive"] } +strum = { version = "0.26.3", features = ["derive"] } toml_edit = "0.22.22" walkdir = "2.5.0" diff --git a/xtask/src/documentation.rs b/xtask/src/documentation.rs index 2cb403a358..9b13de692d 100644 --- a/xtask/src/documentation.rs +++ b/xtask/src/documentation.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashSet, fs, path::{Path, PathBuf}, }; @@ -6,7 +7,9 @@ use std::{ use anyhow::{ensure, Context as _, Result}; use clap::ValueEnum; use esp_metadata::Config; +use kuchikiki::traits::*; use minijinja::Value; +use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use crate::{cargo::CargoArgsBuilder, Chip, Package}; @@ -14,10 +17,16 @@ use crate::{cargo::CargoArgsBuilder, Chip, Package}; // ---------------------------------------------------------------------------- // Build Documentation +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] +struct Manifest { + versions: HashSet, +} + pub fn build_documentation( workspace: &Path, packages: &mut [Package], chips: &mut [Chip], + base_url: Option, ) -> Result<()> { let output_path = workspace.join("docs"); @@ -32,6 +41,23 @@ pub fn build_documentation( continue; } + // Download the manifest from the documentation server if able, + // otherwise just create a default (empty) manifest: + let mut manifest_url = base_url + .clone() + .unwrap_or_default() + .trim_end_matches('/') + .to_string(); + manifest_url.push_str(&format!("/{package}/manifest.json")); + + let mut manifest = match reqwest::blocking::get(manifest_url) { + Ok(resp) => resp.json::()?, + Err(err) => { + log::warn!("Unable to fetch package manifest: {err}"); + Manifest::default() + } + }; + // If the package does not have chip features, then just ignore // whichever chip(s) were specified as arguments: let chips = if package.has_chip_features() { @@ -48,6 +74,11 @@ pub fn build_documentation( vec![] }; + // Update the package manifest to include the latest version: + let version = crate::package_version(workspace, *package)?; + manifest.versions.insert(version.clone()); + + // Build the documentation for the package: if chips.is_empty() { build_documentation_for_package(workspace, package, None)?; } else { @@ -55,6 +86,15 @@ pub fn build_documentation( build_documentation_for_package(workspace, package, Some(chip))?; } } + + // Write out the package manifest JSON file: + fs::write( + output_path.join(package.to_string()).join("manifest.json"), + serde_json::to_string(&manifest)?, + )?; + + // Patch the generated documentation to include a select box for the version: + patch_documentation_index_for_package(workspace, package, &version, &base_url)?; } Ok(()) @@ -230,6 +270,56 @@ fn apply_feature_rules(package: &Package, config: &Config) -> Vec { features } +fn patch_documentation_index_for_package( + workspace: &Path, + package: &Package, + version: &semver::Version, + base_url: &Option, +) -> Result<()> { + let package_name = package.to_string().replace('-', "_"); + let package_path = workspace.join("docs").join(package.to_string()); + let version_path = package_path.join(version.to_string()); + + let mut index_paths = Vec::new(); + + if package.chip_features_matter() { + for chip_path in fs::read_dir(version_path)? { + let chip_path = chip_path?.path(); + if chip_path.is_dir() { + let path = chip_path.join(&package_name).join("index.html"); + index_paths.push((version.clone(), path)); + } + } + } else { + let path = version_path.join(&package_name).join("index.html"); + index_paths.push((version.clone(), path)); + } + + for (version, index_path) in index_paths { + let html = fs::read_to_string(&index_path)?; + let document = kuchikiki::parse_html().one(html); + + let elem = document + .select_first(".sidebar-crate") + .expect("Unable to select '.sidebar-crate' element in HTML"); + + let base_url = base_url.clone().unwrap_or_default(); + let resources_path = workspace.join("resources"); + let html = render_template( + &resources_path, + "select.html.jinja", + minijinja::context! { base_url => base_url, package => package, version => version }, + )?; + + let node = elem.as_node(); + node.append(kuchikiki::parse_html().one(html)); + + fs::write(&index_path, document.to_string())?; + } + + Ok(()) +} + // ---------------------------------------------------------------------------- // Build Documentation Index @@ -293,13 +383,16 @@ pub fn build_documentation_index(workspace: &Path, packages: &mut [Package]) -> chips.sort(); let meta = generate_documentation_meta_for_package(workspace, *package, &chips)?; - render_template( - "package_index.html.jinja", - "index.html", - &version_path, + + // Render the template to HTML and write it out to the desired path: + let html = render_template( &resources_path, + "package_index.html.jinja", minijinja::context! { metadata => meta }, )?; + let path = version_path.join("index.html"); + fs::write(&path, html).context(format!("Failed to write index.html"))?; + log::info!("Created {}", path.display()); } } @@ -312,13 +405,15 @@ pub fn build_documentation_index(workspace: &Path, packages: &mut [Package]) -> let meta = generate_documentation_meta_for_index(&workspace)?; - render_template( - "index.html.jinja", - "index.html", - &docs_path, + // Render the template to HTML and write it out to the desired path: + let html = render_template( &resources_path, + "index.html.jinja", minijinja::context! { metadata => meta }, )?; + let path = docs_path.join("index.html"); + fs::write(&path, html).context(format!("Failed to write index.html"))?; + log::info!("Created {}", path.display()); Ok(()) } @@ -381,13 +476,7 @@ fn generate_documentation_meta_for_index(workspace: &Path) -> Result> // ---------------------------------------------------------------------------- // Helper Functions -fn render_template( - template: &str, - name: &str, - path: &Path, - resources: &Path, - ctx: C, -) -> Result<()> +fn render_template(resources: &Path, template: &str, ctx: C) -> Result where C: serde::Serialize, { @@ -400,10 +489,5 @@ where let tmpl = env.get_template(template)?; let html = tmpl.render(ctx)?; - // Write out the rendered HTML to the desired path: - let path = path.join(name); - fs::write(&path, html).context(format!("Failed to write {name}"))?; - log::info!("Created {}", path.display()); - - Ok(()) + Ok(html) } diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index b47f830da9..1d485f0bfa 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -30,6 +30,7 @@ pub mod firmware; Display, EnumIter, ValueEnum, + serde::Deserialize, serde::Serialize, )] #[serde(rename_all = "kebab-case")] diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 93eecca4f8..f24be0100d 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -89,6 +89,9 @@ struct BuildDocumentationArgs { /// Chip(s) to build documentation for. #[arg(long, value_enum, value_delimiter = ',', default_values_t = Chip::iter())] chips: Vec, + /// Base URL of the deployed documentation. + #[arg(long)] + base_url: Option, } #[derive(Debug, Args)] @@ -474,7 +477,12 @@ fn tests(workspace: &Path, args: TestArgs, action: CargoAction) -> Result<()> { } fn build_documentation(workspace: &Path, mut args: BuildDocumentationArgs) -> Result<()> { - xtask::documentation::build_documentation(workspace, &mut args.packages, &mut args.chips) + xtask::documentation::build_documentation( + workspace, + &mut args.packages, + &mut args.chips, + args.base_url, + ) } fn build_documentation_index(