diff --git a/Cargo.lock b/Cargo.lock index 2618450bd..e35d61cf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,6 +831,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -966,28 +975,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "chrono-tz" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" -dependencies = [ - "chrono", - "chrono-tz-build", - "phf 0.11.2", -] - -[[package]] -name = "chrono-tz-build" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" -dependencies = [ - "parse-zoneinfo", - "phf 0.11.2", - "phf_codegen 0.11.2", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -1659,6 +1646,7 @@ dependencies = [ "rayon", "regex", "reqwest", + "rinja", "rusqlite", "rustwide", "semver", @@ -1677,7 +1665,6 @@ dependencies = [ "strum", "syntect", "tempfile", - "tera", "test-case", "thiserror", "thread_local", @@ -2899,30 +2886,6 @@ dependencies = [ "gix-validate", ] -[[package]] -name = "globset" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", -] - -[[package]] -name = "globwalk" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" -dependencies = [ - "bitflags 2.6.0", - "ignore", - "walkdir", -] - [[package]] name = "grass" version = "0.13.3" @@ -3387,22 +3350,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "ignore" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata 0.4.7", - "same-file", - "walkdir", - "winapi-util", -] - [[package]] name = "imara-diff" version = "0.1.6" @@ -4044,6 +3991,18 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "once_map" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa7085055bbe9c8edbd982048dbcf8181794d4a81cb04a11931673e63cc18dc6" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "parking_lot", + "stable_deref_trait", +] + [[package]] name = "onig" version = "6.4.0" @@ -4173,15 +4132,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "parse-zoneinfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] - [[package]] name = "paste" version = "1.0.15" @@ -4209,51 +4159,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.70", -] - -[[package]] -name = "pest_meta" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "phf" version = "0.8.0" @@ -4304,16 +4209,6 @@ dependencies = [ "phf_shared 0.10.0", ] -[[package]] -name = "phf_codegen" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", -] - [[package]] name = "phf_generator" version = "0.8.0" @@ -4952,6 +4847,44 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rinja" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d47a46d7729e891c8accf260e9daa02ae6d570aa2a94fb1fb27eb5364a2323" +dependencies = [ + "humansize", + "num-traits", + "percent-encoding", + "rinja_derive", +] + +[[package]] +name = "rinja_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dae9afe59d58ed8d988d67d1945f3638125d2fd2104058399382e11bd3ea2a" +dependencies = [ + "basic-toml", + "mime", + "mime_guess", + "once_map", + "proc-macro2", + "quote", + "rinja_parser", + "serde", + "syn 2.0.70", +] + +[[package]] +name = "rinja_parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1771c78cd5d3b1646ef8d8f2ed100db936e8b291d3cc06e92a339ff346858c" +dependencies = [ + "nom", +] + [[package]] name = "roxmltree" version = "0.14.1" @@ -6096,28 +6029,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "tera" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" -dependencies = [ - "chrono", - "chrono-tz", - "globwalk", - "humansize", - "lazy_static", - "percent-encoding", - "pest", - "pest_derive", - "rand 0.8.5", - "regex", - "serde", - "serde_json", - "slug", - "unic-segment", -] - [[package]] name = "test-case" version = "3.3.1" @@ -6547,12 +6458,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - [[package]] name = "uluru" version = "3.1.0" @@ -6571,56 +6476,6 @@ dependencies = [ "libc", ] -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" -dependencies = [ - "unic-ucd-segment", -] - -[[package]] -name = "unic-ucd-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicase" version = "2.7.0" diff --git a/Cargo.toml b/Cargo.toml index a9fbc1bab..59a395683 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,7 @@ tempfile = "3.1.0" fn-error-context = "0.2.0" # Templating -tera = { version = "1.5.0", features = ["builtins"] } +rinja = "0.2" walkdir = "2" # Date and Time utilities diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile index f9ec2c4ad..cce7ccf16 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -55,7 +55,7 @@ COPY build.rs build.rs RUN touch build.rs COPY src src/ RUN find src -name "*.rs" -exec touch {} \; -COPY templates/style templates/style +COPY templates templates/ COPY vendor vendor/ COPY assets assets/ COPY .sqlx .sqlx/ diff --git a/src/db/types.rs b/src/db/types.rs index 17140c595..ea0217dfd 100644 --- a/src/db/types.rs +++ b/src/db/types.rs @@ -45,6 +45,16 @@ impl sqlx::postgres::PgHasArrayType for BuildStatus { } } +impl<'a> PartialEq<&'a str> for BuildStatus { + fn eq(&self, other: &&str) -> bool { + match self { + Self::Success => *other == "success", + Self::Failure => *other == "failure", + Self::InProgress => *other == "in_progress", + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/docbuilder/limits.rs b/src/docbuilder/limits.rs index 1dcdcb797..6b653b4b9 100644 --- a/src/docbuilder/limits.rs +++ b/src/docbuilder/limits.rs @@ -6,11 +6,11 @@ const GB: usize = 1024 * 1024 * 1024; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub(crate) struct Limits { - memory: usize, - targets: usize, - timeout: Duration, - networking: bool, - max_log_size: usize, + pub memory: usize, + pub targets: usize, + pub timeout: Duration, + pub networking: bool, + pub max_log_size: usize, } impl Limits { diff --git a/src/registry_api.rs b/src/registry_api.rs index 47d403179..5d5ada084 100644 --- a/src/registry_api.rs +++ b/src/registry_api.rs @@ -4,6 +4,7 @@ use chrono::{DateTime, Utc}; use reqwest::header::{HeaderValue, ACCEPT, USER_AGENT}; use semver::Version; use serde::{Deserialize, Serialize}; +use std::fmt; use tracing::instrument; use url::Url; @@ -59,6 +60,15 @@ pub enum OwnerKind { Team, } +impl fmt::Display for OwnerKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::User => f.write_str("user"), + Self::Team => f.write_str("team"), + } + } +} + impl RegistryApi { pub fn new(api_base: Url, max_retries: u32) -> Result { let headers = vec![ diff --git a/src/utils/html.rs b/src/utils/html.rs index 2f015ae63..c84867e15 100644 --- a/src/utils/html.rs +++ b/src/utils/html.rs @@ -1,7 +1,8 @@ -use crate::web::page::TemplateData; +use crate::web::page::templates::{Body, Head, Topbar, Vendored}; +use crate::web::rustdoc::RustdocPage; use lol_html::element; use lol_html::errors::RewritingError; -use tera::Context; +use rinja::Template; /// Rewrite a rustdoc page to have the docs.rs topbar /// @@ -12,17 +13,15 @@ use tera::Context; pub(crate) fn rewrite_lol( html: &[u8], max_allowed_memory_usage: usize, - ctx: Context, - templates: &TemplateData, + data: &RustdocPage, ) -> Result, RewritingError> { use lol_html::html_content::{ContentType, Element}; use lol_html::{HtmlRewriter, MemorySettings, Settings}; - let templates = &templates.templates; - let tera_head = templates.render("rustdoc/head.html", &ctx).unwrap(); - let tera_vendored_css = templates.render("rustdoc/vendored.html", &ctx).unwrap(); - let tera_body = templates.render("rustdoc/body.html", &ctx).unwrap(); - let tera_rustdoc_topbar = templates.render("rustdoc/topbar.html", &ctx).unwrap(); + let head_html = Head::new(data).render().unwrap(); + let vendored_html = Vendored::new(data).render().unwrap(); + let body_html = Body::new(data).render().unwrap(); + let topbar_html = Topbar::new(data).render().unwrap(); // Before: ... rustdoc content ... // After: @@ -46,12 +45,12 @@ pub(crate) fn rewrite_lol( rustdoc_body_class.set_attribute("tabindex", "-1")?; // Change the `body` to a `div` rustdoc_body_class.set_tag_name("div")?; - // Prepend the tera content - rustdoc_body_class.prepend(&tera_body, ContentType::Html); + // Prepend the rinja content + rustdoc_body_class.prepend(&body_html, ContentType::Html); // Wrap the transformed body and topbar into a element rustdoc_body_class.before(r#""#, ContentType::Html); // Insert the topbar outside of the rustdoc div - rustdoc_body_class.before(&tera_rustdoc_topbar, ContentType::Html); + rustdoc_body_class.before(&topbar_html, ContentType::Html); // Finalize body with rustdoc_body_class.after("", ContentType::Html); @@ -62,7 +61,7 @@ pub(crate) fn rewrite_lol( element_content_handlers: vec![ // Append `style.css` stylesheet after all head elements. element!("head", |head: &mut Element| { - head.append(&tera_head, ContentType::Html); + head.append(&head_html, ContentType::Html); Ok(()) }), element!("body", body_handler), @@ -81,7 +80,7 @@ pub(crate) fn rewrite_lol( element!( "link[rel='stylesheet'][href*='rustdoc-']", |rustdoc_css: &mut Element| { - rustdoc_css.before(&tera_vendored_css, ContentType::Html); + rustdoc_css.before(&vendored_html, ContentType::Html); Ok(()) } ), diff --git a/src/web/build_details.rs b/src/web/build_details.rs index 6e5711c8c..4bfbe5718 100644 --- a/src/web/build_details.rs +++ b/src/web/build_details.rs @@ -2,10 +2,11 @@ use crate::{ db::types::BuildStatus, impl_axum_webpage, web::{ + crate_details::CrateDetails, error::{AxumNope, AxumResult}, extractors::{DbConnection, Path}, file::File, - MetaData, + filters, MetaData, }, AsyncStorage, Config, }; @@ -13,11 +14,12 @@ use anyhow::Context as _; use axum::{extract::Extension, response::IntoResponse}; use chrono::{DateTime, Utc}; use futures_util::TryStreamExt; +use rinja::Template; use semver::Version; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::sync::Arc; -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct BuildDetails { id: i32, rustc_version: Option, @@ -28,17 +30,33 @@ pub(crate) struct BuildDetails { errors: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Template)] +#[template(path = "crate/build_details.html")] +#[derive(Debug, Clone, PartialEq, Eq)] struct BuildDetailsPage { metadata: MetaData, build_details: BuildDetails, - use_direct_platform_links: bool, all_log_filenames: Vec, current_filename: Option, + csp_nonce: String, } -impl_axum_webpage! { - BuildDetailsPage = "crate/build_details.html", +impl_axum_webpage! { BuildDetailsPage } + +// Used for template rendering. +impl BuildDetailsPage { + pub(crate) fn krate(&self) -> Option<&CrateDetails> { + None + } + pub(crate) fn permalink_path(&self) -> &str { + "" + } + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + Some(&self.metadata) + } + pub(crate) fn use_direct_platform_links(&self) -> bool { + true + } } #[derive(Clone, Deserialize, Debug)] @@ -123,9 +141,9 @@ pub(crate) async fn build_details_handler( output, errors: row.errors, }, - use_direct_platform_links: true, all_log_filenames, current_filename, + csp_nonce: String::new(), } .into_response()) } diff --git a/src/web/builds.rs b/src/web/builds.rs index 51b46ae50..45a1b6e3a 100644 --- a/src/web/builds.rs +++ b/src/web/builds.rs @@ -9,9 +9,10 @@ use crate::{ impl_axum_webpage, utils::spawn_blocking, web::{ + crate_details::CrateDetails, error::AxumResult, extractors::{DbConnection, Path}, - match_version, MetaData, ReqVersion, + filters, match_version, MetaData, ReqVersion, }, BuildQueue, Config, }; @@ -26,12 +27,11 @@ use axum_extra::{ use chrono::{DateTime, Utc}; use constant_time_eq::constant_time_eq; use http::StatusCode; +use rinja::Template; use semver::Version; -use serde::Serialize; -use serde_json::json; use std::sync::Arc; -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct Build { id: i32, rustc_version: Option, @@ -41,17 +41,32 @@ pub(crate) struct Build { errors: Option, } -#[derive(Debug, Clone, Serialize)] +#[derive(Template)] +#[template(path = "crate/builds.html")] +#[derive(Debug, Clone)] struct BuildsPage { metadata: MetaData, builds: Vec, limits: Limits, canonical_url: CanonicalUrl, - use_direct_platform_links: bool, + csp_nonce: String, } -impl_axum_webpage! { - BuildsPage = "crate/builds.html", +impl_axum_webpage! { BuildsPage } + +impl BuildsPage { + pub(crate) fn krate(&self) -> Option<&CrateDetails> { + None + } + pub(crate) fn permalink_path(&self) -> &str { + "" + } + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + Some(&self.metadata) + } + pub(crate) fn use_direct_platform_links(&self) -> bool { + true + } } pub(crate) async fn build_list_handler( @@ -75,7 +90,7 @@ pub(crate) async fn build_list_handler( builds: get_builds(&mut conn, &name, &version).await?, limits: Limits::for_crate(&config, &mut conn, &name).await?, canonical_url: CanonicalUrl::from_path(format!("/crate/{name}/latest/builds")), - use_direct_platform_links: true, + csp_nonce: String::new(), } .into_response()) } @@ -218,7 +233,7 @@ pub(crate) async fn build_trigger_rebuild_handler( .await .map_err(|e| JsonAxumNope(e.into()))?; - Ok((StatusCode::CREATED, Json(json!({})))) + Ok((StatusCode::CREATED, Json(serde_json::json!({})))) } async fn get_builds( @@ -606,9 +621,9 @@ mod tests { let values: Vec<_> = values.iter().map(|v| &**v).collect(); dbg!(&values); - assert!(values.contains(&"6 GB")); + assert!(values.contains(&"6.44 GB")); assert!(values.contains(&"2 hours")); - assert!(values.contains(&"100 kB")); + assert!(values.contains(&"102.40 kB")); assert!(values.contains(&"blocked")); assert!(values.contains(&"1")); diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index 30327645b..207977359 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -1,4 +1,4 @@ -use super::{markdown, match_version, MetaData}; +use super::{match_version, MetaData}; use crate::registry_api::OwnerKind; use crate::utils::{get_correct_docsrs_style_file, report_error}; use crate::web::rustdoc::RustdocHtmlParams; @@ -11,6 +11,7 @@ use crate::{ encode_url_path, error::{AxumNope, AxumResult}, extractors::{DbConnection, Path}, + page::templates::filters, MatchedRelease, ReqVersion, }, AsyncStorage, @@ -23,24 +24,22 @@ use axum::{ use chrono::{DateTime, Utc}; use futures_util::stream::TryStreamExt; use log::warn; +use rinja::Template; use semver::Version; use serde::Deserialize; -use serde::{ser::Serializer, Serialize}; use serde_json::Value; use std::sync::Arc; // TODO: Add target name and versions -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] pub(crate) struct CrateDetails { - name: String, - pub version: Version, - description: Option, - owners: Vec<(String, String, OwnerKind)>, - dependencies: Option, - #[serde(serialize_with = "optional_markdown")] + pub(crate) name: String, + pub(crate) version: Version, + pub(crate) description: Option, + pub(crate) owners: Vec<(String, String, OwnerKind)>, + pub(crate) dependencies: Option, readme: Option, - #[serde(serialize_with = "optional_markdown")] rustdoc: Option, // this is description_long in database release_time: Option>, build_status: BuildStatus, @@ -48,8 +47,8 @@ pub(crate) struct CrateDetails { last_successful_build: Option, pub rustdoc_status: Option, pub archive_storage: bool, - repository_url: Option, - homepage_url: Option, + pub repository_url: Option, + pub homepage_url: Option, keywords: Option, have_examples: Option, // need to check this manually pub target_name: Option, @@ -57,19 +56,19 @@ pub(crate) struct CrateDetails { repository_metadata: Option, pub(crate) metadata: MetaData, is_library: Option, - license: Option, + pub(crate) license: Option, pub(crate) documentation_url: Option, - total_items: Option, - documented_items: Option, - total_items_needing_examples: Option, - items_with_examples: Option, + pub(crate) total_items: Option, + pub(crate) documented_items: Option, + pub(crate) total_items_needing_examples: Option, + pub(crate) items_with_examples: Option, /// Database id for this crate pub(crate) crate_id: i32, /// Database id for this release pub(crate) release_id: i32, } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] struct RepositoryMetadata { stars: i32, forks: i32, @@ -77,17 +76,7 @@ struct RepositoryMetadata { name: Option, } -fn optional_markdown(markdown: &Option, serializer: S) -> Result -where - S: Serializer, -{ - markdown - .as_ref() - .map(|markdown| markdown::render(markdown)) - .serialize(serializer) -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct Release { pub id: i32, pub version: semver::Version, @@ -421,16 +410,38 @@ pub(crate) async fn releases_for_crate( Ok(releases) } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Template)] +#[template(path = "crate/details.html")] +#[derive(Debug, Clone, PartialEq)] struct CrateDetailsPage { details: CrateDetails, + csp_nonce: String, } impl_axum_webpage! { - CrateDetailsPage = "crate/details.html", + CrateDetailsPage, cpu_intensive_rendering = true, } +// Used by templates. +impl CrateDetailsPage { + pub(crate) fn krate(&self) -> Option<&CrateDetails> { + None + } + + pub(crate) fn permalink_path(&self) -> &str { + "" + } + + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + Some(&self.details.metadata) + } + + pub(crate) fn use_direct_platform_links(&self) -> bool { + true + } +} + #[derive(Deserialize, Clone, Debug)] pub(crate) struct CrateDetailHandlerParams { name: String, @@ -467,7 +478,11 @@ pub(crate) async fn crate_details_handler( Err(e) => warn!("error fetching readme: {:?}", &e), } - let mut res = CrateDetailsPage { details }.into_response(); + let mut res = CrateDetailsPage { + details, + csp_nonce: String::new(), + } + .into_response(); res.extensions_mut() .insert::(if req_version.is_latest() { CachePolicy::ForeverInCdn @@ -477,16 +492,19 @@ pub(crate) async fn crate_details_handler( Ok(res.into_response()) } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Template)] +#[template(path = "rustdoc/releases.html")] +#[derive(Debug, Clone, PartialEq)] struct ReleaseList { releases: Vec, crate_name: String, inner_path: String, target: String, + csp_nonce: String, } impl_axum_webpage! { - ReleaseList = "rustdoc/releases.html", + ReleaseList, cache_policy = |_| CachePolicy::ForeverInCdn, cpu_intensive_rendering = true, } @@ -564,11 +582,12 @@ pub(crate) async fn get_all_releases( target, inner_path, crate_name: params.name, + csp_nonce: String::new(), }; Ok(res.into_response()) } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] struct ShortMetadata { name: String, version: Version, @@ -576,16 +595,26 @@ struct ShortMetadata { doc_targets: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize)] +impl ShortMetadata { + // Used in templates. + pub(crate) fn doc_targets(&self) -> Option<&[String]> { + Some(&self.doc_targets) + } +} + +#[derive(Template)] +#[template(path = "rustdoc/platforms.html")] +#[derive(Debug, Clone, PartialEq)] struct PlatformList { metadata: ShortMetadata, inner_path: String, use_direct_platform_links: bool, current_target: String, + csp_nonce: String, } impl_axum_webpage! { - PlatformList = "rustdoc/platforms.html", + PlatformList, cache_policy = |_| CachePolicy::ForeverInCdn, cpu_intensive_rendering = true, } @@ -652,6 +681,7 @@ pub(crate) async fn get_all_platforms_inner( inner_path: "".into(), use_direct_platform_links: is_crate_root, current_target: "".into(), + csp_nonce: String::new(), } .into_response()); } @@ -706,6 +736,7 @@ pub(crate) async fn get_all_platforms_inner( inner_path, use_direct_platform_links: is_crate_root, current_target, + csp_nonce: String::new(), } .into_response()) } @@ -1602,7 +1633,7 @@ mod tests { assert_eq!( url.contains("/target-redirect/"), should_contain_redirect, - "ajax: {ajax:?}, should_contain_redirect: {should_contain_redirect:?}", + "url: {url:?}, ajax: {ajax:?}, should_contain_redirect: {should_contain_redirect:?}", ); if !should_contain_redirect { assert_eq!(rel, ""); diff --git a/src/web/error.rs b/src/web/error.rs index 75bb51407..ae8f41403 100644 --- a/src/web/error.rs +++ b/src/web/error.rs @@ -148,6 +148,7 @@ impl IntoResponse for AxumNope { title, message, status, + csp_nonce: String::new(), } .into_response() } diff --git a/src/web/features.rs b/src/web/features.rs index 047efcc9a..8794135a0 100644 --- a/src/web/features.rs +++ b/src/web/features.rs @@ -3,31 +3,35 @@ use crate::{ impl_axum_webpage, web::{ cache::CachePolicy, + crate_details::CrateDetails, error::{AxumNope, AxumResult}, extractors::{DbConnection, Path}, + filters, headers::CanonicalUrl, match_version, MetaData, ReqVersion, }, }; use anyhow::anyhow; use axum::response::IntoResponse; -use serde::Serialize; +use rinja::Template; use std::collections::{HashMap, VecDeque}; const DEFAULT_NAME: &str = "default"; -#[derive(Debug, Clone, Serialize)] +#[derive(Template)] +#[template(path = "crate/features.html")] +#[derive(Debug, Clone)] struct FeaturesPage { metadata: MetaData, features: Option>, default_len: usize, canonical_url: CanonicalUrl, is_latest_url: bool, - use_direct_platform_links: bool, + csp_nonce: String, } impl_axum_webpage! { - FeaturesPage = "crate/features.html", + FeaturesPage, cache_policy = |page| if page.is_latest_url { CachePolicy::ForeverInCdn } else { @@ -35,6 +39,21 @@ impl_axum_webpage! { }, } +impl FeaturesPage { + pub(crate) fn krate(&self) -> Option<&CrateDetails> { + None + } + pub(crate) fn permalink_path(&self) -> &str { + "" + } + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + Some(&self.metadata) + } + pub(crate) fn use_direct_platform_links(&self) -> bool { + true + } +} + pub(crate) async fn build_features_handler( Path((name, req_version)): Path<(String, ReqVersion)>, mut conn: DbConnection, @@ -81,7 +100,7 @@ pub(crate) async fn build_features_handler( default_len, is_latest_url: req_version.is_latest(), canonical_url: CanonicalUrl::from_path(format!("/crate/{}/latest/features", &name)), - use_direct_platform_links: true, + csp_nonce: String::new(), } .into_response()) } diff --git a/src/web/headers.rs b/src/web/headers.rs index b93a92257..867a4dc27 100644 --- a/src/web/headers.rs +++ b/src/web/headers.rs @@ -3,6 +3,7 @@ use anyhow::Result; use axum::http::uri::{PathAndQuery, Uri}; use axum_extra::headers::{Header, HeaderName, HeaderValue}; use serde::Serialize; +use std::fmt; /// simplified typed header for a `Link rel=canonical` header in the response. /// Only takes the path to be used, url-encodes it and attaches domain & schema to it. @@ -52,6 +53,12 @@ impl Header for CanonicalUrl { } } +impl fmt::Display for CanonicalUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.build_full_uri()) + } +} + impl Serialize for CanonicalUrl { fn serialize(&self, serializer: S) -> Result where diff --git a/src/web/highlight.rs b/src/web/highlight.rs index 81d659b38..7b91a1234 100644 --- a/src/web/highlight.rs +++ b/src/web/highlight.rs @@ -73,7 +73,9 @@ pub fn with_lang(lang: Option<&str>, code: &str) -> String { } else { log::error!("failed while highlighting code: {err:?}"); } - tera::escape_html(code) + crate::web::page::templates::filters::escape_html(code) + .map(|s| s.to_string()) + .unwrap_or_default() } } } diff --git a/src/web/mod.rs b/src/web/mod.rs index 8976e7bb5..b0cc58de6 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,12 +1,15 @@ //! Web interface of docs.rs pub mod page; +// mod tmp; use crate::db::types::BuildStatus; use crate::utils::get_correct_docsrs_style_file; use crate::utils::report_error; +use crate::web::page::templates::filters; use anyhow::{anyhow, bail, Context as _, Result}; use axum_extra::middleware::option_layer; +use rinja::Template; use serde_json::Value; use tracing::{info, instrument}; @@ -25,7 +28,7 @@ mod markdown; pub(crate) mod metrics; mod releases; mod routes; -mod rustdoc; +pub(crate) mod rustdoc; mod sitemap; mod source; mod statics; @@ -46,7 +49,6 @@ use error::AxumNope; use page::TemplateData; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use semver::{Version, VersionReq}; -use serde::Serialize; use serde_with::{DeserializeFromStr, SerializeDisplay}; use std::{ borrow::{Borrow, Cow}, @@ -615,7 +617,8 @@ where } /// MetaData used in header -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize))] pub(crate) struct MetaData { pub(crate) name: String, /// The exact version of the release being shown. @@ -702,20 +705,40 @@ impl MetaData { targets.sort_unstable(); targets } + + fn target_name_url(&self) -> String { + if let Some(ref target_name) = self.target_name { + format!("{target_name}/index.html") + } else { + String::new() + } + } + + pub(crate) fn doc_targets(&self) -> Option<&[String]> { + self.doc_targets.as_deref() + } } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Template)] +#[template(path = "error.html")] +#[derive(Debug, Clone, PartialEq)] pub(crate) struct AxumErrorPage { /// The title of the page pub title: &'static str, /// The error message, displayed as a description pub message: Cow<'static, str>, - #[serde(skip)] pub status: StatusCode, + pub csp_nonce: String, +} + +impl AxumErrorPage { + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + None + } } impl_axum_webpage! { - AxumErrorPage = "error.html", + AxumErrorPage, status = |err| err.status, } @@ -795,11 +818,14 @@ mod test { let foo_crate = kuchikiki::parse_html().one(web.get("/crate/foo/0.0.1").send()?.text()?); - for value in &["60%", "6", "10", "2", "1"] { - assert!(foo_crate - .select(".pure-menu-item b") - .unwrap() - .any(|e| dbg!(e.text_contents()).contains(value))); + for (idx, value) in ["60%", "6", "10", "2", "1"].iter().enumerate() { + assert!( + foo_crate + .select(".pure-menu-item b") + .unwrap() + .any(|e| dbg!(e.text_contents()).contains(value)), + "({idx}, {value:?})" + ); } let foo_doc = kuchikiki::parse_html().one(web.get("/foo/0.0.1/foo").send()?.text()?); diff --git a/src/web/page/mod.rs b/src/web/page/mod.rs index 3f8a9d2bf..200b9f6a0 100644 --- a/src/web/page/mod.rs +++ b/src/web/page/mod.rs @@ -1,4 +1,4 @@ -mod templates; +pub(crate) mod templates; pub(crate) mod web_page; pub(crate) use templates::TemplateData; diff --git a/src/web/page/templates.rs b/src/web/page/templates.rs index aa4aeebd4..453bf794c 100644 --- a/src/web/page/templates.rs +++ b/src/web/page/templates.rs @@ -1,19 +1,49 @@ use crate::error::Result; +use crate::web::rustdoc::RustdocPage; +use crate::web::MetaData; use anyhow::Context; -use chrono::{DateTime, Utc}; -use path_slash::PathExt; -use serde_json::Value; -use std::{collections::HashMap, fmt, path::PathBuf, sync::Arc}; -use tera::{Result as TeraResult, Tera}; +use rinja::Template; +use std::{fmt, ops::Deref, sync::Arc}; use tracing::trace; -use walkdir::WalkDir; -const TEMPLATES_DIRECTORY: &str = "templates"; +macro_rules! rustdoc_page { + ($name:ident, $path:literal $(, $meta:ident)?) => { + #[derive(Template)] + #[template(path = $path)] + pub struct $name<'a> { + inner: &'a RustdocPage, + } + + impl<'a> $name<'a> { + pub fn new(inner: &'a RustdocPage) -> Self { + Self { inner } + } + + $( + pub(crate) fn $meta(&self) -> Option<&MetaData> { + Some(&self.inner.metadata) + } + )? + } + + impl<'a> Deref for $name<'a> { + type Target = RustdocPage; + + fn deref(&self) -> &Self::Target { + self.inner + } + } + }; +} + +rustdoc_page!(Head, "rustdoc/head.html"); +rustdoc_page!(Vendored, "rustdoc/vendored.html"); +rustdoc_page!(Body, "rustdoc/body.html"); +rustdoc_page!(Topbar, "rustdoc/topbar.html", get_metadata); /// Holds all data relevant to templating #[derive(Debug)] pub(crate) struct TemplateData { - pub templates: Tera, /// rendering threadpool for CPU intensive rendering. /// When the app is shut down, the pool won't wait /// for pending tasks in this pool. @@ -31,7 +61,6 @@ impl TemplateData { trace!("Loading templates"); let data = Self { - templates: load_templates()?, rendering_threadpool: rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .thread_name(move |idx| format!("docsrs-render {idx}")) @@ -51,12 +80,11 @@ impl TemplateData { /// Use this instead of `spawn_blocking` so we don't block tokio. pub(crate) async fn render_in_threadpool(self: &Arc, render_fn: F) -> Result where - F: FnOnce(&TemplateData) -> Result + Send + 'static, + F: FnOnce() -> Result + Send + 'static, R: Send + 'static, { let (send, recv) = tokio::sync::oneshot::channel(); self.rendering_threadpool.spawn({ - let templates = self.clone(); move || { // the job may have been queued on the thread-pool for a while, // if the request was closed in the meantime the receiver should have @@ -64,7 +92,7 @@ impl TemplateData { if !send.is_closed() { // `.send` only fails when the receiver is dropped while we were rendering, // at which point we don't need the result any more. - let _ = send.send(render_fn(&templates)); + let _ = send.send(render_fn()); } } }); @@ -73,106 +101,62 @@ impl TemplateData { } } -fn load_templates() -> Result { - // This uses a custom function to find the templates in the filesystem instead of Tera's - // builtin way (passing a glob expression to Tera::new), speeding up the startup of the - // application and running the tests. - // - // The problem with Tera's template loading code is, it walks all the files in the current - // directory and matches them against the provided glob expression. Unfortunately this means - // Tera will walk all the rustwide workspaces, the git repository and a bunch of other - // unrelated data, slowing down the search a lot. - // - // TODO: remove this when https://github.com/Gilnaa/globwalk/issues/29 is fixed - let mut tera = Tera::default(); - let template_files = find_templates_in_filesystem(TEMPLATES_DIRECTORY) - .with_context(|| format!("failed to search {TEMPLATES_DIRECTORY:?} for tera templates"))?; - tera.add_template_files(template_files).with_context(|| { - format!("failed while loading tera templates in {TEMPLATES_DIRECTORY:?}") - })?; - - // This function will return any global alert, if present. - ReturnValue::add_function_to( - &mut tera, - "global_alert", - serde_json::to_value(crate::GLOBAL_ALERT)?, - ); - // This function will return the current version of docs.rs. - ReturnValue::add_function_to( - &mut tera, - "docsrs_version", - Value::String(crate::BUILD_VERSION.into()), - ); - - // Custom filters - tera.register_filter("timeformat", timeformat); - tera.register_filter("dbg", dbg); - tera.register_filter("dedent", dedent); - tera.register_filter("fas", IconType::Strong); - tera.register_filter("far", IconType::Regular); - tera.register_filter("fab", IconType::Brand); - tera.register_filter("highlight", Highlight); - - Ok(tera) -} - -fn find_templates_in_filesystem(base: &str) -> Result)>> { - let root = std::fs::canonicalize(base)?; - - let mut files = Vec::new(); - for entry in WalkDir::new(&root) { - let entry = entry?; - let path = entry.path(); +pub mod filters { + use super::IconType; + use chrono::{DateTime, Utc}; + use std::borrow::Cow; + use std::fmt; - if !entry.metadata()?.is_file() { - continue; + // Copied from `tera`. + pub fn escape_html(input: &str) -> rinja::Result> { + if !input.chars().any(|c| "&<>\"'/".contains(c)) { + return Ok(Cow::Borrowed(input)); + } + let mut output = String::with_capacity(input.len() * 2); + for c in input.chars() { + match c { + '&' => output.push_str("&"), + '<' => output.push_str("<"), + '>' => output.push_str(">"), + '"' => output.push_str("""), + '\'' => output.push_str("'"), + '/' => output.push_str("/"), + _ => output.push(c), + } } - // Strip the root directory from the path and use it as the template name. - let name = path - .strip_prefix(&root) - .with_context(|| format!("{} is not a child of {}", path.display(), root.display()))? - .to_slash() - .with_context(|| anyhow::anyhow!("failed to normalize {}", path.display()))?; - files.push((path.to_path_buf(), Some(name.to_string()))); + // Not using shrink_to_fit() on purpose + Ok(Cow::Owned(output)) } - Ok(files) -} - -/// Simple function that returns the pre-defined value. -struct ReturnValue { - name: &'static str, - value: Value, -} - -impl ReturnValue { - fn add_function_to(tera: &mut Tera, name: &'static str, value: Value) { - tera.register_function(name, Self { name, value }) + // Copied from `tera`. + pub fn escape_xml(input: &str) -> rinja::Result> { + if !input.chars().any(|c| "&<>\"'".contains(c)) { + return Ok(Cow::Borrowed(input)); + } + let mut output = String::with_capacity(input.len() * 2); + for c in input.chars() { + match c { + '&' => output.push_str("&"), + '<' => output.push_str("<"), + '>' => output.push_str(">"), + '"' => output.push_str("""), + '\'' => output.push_str("'"), + _ => output.push(c), + } + } + Ok(Cow::Owned(output)) } -} -impl tera::Function for ReturnValue { - fn call(&self, args: &HashMap) -> TeraResult { - debug_assert!(args.is_empty(), "{} takes no args", self.name); - Ok(self.value.clone()) + /// Prettily format a timestamp + // TODO: This can be replaced by chrono + pub fn timeformat(value: &DateTime) -> rinja::Result { + Ok(crate::web::duration_to_str(*value)) } -} -/// Prettily format a timestamp -// TODO: This can be replaced by chrono -#[allow(clippy::unnecessary_wraps)] -fn timeformat(value: &Value, args: &HashMap) -> TeraResult { - let fmt = if let Some(Value::Bool(true)) = args.get("relative") { - let value = DateTime::parse_from_rfc3339(value.as_str().unwrap()) - .unwrap() - .with_timezone(&Utc); - - super::super::duration_to_str(value) - } else { + pub fn format_secs(mut value: f32) -> rinja::Result { const TIMES: &[&str] = &["seconds", "minutes", "hours"]; - let mut value = value.as_f64().unwrap(); let mut chosen_time = &TIMES[0]; for time in &TIMES[1..] { @@ -190,56 +174,115 @@ fn timeformat(value: &Value, args: &HashMap) -> TeraResult value.truncate(value.len() - 2); } - format!("{value} {chosen_time}") - }; + Ok(format!("{value} {chosen_time}")) + } - Ok(Value::String(fmt)) -} + /// Dedent a string by removing all leading whitespace + #[allow(clippy::unnecessary_wraps)] + pub fn dedent>>( + value: T, + levels: I, + ) -> rinja::Result { + let string = value.to_string(); + + let unindented = if let Some(levels) = levels.into() { + string + .lines() + .map(|mut line| { + for _ in 0..levels { + // `.strip_prefix` returns `Some(suffix without prefix)` if it's successful. If it fails + // to strip the prefix (meaning there's less than `levels` levels of indentation), + // we can just quit early + if let Some(suffix) = line.strip_prefix(" ") { + line = suffix; + } else { + break; + } + } -/// Print a tera value to stdout -#[allow(clippy::unnecessary_wraps)] -fn dbg(value: &Value, _args: &HashMap) -> TeraResult { - println!("{value:?}"); + line + }) + .collect::>() + .join("\n") + } else { + string + .lines() + .map(|l| l.trim_start()) + .collect::>() + .join("\n") + }; - Ok(value.clone()) -} + Ok(unindented) + } -/// Dedent a string by removing all leading whitespace -#[allow(clippy::unnecessary_wraps)] -fn dedent(value: &Value, args: &HashMap) -> TeraResult { - let string = value.as_str().expect("dedent takes a string"); + pub fn fas(value: &str, fw: bool, spin: bool, extra: &str) -> rinja::Result { + IconType::Strong.render(value, fw, spin, extra) + } - let unindented = if let Some(levels) = args - .get("levels") - .map(|l| l.as_i64().expect("`levels` must be an integer")) - { - string - .lines() - .map(|mut line| { - for _ in 0..levels { - // `.strip_prefix` returns `Some(suffix without prefix)` if it's successful. If it fails - // to strip the prefix (meaning there's less than `levels` levels of indentation), - // we can just quit early - if let Some(suffix) = line.strip_prefix(" ") { - line = suffix; - } else { - break; - } - } + pub fn far(value: &str, fw: bool, spin: bool, extra: &str) -> rinja::Result { + IconType::Regular.render(value, fw, spin, extra) + } - line - }) - .collect::>() - .join("\n") - } else { - string - .lines() - .map(|l| l.trim_start()) - .collect::>() - .join("\n") - }; + pub fn fab(value: &str, fw: bool, spin: bool, extra: &str) -> rinja::Result { + IconType::Brand.render(value, fw, spin, extra) + } - Ok(Value::String(unindented)) + pub fn highlight(code: impl std::fmt::Display, lang: &str) -> rinja::Result { + let highlighted_code = crate::web::highlight::with_lang(Some(lang), &code.to_string()); + Ok(format!("
{}
", highlighted_code)) + } + + pub fn slugify>(code: T) -> rinja::Result { + Ok(slug::slugify(code.as_ref())) + } + + pub fn round(value: &f32, precision: u32) -> rinja::Result { + let multiplier = if precision == 0 { + 1.0 + } else { + 10.0_f32.powi(precision as _) + }; + Ok(((multiplier * *value).round() / multiplier).to_string()) + } + + pub fn date(value: &DateTime, format: &str) -> rinja::Result { + Ok(format!("{}", value.format(format))) + } + + pub fn opt_date(value: &Option>, format: &str) -> rinja::Result { + if let Some(value) = value { + date(value, format) + } else { + Ok(String::new()) + } + } + + pub fn unwrap(value: &Option) -> rinja::Result<&T> { + Ok(value.as_ref().expect("`unwrap` filter failed")) + } + + pub fn split_first<'a>(value: &'a str, pat: &str) -> rinja::Result> { + Ok(value.split(pat).next()) + } + + pub fn to_string(value: &T) -> rinja::Result { + Ok(value.to_string()) + } + + pub fn json_encode(value: &T) -> rinja::Result { + Ok(serde_json::to_string(value).expect("`encode_json` failed")) + } + + pub fn as_f32(value: &i32) -> rinja::Result { + Ok(*value as f32) + } + + pub fn rest_menu_url(current_target: &str, inner_path: &str) -> rinja::Result { + if current_target.is_empty() { + return Ok(String::new()); + } + Ok(format!("/{current_target}/{inner_path}")) + } } enum IconType { @@ -260,27 +303,24 @@ impl fmt::Display for IconType { } } -impl tera::Filter for IconType { - fn filter(&self, value: &Value, args: &HashMap) -> TeraResult { - let icon_name = tera::escape_html(value.as_str().expect("Icons only take strings")); - +impl IconType { + fn render(self, icon_name: &str, fw: bool, spin: bool, extra: &str) -> rinja::Result { let type_ = match self { IconType::Strong => font_awesome_as_a_crate::Type::Solid, IconType::Regular => font_awesome_as_a_crate::Type::Regular, IconType::Brand => font_awesome_as_a_crate::Type::Brands, }; - let icon_file_string = font_awesome_as_a_crate::svg(type_, &icon_name[..]).unwrap_or(""); + let icon_file_string = font_awesome_as_a_crate::svg(type_, icon_name).unwrap_or(""); - let mut classes = vec!["fa-svg", "fa-svg-fw"]; - if args - .get("spin") - .and_then(|spin| spin.as_bool()) - .unwrap_or(false) - { + let mut classes = vec!["fa-svg"]; + if fw { + classes.push("fa-svg-fw"); + } + if spin { classes.push("fa-svg-spin"); - }; - if let Some(extra) = args.get("extra").and_then(|s| s.as_str()) { + } + if !extra.is_empty() { classes.push(extra); } let icon = format!( @@ -289,55 +329,6 @@ impl tera::Filter for IconType { class = classes.join(" "), ); - Ok(Value::String(icon)) - } - - fn is_safe(&self) -> bool { - true - } -} - -struct Highlight; - -impl tera::Filter for Highlight { - fn filter(&self, value: &Value, args: &HashMap) -> TeraResult { - let code = value.as_str().ok_or_else(|| { - let msg = format!( "Filter `highlight` was called on an incorrect value: got `{value}` but expected a string"); - tera::Error::msg(msg) - })?; - let lang = args - .get("lang") - .and_then(|lang| { - if lang.is_null() { - None - } else { - Some(lang.as_str().ok_or_else(|| { - let msg = format!("Filter `highlight` received an incorrect type for arg `{lang}`: got `{lang}` but expected a string"); - tera::Error::msg(msg) - })) - } - }) - .transpose()?; - let highlighted = crate::web::highlight::with_lang(lang, code); - Ok(format!("
{highlighted}
").into()) - } - - fn is_safe(&self) -> bool { - true - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_templates_are_valid() { - crate::test::wrapper(|_| { - let tera = load_templates().unwrap(); - tera.check_macro_files().unwrap(); - - Ok(()) - }); + Ok(icon) } } diff --git a/src/web/page/web_page.rs b/src/web/page/web_page.rs index 7ead62b42..20a425f63 100644 --- a/src/web/page/web_page.rs +++ b/src/web/page/web_page.rs @@ -1,6 +1,4 @@ -use super::TemplateData; -use crate::web::{csp::Csp, error::AxumNope}; -use anyhow::Error; +use crate::web::{csp::Csp, error::AxumNope, TemplateData}; use axum::{ body::Body, extract::Request as AxumRequest, @@ -10,12 +8,15 @@ use axum::{ use futures_util::future::{BoxFuture, FutureExt}; use http::header::CONTENT_LENGTH; use std::sync::Arc; -use tera::Context; + +pub(crate) trait AddCspNonce: IntoResponse { + fn render_with_csp_nonce(&mut self, csp_nonce: String) -> rinja::Result; +} #[macro_export] macro_rules! impl_axum_webpage { ( - $page:ty = $template:literal + $page:ty $(, status = $status:expr)? $(, content_type = $content_type:expr)? $(, canonical_url = $canonical_url:expr)? @@ -23,25 +24,13 @@ macro_rules! impl_axum_webpage { $(, cpu_intensive_rendering = $cpu_intensive_rendering:expr)? $(,)? ) => { - $crate::impl_axum_webpage!( - $page = |_| ::std::borrow::Cow::Borrowed($template) - $(, status = $status)? - $(, content_type = $content_type)? - $(, canonical_url = $canonical_url)? - $(, cache_policy = $cache_policy)? - $(, cpu_intensive_rendering = $cpu_intensive_rendering )? - ); - }; + impl $crate::web::page::web_page::AddCspNonce for $page { + fn render_with_csp_nonce(&mut self, csp_nonce: String) -> rinja::Result { + self.csp_nonce = csp_nonce; + self.render() + } + } - ( - $page:ty = $template:expr - $(, status = $status:expr)? - $(, content_type = $content_type:expr)? - $(, canonical_url = $canonical_url:expr)? - $(, cache_policy = $cache_policy:expr)? - $(, cpu_intensive_rendering = $cpu_intensive_rendering:expr)? - $(,)? - ) => { impl axum::response::IntoResponse for $page { fn into_response(self) -> ::axum::response::Response { @@ -92,16 +81,7 @@ macro_rules! impl_axum_webpage { response.extensions_mut().insert($crate::web::page::web_page::DelayedTemplateRender { - context: { - let mut c = ::tera::Context::from_serialize(&self) - .expect("could not create tera context from web-page"); - c.insert("DEFAULT_MAX_TARGETS", &$crate::DEFAULT_MAX_TARGETS); - c - }, - template: { - let template: fn(&Self) -> ::std::borrow::Cow<'static, str> = $template; - template(&self).to_string() - }, + template: std::sync::Arc::new(Box::new(self)), cpu_intensive_rendering, }); response @@ -115,8 +95,7 @@ macro_rules! impl_axum_webpage { /// the context. #[derive(Clone)] pub(crate) struct DelayedTemplateRender { - pub template: String, - pub context: Context, + pub template: Arc>, pub cpu_intensive_rendering: bool, } @@ -129,28 +108,26 @@ fn render_response( if let Some(render) = response.extensions_mut().remove::() { let DelayedTemplateRender { template, - mut context, cpu_intensive_rendering, } = render; - context.insert("csp_nonce", &csp_nonce); + let mut template = Arc::into_inner(template).unwrap(); + let csp_nonce_clone = csp_nonce.clone(); - let rendered = if cpu_intensive_rendering { + let result: Result = if cpu_intensive_rendering { templates - .render_in_threadpool(move |templates| { - templates - .templates - .render(&template, &context) - .map_err(Into::into) + .render_in_threadpool(move || { + template + .render_with_csp_nonce(csp_nonce_clone) + .map_err(|err| err.into()) }) .await } else { - templates - .templates - .render(&template, &context) - .map_err(Error::new) + template + .render_with_csp_nonce(csp_nonce_clone) + .map_err(|err| err.into()) }; - let rendered = match rendered { + let rendered = match result { Ok(content) => content, Err(err) => { if response.status().is_server_error() { diff --git a/src/web/releases.rs b/src/web/releases.rs index 5b5c43a70..477a05c80 100644 --- a/src/web/releases.rs +++ b/src/web/releases.rs @@ -10,7 +10,9 @@ use crate::{ axum_parse_uri_with_params, axum_redirect, encode_url_path, error::{AxumNope, AxumResult}, extractors::{DbConnection, Path}, - match_version, ReqVersion, + match_version, + page::templates::filters, + MetaData, ReqVersion, }, BuildQueue, Config, InstanceMetrics, }; @@ -23,6 +25,7 @@ use base64::{engine::general_purpose::STANDARD as b64, Engine}; use chrono::{DateTime, Utc}; use futures_util::stream::TryStreamExt; use once_cell::sync::Lazy; +use rinja::Template; use serde::{Deserialize, Serialize}; use sqlx::Row; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -44,12 +47,12 @@ const RELEASES_IN_FEED: i64 = 150; pub struct Release { pub(crate) name: String, pub(crate) version: String, - description: Option, - target_name: Option, - rustdoc_status: bool, + pub(crate) description: Option, + pub(crate) target_name: Option, + pub(crate) rustdoc_status: bool, pub(crate) build_time: Option>, - stars: i32, - has_unyanked_releases: Option, + pub(crate) stars: i32, + pub(crate) has_unyanked_releases: Option, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -296,40 +299,60 @@ async fn get_search_results( }) } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Template)] +#[template(path = "core/home.html")] +#[derive(Debug, Clone, PartialEq, Eq)] struct HomePage { recent_releases: Vec, + csp_nonce: String, } impl_axum_webpage! { - HomePage = "core/home.html", + HomePage, cache_policy = |_| CachePolicy::ShortInCdnAndBrowser, } +impl HomePage { + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + None + } +} + pub(crate) async fn home_page(mut conn: DbConnection) -> AxumResult { let recent_releases = get_releases(&mut conn, 1, RELEASES_IN_HOME, Order::ReleaseTime, true).await?; - Ok(HomePage { recent_releases }) + Ok(HomePage { + recent_releases, + csp_nonce: String::new(), + }) } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Template)] +#[template(path = "releases/feed.xml")] +#[derive(Debug, Clone, PartialEq, Eq)] struct ReleaseFeed { recent_releases: Vec, + csp_nonce: String, } impl_axum_webpage! { - ReleaseFeed = "releases/feed.xml", + ReleaseFeed, content_type = "application/xml", } pub(crate) async fn releases_feed_handler(mut conn: DbConnection) -> AxumResult { let recent_releases = get_releases(&mut conn, 1, RELEASES_IN_FEED, Order::ReleaseTime, true).await?; - Ok(ReleaseFeed { recent_releases }) + Ok(ReleaseFeed { + recent_releases, + csp_nonce: String::new(), + }) } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Template)] +#[template(path = "releases/releases.html")] +#[derive(Debug, Clone, PartialEq, Eq)] struct ViewReleases { releases: Vec, description: String, @@ -338,14 +361,18 @@ struct ViewReleases { show_previous_page: bool, page_number: i64, owner: Option, + csp_nonce: String, } -impl_axum_webpage! { - ViewReleases = "releases/releases.html", +impl_axum_webpage! { ViewReleases } + +impl ViewReleases { + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + None + } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(crate) enum ReleaseType { Recent, Stars, @@ -354,6 +381,29 @@ pub(crate) enum ReleaseType { Search, } +impl<'a> PartialEq<&'a str> for ReleaseType { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} +impl PartialEq for ReleaseType { + fn eq(&self, other: &str) -> bool { + self.as_str() == other + } +} + +impl ReleaseType { + fn as_str(&self) -> &str { + match self { + Self::Recent => "recent", + Self::Stars => "stars", + Self::RecentFailures => "recent_failures", + Self::Failures => "failures", + Self::Search => "search", + } + } +} + pub(crate) async fn releases_handler( conn: &mut sqlx::PgConnection, page: Option, @@ -403,6 +453,7 @@ pub(crate) async fn releases_handler( show_previous_page, page_number, owner: None, + csp_nonce: String::new(), }) } @@ -442,36 +493,44 @@ pub(crate) async fn owner_handler(Path(owner): Path) -> AxumResult, + pub(super) releases: Vec, pub(super) search_query: Option, pub(super) search_sort_by: Option, pub(super) previous_page_link: Option, pub(super) next_page_link: Option, /// This should always be `ReleaseType::Search` pub(super) release_type: ReleaseType, - #[serde(skip)] pub(super) status: http::StatusCode, + pub(super) csp_nonce: String, } impl Default for Search { fn default() -> Self { Self { title: String::default(), - results: Vec::default(), + releases: Vec::default(), search_query: None, previous_page_link: None, next_page_link: None, search_sort_by: None, release_type: ReleaseType::Search, status: http::StatusCode::OK, + csp_nonce: String::new(), } } } +impl Search { + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + None + } +} + async fn redirect_to_random_crate( config: Arc, metrics: Arc, @@ -527,7 +586,7 @@ async fn redirect_to_random_crate( } impl_axum_webpage! { - Search = "releases/search_results.html", + Search, status = |search| search.status, } @@ -655,7 +714,7 @@ pub(crate) async fn search_handler( Ok(Search { title, - results: search_result.results, + releases: search_result.results, search_query: Some(executed_query), search_sort_by: Some(sort_by), next_page_link: search_result @@ -669,18 +728,25 @@ pub(crate) async fn search_handler( .into_response()) } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Template)] +#[template(path = "releases/activity.html")] +#[derive(Debug, Clone, PartialEq)] struct ReleaseActivity { description: &'static str, dates: Vec, counts: Vec, failures: Vec, + csp_nonce: String, } -impl_axum_webpage! { - ReleaseActivity = "releases/activity.html", +impl ReleaseActivity { + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + None + } } +impl_axum_webpage! { ReleaseActivity } + pub(crate) async fn activity_handler(mut conn: DbConnection) -> AxumResult { let rows: Vec<_> = sqlx::query!( r#"WITH dates AS ( @@ -732,18 +798,26 @@ pub(crate) async fn activity_handler(mut conn: DbConnection) -> AxumResult, active_deployments: Vec, + csp_nonce: String, } -impl_axum_webpage! { - BuildQueuePage = "releases/build_queue.html", +impl_axum_webpage! { BuildQueuePage } + +impl BuildQueuePage { + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + None + } } pub(crate) async fn build_queue_handler( @@ -780,6 +854,7 @@ pub(crate) async fn build_queue_handler( description: "crate documentation scheduled to build & deploy", queue, active_deployments, + csp_nonce: String::new(), }) } @@ -1580,12 +1655,13 @@ mod tests { wrapper(|env| { let web = env.frontend(); - let empty_data = format!("data: [{}]", vec!["0"; 30].join(",")); + let empty_data = format!("data: [{}]", vec!["0"; 30].join(", ")); // no data / only zeros without releases let response = web.get("/releases/activity/").send()?; assert!(response.status().is_success()); - assert_eq!(response.text().unwrap().matches(&empty_data).count(), 2); + let text = response.text(); + assert_eq!(text.unwrap().matches(&empty_data).count(), 2); env.fake_release().name("some_random_crate").create()?; env.fake_release() @@ -1613,9 +1689,9 @@ mod tests { assert!(response.status().is_success()); let text = response.text().unwrap(); // counts contain both releases - assert!(text.contains(&format!("data: [{},2]", vec!["0"; 29].join(",")))); + assert!(text.contains(&format!("data: [{}, 2]", vec!["0"; 29].join(", ")))); // failures only one - assert!(text.contains(&format!("data: [{},1]", vec!["0"; 29].join(",")))); + assert!(text.contains(&format!("data: [{}, 1]", vec!["0"; 29].join(", ")))); Ok(()) }) diff --git a/src/web/routes.rs b/src/web/routes.rs index 00d80b846..7b2517d13 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -10,6 +10,7 @@ use axum::{ Router as AxumRouter, }; use axum_extra::routing::RouterExt; +use rinja::Template; use std::convert::Infallible; use tracing::{debug, instrument}; @@ -287,13 +288,19 @@ pub(super) fn build_axum_routes() -> AxumRouter { .route( "/-/storage-change-detection.html", get_internal(|| async { - #[derive(Debug, Clone, serde::Serialize)] - struct StorageChangeDetection {} + #[derive(Template)] + #[template(path = "storage-change-detection.html")] + #[derive(Debug, Clone)] + struct StorageChangeDetection { + csp_nonce: String, + } crate::impl_axum_webpage!( - StorageChangeDetection = "storage-change-detection.html", + StorageChangeDetection, cache_policy = |_| CachePolicy::ForeverInCdnAndBrowser, ); - StorageChangeDetection {} + StorageChangeDetection { + csp_nonce: String::new(), + } }), ) .route_with_tsr( diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index 177991e44..bb3d99c1a 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -28,7 +28,7 @@ use axum::{ use lol_html::errors::RewritingError; use once_cell::sync::Lazy; use semver::Version; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::{ collections::{BTreeMap, HashMap}, sync::Arc, @@ -257,22 +257,20 @@ pub(crate) async fn rustdoc_redirector_handler( } } -#[derive(Debug, Clone, Serialize)] -struct RustdocPage { - latest_path: String, - permalink_path: String, - latest_version: String, - target: String, - inner_path: String, +#[derive(Debug, Clone)] +pub struct RustdocPage { + pub latest_path: String, + pub permalink_path: String, + pub inner_path: String, // true if we are displaying the latest version of the crate, regardless // of whether the URL specifies a version number or the string "latest." - is_latest_version: bool, + pub is_latest_version: bool, // true if the URL specifies a version using the string "latest." - is_latest_url: bool, - is_prerelease: bool, - krate: CrateDetails, - metadata: MetaData, - current_target: String, + pub is_latest_url: bool, + pub is_prerelease: bool, + pub krate: CrateDetails, + pub metadata: MetaData, + pub current_target: String, } impl RustdocPage { @@ -280,20 +278,15 @@ impl RustdocPage { self, rustdoc_html: &[u8], max_parse_memory: usize, - templates: &TemplateData, metrics: &InstanceMetrics, config: &Config, file_path: &str, ) -> AxumResult { let is_latest_url = self.is_latest_url; - // Build the page of documentation - let mut ctx = tera::Context::from_serialize(self).context("error creating tera context")?; - ctx.insert("DEFAULT_MAX_TARGETS", &crate::DEFAULT_MAX_TARGETS); - // Extract the head and body of the rustdoc file so that we can insert it into our own html // while logging OOM errors from html rewriting - let html = match utils::rewrite_lol(rustdoc_html, max_parse_memory, ctx, templates) { + let html = match utils::rewrite_lol(rustdoc_html, max_parse_memory, &self) { Err(RewritingError::MemoryLimitExceeded(..)) => { metrics.html_rewrite_ooms.inc(); @@ -319,6 +312,20 @@ impl RustdocPage { ) .into_response()) } + + // Used for template rendering. + pub(crate) fn krate(&self) -> Option<&CrateDetails> { + Some(&self.krate) + } + + // Used for template rendering. + pub(crate) fn permalink_path(&self) -> &str { + &self.permalink_path + } + + pub(crate) fn use_direct_platform_links(&self) -> bool { + !self.latest_path.contains("/target-redirect/") + } } #[derive(Clone, Deserialize, Debug)] @@ -595,23 +602,15 @@ pub(crate) async fn rustdoc_html_server_handler( .recently_accessed_releases .record(krate.crate_id, krate.release_id, target); - let target = if target.is_empty() { - String::new() - } else { - format!("{target}/") - }; - // Build the page of documentation, templates .render_in_threadpool({ let metrics = metrics.clone(); - move |templates| { + move || { let metadata = krate.metadata.clone(); Ok(RustdocPage { latest_path, permalink_path, - latest_version: latest_version.to_string(), - target, inner_path, is_latest_version, is_latest_url: params.version.is_latest(), @@ -623,7 +622,6 @@ pub(crate) async fn rustdoc_html_server_handler( .into_response( &blob.content, config.max_parse_memory, - templates, &metrics, &config, &storage_path, diff --git a/src/web/sitemap.rs b/src/web/sitemap.rs index b388a7c64..614521e72 100644 --- a/src/web/sitemap.rs +++ b/src/web/sitemap.rs @@ -6,34 +6,41 @@ use crate::{ web::{ error::{AxumNope, AxumResult}, extractors::{DbConnection, Path}, - AxumErrorPage, + page::templates::filters, + AxumErrorPage, MetaData, }, Config, }; use axum::{extract::Extension, http::StatusCode, response::IntoResponse}; use chrono::{TimeZone, Utc}; use futures_util::stream::TryStreamExt; -use serde::Serialize; +use rinja::Template; use std::sync::Arc; /// sitemap index -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Template)] +#[template(path = "core/sitemapindex.xml")] +#[derive(Debug, Clone, PartialEq, Eq)] struct SitemapIndexXml { sitemaps: Vec, + csp_nonce: String, } impl_axum_webpage! { - SitemapIndexXml = "core/sitemapindex.xml", + SitemapIndexXml, content_type = "application/xml", } pub(crate) async fn sitemapindex_handler() -> impl IntoResponse { let sitemaps: Vec = ('a'..='z').collect(); - SitemapIndexXml { sitemaps } + SitemapIndexXml { + sitemaps, + csp_nonce: String::new(), + } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq)] struct SitemapRow { crate_name: String, last_modified: String, @@ -41,13 +48,16 @@ struct SitemapRow { } /// The sitemap -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Template)] +#[template(path = "core/sitemap.xml")] +#[derive(Debug, Clone, PartialEq, Eq)] struct SitemapXml { releases: Vec, + csp_nonce: String, } impl_axum_webpage! { - SitemapXml = "core/sitemap.xml", + SitemapXml, content_type = "application/xml", } @@ -93,10 +103,15 @@ pub(crate) async fn sitemap_handler( .try_collect() .await?; - Ok(SitemapXml { releases }) + Ok(SitemapXml { + releases, + csp_nonce: String::new(), + }) } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Template)] +#[template(path = "core/about/builds.html")] +#[derive(Debug, Clone, PartialEq, Eq)] struct AboutBuilds { /// The current version of rustc that docs.rs is using to build crates rustc_version: Option, @@ -104,9 +119,16 @@ struct AboutBuilds { limits: Limits, /// Just for the template, since this isn't shared with AboutPage active_tab: &'static str, + csp_nonce: String, +} + +impl AboutBuilds { + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + None + } } -impl_axum_webpage!(AboutBuilds = "core/about/builds.html"); +impl_axum_webpage!(AboutBuilds); pub(crate) async fn about_builds_handler( Extension(pool): Extension, @@ -122,17 +144,34 @@ pub(crate) async fn about_builds_handler( rustc_version, limits: Limits::new(&config), active_tab: "builds", + csp_nonce: String::new(), }) } -#[derive(Serialize)] -struct AboutPage<'a> { - #[serde(skip)] - template: String, - active_tab: &'a str, +macro_rules! about_page { + ($ty:ident, $template:literal) => { + #[derive(Template)] + #[template(path = $template)] + struct $ty { + active_tab: &'static str, + csp_nonce: String, + } + + impl_axum_webpage! { $ty } + + impl $ty { + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + None + } + } + }; } -impl_axum_webpage!(AboutPage<'_> = |this: &AboutPage| this.template.clone().into()); +about_page!(AboutPage, "core/about/index.html"); +about_page!(AboutPageBadges, "core/about/badges.html"); +about_page!(AboutPageMetadata, "core/about/metadata.html"); +about_page!(AboutPageRedirection, "core/about/redirections.html"); +about_page!(AboutPageDownload, "core/about/download.html"); pub(crate) async fn about_handler(subpage: Option>) -> AxumResult { let subpage = match subpage { @@ -140,9 +179,32 @@ pub(crate) async fn about_handler(subpage: Option>) -> AxumResult "index".to_string(), }; - let name = match &subpage[..] { - "about" | "index" => "index", - x @ "badges" | x @ "metadata" | x @ "redirections" | x @ "download" => x, + let response = match &subpage[..] { + "about" | "index" => AboutPage { + active_tab: "index", + csp_nonce: String::new(), + } + .into_response(), + "badges" => AboutPageBadges { + active_tab: "badges", + csp_nonce: String::new(), + } + .into_response(), + "metadata" => AboutPageMetadata { + active_tab: "metadata", + csp_nonce: String::new(), + } + .into_response(), + "redirections" => AboutPageRedirection { + active_tab: "redirections", + csp_nonce: String::new(), + } + .into_response(), + "download" => AboutPageDownload { + active_tab: "download", + csp_nonce: String::new(), + } + .into_response(), _ => { let msg = "This /about page does not exist. \ Perhaps you are interested in creating it?"; @@ -150,16 +212,12 @@ pub(crate) async fn about_handler(subpage: Option>) -> AxumResult, } @@ -142,7 +143,9 @@ impl FileList { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Template)] +#[template(path = "crate/source.html")] +#[derive(Debug, Clone)] struct SourcePage { file_list: FileList, metadata: MetaData, @@ -152,11 +155,11 @@ struct SourcePage { canonical_url: CanonicalUrl, is_file_too_large: bool, is_latest_url: bool, - use_direct_platform_links: bool, + csp_nonce: String, } impl_axum_webpage! { - SourcePage = "crate/source.html", + SourcePage, canonical_url = |page| Some(page.canonical_url.clone()), cache_policy = |page| if page.is_latest_url { CachePolicy::ForeverInCdn @@ -166,6 +169,22 @@ impl_axum_webpage! { cpu_intensive_rendering = true, } +// Used in templates. +impl SourcePage { + pub(crate) fn get_metadata(&self) -> Option<&MetaData> { + Some(&self.metadata) + } + pub(crate) fn permalink_path(&self) -> &str { + "" + } + pub(crate) fn krate(&self) -> Option<&CrateDetails> { + None + } + pub(crate) fn use_direct_platform_links(&self) -> bool { + true + } +} + #[derive(Deserialize, Clone, Debug)] pub(crate) struct SourceBrowserHandlerParams { name: String, @@ -320,7 +339,7 @@ pub(crate) async fn source_browser_handler( canonical_url, is_file_too_large, is_latest_url: params.version.is_latest(), - use_direct_platform_links: true, + csp_nonce: String::new(), } .into_response()) } diff --git a/src/web/statics.rs b/src/web/statics.rs index 1ffc7956b..339f5d6f7 100644 --- a/src/web/statics.rs +++ b/src/web/statics.rs @@ -26,7 +26,8 @@ fn build_static_css_response(content: &'static str) -> impl IntoResponse { } async fn set_needed_static_headers(req: Request, next: Next) -> Response { - let is_opensearch_xml = req.uri().path().ends_with("/opensearch.xml"); + let req_path = req.uri().path(); + let is_opensearch_xml = req_path.ends_with("/opensearch.xml"); let mut response = next.run(req).await; diff --git a/templates/about-base.html b/templates/about-base.html index 92222f177..6283f516c 100644 --- a/templates/about-base.html +++ b/templates/about-base.html @@ -11,32 +11,38 @@

Docs.rs documentation

    - {% set text = "circle-info" | fas %} - {% set text = text ~ ' About' %} - {{ macros::active_link(expected="index", href="/about", text=text) }} + {% set text = "circle-info"|fas(false, false, "")|safe %} + {% set text = "{} About"|format(text) %} + {% call macros::active_link(expected="index", href="/about", text=text) %} - {% set text = "fonticons" | fab %} - {% set text = text ~ ' Badges' %} - {{ macros::active_link(expected="badges", href="/about/badges", text=text) }} + {% set text = "fonticons"|fab(false, false, "") %} + {% set text = "{} Badges"|format(text) %} + {% call macros::active_link(expected="badges", href="/about/badges", text=text) %} - {% set text = "gears" | fas %} - {% set text = text ~ ' Builds' %} - {{ macros::active_link(expected="builds", href="/about/builds", text=text) }} + {% set text = "gears"|fas(false, false, "")|safe %} + {% set text = "{} Builds"|format(text) %} + {% call macros::active_link(expected="builds", href="/about/builds", text=text) %} - {% set text = "table" | fas %} - {% set text = text ~ ' Metadata' %} - {{ macros::active_link(expected="metadata", href="/about/metadata", text=text) }} + {% set text = "table"|fas(false, false, "")|safe %} + {% set text = "{} Metadata"|format(text) %} + {% call macros::active_link(expected="metadata", href="/about/metadata", text=text) %} - {% set text = "road" | fas %} - {% set text = text ~ ' Shorthand URLs' %} - {{ macros::active_link(expected="redirections", href="/about/redirections", text=text) }} + {% set text = "road"|fas(false, false, "")|safe %} + {% set text = "{} Shorthand URLs"|format(text) %} + {% call macros::active_link(expected="redirections", href="/about/redirections", text=text) %} - {% set text = "download" | fas %} - {% set text = text ~ ' Download' %} - {{ macros::active_link(expected="download", href="/about/download", text=text) }} + {% set text = "download"|fas(false, false, "")|safe %} + {% set text = "{} Download"|format(text) %} + {% call macros::active_link(expected="download", href="/about/download", text=text) %}
{% endblock %} + +{%- block topbar -%} + {% set is_latest_version = true %} + {% let search_query = Some(String::new()) %} + {%- include "header/topbar.html" -%} +{%- endblock topbar -%} diff --git a/templates/base.html b/templates/base.html index 6f3fb9ba8..02a30faaf 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,17 +1,15 @@ {%- import "macros.html" as macros -%} - - - + {%- block meta -%}{%- endblock meta -%} {# Docs.rs styles #} - - + + @@ -20,8 +18,8 @@ {%- block css -%}{%- endblock css -%} - - + + diff --git a/templates/core/about/badges.html b/templates/core/about/badges.html index 762fc807d..b61a3c1f2 100644 --- a/templates/core/about/badges.html +++ b/templates/core/about/badges.html @@ -1,4 +1,4 @@ -{% extends "about-base.html" -%} +{% extends "about-base.html" %} {%- block title -%} Badges {%- endblock title -%} diff --git a/templates/core/about/builds.html b/templates/core/about/builds.html index 4c6c6a50a..3c6ca4c3c 100644 --- a/templates/core/about/builds.html +++ b/templates/core/about/builds.html @@ -1,4 +1,4 @@ -{% extends "about-base.html" -%} +{% extends "about-base.html" %} {%- block title -%} Builds {%- endblock title -%} @@ -15,7 +15,7 @@

Builds

All crates are built in a sandbox using the nightly release of the Rust compiler. - {%- if rustc_version %} + {%- if let Some(rustc_version) = rustc_version %} The current version in use is {{ rustc_version }}. {%- endif -%}

@@ -31,20 +31,20 @@

Setting a README

Detecting Docs.rs

To recognize Docs.rs from your Rust code, you can test for the docsrs cfg, e.g.: - {% filter highlight(lang="rust") %}{% filter dedent(levels=3) -%} + {% filter dedent(None)|highlight("rust")|safe -%} #[cfg(docsrs)] mod documentation; - {%- endfilter %}{% endfilter %} + {%- endfilter %} The `docsrs` cfg only applies to the final rustdoc invocation (i.e. the crate currently being documented). It does not apply to dependencies (including workspace ones).

To recognize Docs.rs from build.rs files, you can test for the environment variable DOCS_RS, e.g.: - {% filter highlight(lang="rust") %}{% filter dedent(levels=3) -%} + {% filter dedent(3)|highlight("rust")|safe -%} if std::env::var("DOCS_RS").is_ok() { // ... your code here ... } - {%- endfilter %}{% endfilter %} + {%- endfilter %} This approach can be helpful if you need dependencies for building the library, but not for building the documentation.

@@ -55,18 +55,18 @@

Cross-compiling

You can configure how your crate is built by adding package metadata to your Cargo.toml, e.g.: - {% filter highlight(lang="toml") %}{% filter dedent -%} + {% filter dedent(None)|highlight("toml")|safe -%} [package.metadata.docs.rs] rustc-args = ["--cfg", "my_cfg"] - {%- endfilter %}{% endfilter %} + {%- endfilter %} Here, the compiler arguments are set so that #[cfg(my_cfg)] (not to be confused with #[cfg(doc)]) can be used for conditional compilation. This approach is also useful for setting cargo features.

Testing documentation builds locally

- {%- set build_subcommand = docsrs_repo ~ "/blob/master/README.md#build-subcommand" -%} + {%- set build_subcommand = "{}/blob/master/README.md#build-subcommand"|format(docsrs_repo) -%}

- The Docs.rs README describes how to build + The Docs.rs README describes how to build unpublished crate documentation locally using the same build environment as the Docs.rs build agent.

@@ -93,11 +93,11 @@

Hitting res All the builds are executed inside a sandbox with limited resources. The current limits are:

- {{ macros::crate_limits(limits=limits) }} + {% call macros::crate_limits(limits=limits) %}

If your build fails because it hit one of these limits, please - open an issue + open an issue to get them increased for your crate. Since our build agent has finite resources, we have to consider each case individually. However, there are a few general policies: