| use std::fmt::Display; | |
| use std::path::PathBuf; | |
| use askama::Template; | |
| use rustc_data_structures::fx::FxIndexMap; | |
| use super::static_files::{STATIC_FILES, StaticFiles}; | |
| use crate::externalfiles::ExternalHtml; | |
| use crate::html::render::{StylePath, ensure_trailing_slash}; | |
| #[cfg(test)] | |
| mod tests; | |
| pub(crate) struct Layout { | |
| pub(crate) logo: String, | |
| pub(crate) favicon: String, | |
| pub(crate) external_html: ExternalHtml, | |
| pub(crate) default_settings: FxIndexMap<String, String>, | |
| pub(crate) krate: String, | |
| pub(crate) krate_version: String, | |
| /// The given user css file which allow to customize the generated | |
| /// documentation theme. | |
| pub(crate) css_file_extension: Option<PathBuf>, | |
| /// If true, then scrape-examples.js will be included in the output HTML file | |
| pub(crate) scrape_examples_extension: bool, | |
| } | |
| pub(crate) struct Page<'a> { | |
| pub(crate) title: &'a str, | |
| pub(crate) short_title: &'a str, | |
| pub(crate) css_class: &'a str, | |
| pub(crate) root_path: &'a str, | |
| pub(crate) static_root_path: Option<&'a str>, | |
| pub(crate) description: &'a str, | |
| pub(crate) resource_suffix: &'a str, | |
| pub(crate) rust_logo: bool, | |
| } | |
| impl Page<'_> { | |
| pub(crate) fn get_static_root_path(&self) -> String { | |
| match self.static_root_path { | |
| Some(s) => s.to_string(), | |
| None => format!("{}static.files/", self.root_path), | |
| } | |
| } | |
| } | |
| #[derive(Template)] | |
| #[template(path = "page.html")] | |
| struct PageLayout<'a> { | |
| static_root_path: String, | |
| page: &'a Page<'a>, | |
| layout: &'a Layout, | |
| files: &'static StaticFiles, | |
| themes: Vec<String>, | |
| sidebar: String, | |
| content: String, | |
| rust_channel: &'static str, | |
| pub(crate) rustdoc_version: &'a str, | |
| // same as layout.krate, except on top-level pages like | |
| // Settings, Help, All Crates, and About Scraped Examples, | |
| // where these things instead give Rustdoc name and version. | |
| // | |
| // These are separate from the variables used for the search | |
| // engine, because "Rustdoc" isn't necessarily a crate in | |
| // the current workspace. | |
| display_krate: &'a str, | |
| display_krate_with_trailing_slash: String, | |
| display_krate_version_number: &'a str, | |
| display_krate_version_extra: &'a str, | |
| } | |
| impl PageLayout<'_> { | |
| /// See [`may_remove_crossorigin`]. | |
| fn static_root_path_may_remove_crossorigin(&self) -> bool { | |
| may_remove_crossorigin(&self.static_root_path) | |
| } | |
| } | |
| pub(crate) use crate::html::render::sidebar::filters; | |
| pub(crate) fn render<T: Display, S: Display>( | |
| layout: &Layout, | |
| page: &Page<'_>, | |
| sidebar: S, | |
| t: T, | |
| style_files: &[StylePath], | |
| ) -> String { | |
| let rustdoc_version = rustc_interface::util::version_str!().unwrap_or("unknown version"); | |
| let (display_krate, display_krate_version, display_krate_with_trailing_slash) = | |
| if page.root_path == "./" { | |
| // top level pages use Rust branding | |
| ("Rustdoc", rustdoc_version, String::new()) | |
| } else { | |
| let display_krate_with_trailing_slash = | |
| ensure_trailing_slash(&layout.krate).to_string(); | |
| (&layout.krate[..], &layout.krate_version[..], display_krate_with_trailing_slash) | |
| }; | |
| let static_root_path = page.get_static_root_path(); | |
| // bootstrap passes in parts of the version separated by tabs, but other stuff might use spaces | |
| let (display_krate_version_number, display_krate_version_extra) = | |
| display_krate_version.split_once([' ', '\t']).unwrap_or((display_krate_version, "")); | |
| let mut themes: Vec<String> = style_files.iter().map(|s| s.basename().unwrap()).collect(); | |
| themes.sort(); | |
| let content = t.to_string(); // Note: This must happen before making the sidebar. | |
| let sidebar = sidebar.to_string(); | |
| PageLayout { | |
| static_root_path, | |
| page, | |
| layout, | |
| files: &STATIC_FILES, | |
| themes, | |
| sidebar, | |
| content, | |
| display_krate, | |
| display_krate_with_trailing_slash, | |
| display_krate_version_number, | |
| display_krate_version_extra, | |
| rust_channel: *crate::clean::utils::RUSTDOC_VERSION, | |
| rustdoc_version, | |
| } | |
| .render() | |
| .unwrap() | |
| } | |
| pub(crate) fn redirect(url: &str) -> String { | |
| // <script> triggers a redirect before refresh, so this is fine. | |
| format!( | |
| r##"<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta http-equiv="refresh" content="0;URL={url}"> | |
| <title>Redirection</title> | |
| </head> | |
| <body> | |
| <p>Redirecting to <a href="{url}">{url}</a>...</p> | |
| <script>location.replace("{url}" + location.search + location.hash);</script> | |
| </body> | |
| </html>"##, | |
| ) | |
| } | |
| /// Conservatively determines if `href` is relative to the current origin, | |
| /// so that `crossorigin` may be safely removed from `<link>` elements. | |
| pub(crate) fn may_remove_crossorigin(href: &str) -> bool { | |
| // Reject scheme-relative URLs (`//example.com/`). | |
| if href.starts_with("//") { | |
| return false; | |
| } | |
| // URL is interpreted as having a scheme iff: it starts with an ascii alpha, and only | |
| // contains ascii alphanumeric or `+` `-` `.` up to the `:`. | |
| // https://url.spec.whatwg.org/#url-parsing | |
| let has_scheme = href.split_once(':').is_some_and(|(scheme, _rest)| { | |
| let mut chars = scheme.chars(); | |
| chars.next().is_some_and(|c| c.is_ascii_alphabetic()) | |
| && chars.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') | |
| }); | |
| // Reject anything with a scheme (`http:`, etc.). | |
| !has_scheme | |
| } |