Skip to content

Commit

Permalink
Patch the generated documentation to include a version selector
Browse files Browse the repository at this point in the history
  • Loading branch information
jessebraham committed Feb 12, 2025
1 parent 775eff6 commit 8c9d030
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 25 deletions.
48 changes: 48 additions & 0 deletions resources/select.html.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<div style="margin-top: 0.5rem; width: 100%">
<label for="version-select">Version:</label>
<select id="version-select" name="version-select" style="text-align: center; width: 100%">
<option value="{{ version }}" selected="selected">{{ version }}</option>
</select>
</div>

<script type="text/javascript">
const select = document.querySelector("#version-select");
select.addEventListener("change", (e) => {
const selected = select.value;
// Replace the existing version number in the URL with the newly
// selected version:
let href = window.location.href;
href = href.replace(/[\d]+\.[\d]+\.[\d]+[^\/]*/g, selected);
// Redirect to the new URL:
window.location.href = href;
});
document.addEventListener("DOMContentLoaded", (e) => {
const selected = select.value;
// Remove any options currently present in the select box:
for (let child of select.children) {
child.remove();
}
// Load the manifest JSON file and re-populate the select box with new
// options for all available versions:
const manifestUrl = "{{ base_url }}/{{ package }}/manifest.json";
fetch(manifestUrl)
.then((res) => res.json())
.then(({ versions }) => {
for (let version of versions) {
let option = document.createElement("option");
option.text = version;
option.value = version;
select.appendChild(option);
}
select.value = selected;
})
.catch((err) => console.error(err));
});
</script>
8 changes: 5 additions & 3 deletions xtask/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
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"
126 changes: 105 additions & 21 deletions xtask/src/documentation.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
use std::{
collections::HashSet,
fs,
path::{Path, PathBuf},
};

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};

// ----------------------------------------------------------------------------
// Build Documentation

#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
struct Manifest {
versions: HashSet<semver::Version>,
}

pub fn build_documentation(
workspace: &Path,
packages: &mut [Package],
chips: &mut [Chip],
base_url: Option<String>,
) -> Result<()> {
let output_path = workspace.join("docs");

Expand All @@ -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::<Manifest>()?,
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() {
Expand All @@ -48,13 +74,27 @@ 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());

// Write out the package manifest JSON file:
fs::write(
output_path.join(package.to_string()).join("manifest.json"),
serde_json::to_string(&manifest)?,
)?;

// Build the documentation for the package:
if chips.is_empty() {
build_documentation_for_package(workspace, package, None)?;
} else {
for chip in chips {
build_documentation_for_package(workspace, package, Some(chip))?;
}
}

// Patch the generated documentation to include a select box for the version:
patch_documentation_index_for_package(workspace, package, &version, &base_url)?;
}

Ok(())
Expand Down Expand Up @@ -230,6 +270,56 @@ fn apply_feature_rules(package: &Package, config: &Config) -> Vec<String> {
features
}

fn patch_documentation_index_for_package(
workspace: &Path,
package: &Package,
version: &semver::Version,
base_url: &Option<String>,
) -> 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

Expand Down Expand Up @@ -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());
}
}

Expand All @@ -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(())
}
Expand Down Expand Up @@ -381,13 +476,7 @@ fn generate_documentation_meta_for_index(workspace: &Path) -> Result<Vec<Value>>
// ----------------------------------------------------------------------------
// Helper Functions

fn render_template<C>(
template: &str,
name: &str,
path: &Path,
resources: &Path,
ctx: C,
) -> Result<()>
fn render_template<C>(resources: &Path, template: &str, ctx: C) -> Result<String>
where
C: serde::Serialize,
{
Expand All @@ -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)
}
1 change: 1 addition & 0 deletions xtask/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub mod firmware;
Display,
EnumIter,
ValueEnum,
serde::Deserialize,
serde::Serialize,
)]
#[serde(rename_all = "kebab-case")]
Expand Down
10 changes: 9 additions & 1 deletion xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Chip>,
/// Base URL of the deployed documentation.
#[arg(long)]
base_url: Option<String>,
}

#[derive(Debug, Args)]
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 8c9d030

Please sign in to comment.