diff --git a/Cargo.lock b/Cargo.lock
index dea1ac1c1..738318331 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -135,6 +135,42 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+[[package]]
+name = "askama"
+version = "0.13.0"
+dependencies = [
+ "askama_derive",
+ "askama_escape",
+ "humansize",
+ "num-traits",
+ "percent-encoding",
+]
+
+[[package]]
+name = "askama_derive"
+version = "0.13.0"
+dependencies = [
+ "askama_parser",
+ "basic-toml",
+ "mime",
+ "mime_guess",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.59",
+]
+
+[[package]]
+name = "askama_escape"
+version = "0.10.3"
+
+[[package]]
+name = "askama_parser"
+version = "0.2.1"
+dependencies = [
+ "nom",
+]
+
[[package]]
name = "assert-json-diff"
version = "1.1.0"
@@ -805,6 +841,15 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+[[package]]
+name = "basic-toml"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2db21524cad41c5591204d22d75e1970a2d1f71060214ca931dc7d5afe2c14e5"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "bincode"
version = "1.3.3"
@@ -933,28 +978,6 @@ dependencies = [
"windows-targets 0.52.5",
]
-[[package]]
-name = "chrono-tz"
-version = "0.8.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e"
-dependencies = [
- "chrono",
- "chrono-tz-build",
- "phf 0.11.2",
-]
-
-[[package]]
-name = "chrono-tz-build"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f"
-dependencies = [
- "parse-zoneinfo",
- "phf 0.11.2",
- "phf_codegen 0.11.2",
-]
-
[[package]]
name = "ciborium"
version = "0.2.2"
@@ -1567,6 +1590,7 @@ name = "docs-rs"
version = "0.6.0"
dependencies = [
"anyhow",
+ "askama",
"async-stream",
"aws-config",
"aws-sdk-cloudfront",
@@ -1641,7 +1665,6 @@ dependencies = [
"strum",
"syntect",
"tempfile",
- "tera",
"test-case",
"thiserror",
"thread_local",
@@ -2872,30 +2895,6 @@ dependencies = [
"gix-path",
]
-[[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.6",
- "regex-syntax 0.8.3",
-]
-
-[[package]]
-name = "globwalk"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
-dependencies = [
- "bitflags 1.3.2",
- "ignore",
- "walkdir",
-]
-
[[package]]
name = "grass"
version = "0.13.2"
@@ -3342,22 +3341,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.6",
- "same-file",
- "walkdir",
- "winapi-util",
-]
-
[[package]]
name = "imara-diff"
version = "0.1.5"
@@ -4093,15 +4076,6 @@ dependencies = [
"windows-targets 0.48.5",
]
-[[package]]
-name = "parse-zoneinfo"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
-dependencies = [
- "regex",
-]
-
[[package]]
name = "paste"
version = "1.0.14"
@@ -4129,51 +4103,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-[[package]]
-name = "pest"
-version = "2.7.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95"
-dependencies = [
- "memchr",
- "thiserror",
- "ucd-trie",
-]
-
-[[package]]
-name = "pest_derive"
-version = "2.7.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c"
-dependencies = [
- "pest",
- "pest_generator",
-]
-
-[[package]]
-name = "pest_generator"
-version = "2.7.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd"
-dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn 2.0.59",
-]
-
-[[package]]
-name = "pest_meta"
-version = "2.7.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca"
-dependencies = [
- "once_cell",
- "pest",
- "sha2",
-]
-
[[package]]
name = "phf"
version = "0.8.0"
@@ -4224,16 +4153,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"
@@ -5995,28 +5914,6 @@ dependencies = [
"utf-8",
]
-[[package]]
-name = "tera"
-version = "1.19.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8"
-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"
@@ -6434,12 +6331,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"
@@ -6458,56 +6349,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 53ba69868..7f67f5d0a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -101,7 +101,8 @@ tempfile = "3.1.0"
fn-error-context = "0.2.0"
# Templating
-tera = { version = "1.5.0", features = ["builtins"] }
+# tera = { version = "1.5.0", features = ["builtins"] }
+askama = { path = "/home/imperio/rust/askama/askama" } # { git = "https://github.com/djc/askama" }
walkdir = "2"
# Date and Time utilities
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/utils/html.rs b/src/utils/html.rs
index 2f015ae63..75a987412 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 askama::Template;
use lol_html::element;
use lol_html::errors::RewritingError;
-use tera::Context;
/// 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 askama 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 119414f0d..a7f301303 100644
--- a/src/web/build_details.rs
+++ b/src/web/build_details.rs
@@ -5,11 +5,13 @@ use crate::{
error::{AxumNope, AxumResult},
extractors::{DbConnection, Path},
file::File,
+ filters,
MetaData,
},
AsyncStorage, Config,
};
use anyhow::Context as _;
+use askama::Template;
use axum::{extract::Extension, response::IntoResponse};
use chrono::{DateTime, Utc};
use futures_util::TryStreamExt;
@@ -27,6 +29,8 @@ pub(crate) struct BuildDetails {
output: String,
}
+#[derive(Template)]
+#[template(path = "crate/build_details.html")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct BuildDetailsPage {
metadata: MetaData,
@@ -34,11 +38,10 @@ struct BuildDetailsPage {
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 }
#[derive(Clone, Deserialize, Debug)]
pub(crate) struct BuildDetailsParams {
@@ -116,6 +119,7 @@ pub(crate) async fn build_details_handler(
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 d79b7bbc4..4c3dc1c44 100644
--- a/src/web/builds.rs
+++ b/src/web/builds.rs
@@ -6,11 +6,13 @@ use crate::{
web::{
error::AxumResult,
extractors::{DbConnection, Path},
+ filters,
match_version, MetaData, ReqVersion,
},
Config,
};
use anyhow::Result;
+use askama::Template;
use axum::{
extract::Extension, http::header::ACCESS_CONTROL_ALLOW_ORIGIN, response::IntoResponse, Json,
};
@@ -28,6 +30,8 @@ pub(crate) struct Build {
build_time: DateTime,
}
+#[derive(Template)]
+#[template(path = "crate/builds.html")]
#[derive(Debug, Clone, Serialize)]
struct BuildsPage {
metadata: MetaData,
@@ -35,11 +39,10 @@ struct BuildsPage {
limits: Limits,
canonical_url: CanonicalUrl,
use_direct_platform_links: bool,
+ csp_nonce: String,
}
-impl_axum_webpage! {
- BuildsPage = "crate/builds.html",
-}
+impl_axum_webpage! { BuildsPage }
pub(crate) async fn build_list_handler(
Path((name, req_version)): Path<(String, ReqVersion)>,
@@ -63,6 +66,7 @@ pub(crate) async fn build_list_handler(
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())
}
diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs
index be079ff41..d995e0d57 100644
--- a/src/web/crate_details.rs
+++ b/src/web/crate_details.rs
@@ -10,11 +10,13 @@ use crate::{
encode_url_path,
error::{AxumNope, AxumResult},
extractors::{DbConnection, Path},
+ page::templates::filters,
MatchedRelease, ReqVersion,
},
AsyncStorage,
};
use anyhow::{anyhow, Context, Result};
+use askama::Template;
use axum::{
extract::Extension,
response::{IntoResponse, Response as AxumResponse},
@@ -32,11 +34,11 @@ use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) struct CrateDetails {
- name: String,
+ pub name: String,
pub version: Version,
- description: Option,
- owners: Vec<(String, String)>,
- dependencies: Option,
+ pub description: Option,
+ pub owners: Vec<(String, String)>,
+ pub dependencies: Option,
#[serde(serialize_with = "optional_markdown")]
readme: Option,
#[serde(serialize_with = "optional_markdown")]
@@ -47,19 +49,19 @@ pub(crate) struct CrateDetails {
last_successful_build: Option,
pub rustdoc_status: bool,
pub archive_storage: bool,
- repository_url: Option,
- homepage_url: Option,
+ pub repository_url: Option,
+ pub homepage_url: Option,
keywords: Option,
have_examples: bool, // need to check this manually
pub target_name: String,
releases: Vec,
repository_metadata: Option,
- pub(crate) metadata: MetaData,
+ pub metadata: MetaData,
is_library: bool,
- license: Option,
- pub(crate) documentation_url: Option,
- total_items: Option,
- documented_items: Option,
+ pub license: Option,
+ pub documentation_url: Option,
+ pub total_items: Option,
+ pub documented_items: Option,
total_items_needing_examples: Option,
items_with_examples: Option,
/// Database id for this crate
@@ -417,13 +419,16 @@ pub(crate) async fn releases_for_crate(
Ok(releases)
}
+#[derive(Template)]
+#[template(path = "crate/details.html")]
#[derive(Debug, Clone, PartialEq, Serialize)]
struct CrateDetailsPage {
details: CrateDetails,
+ csp_nonce: String,
}
impl_axum_webpage! {
- CrateDetailsPage = "crate/details.html",
+ CrateDetailsPage,
cpu_intensive_rendering = true,
}
@@ -463,7 +468,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
@@ -473,16 +482,19 @@ pub(crate) async fn crate_details_handler(
Ok(res.into_response())
}
+#[derive(Template)]
+#[template(path = "rustdoc/releases.html")]
#[derive(Debug, Clone, PartialEq, Serialize)]
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,
}
@@ -547,6 +559,7 @@ pub(crate) async fn get_all_releases(
target,
inner_path,
crate_name: params.name,
+ csp_nonce: String::new(),
};
Ok(res.into_response())
}
@@ -559,16 +572,19 @@ struct ShortMetadata {
doc_targets: Vec,
}
+#[derive(Template)]
+#[template(path = "rustdoc/platforms.html")]
#[derive(Debug, Clone, PartialEq, Serialize)]
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,
}
@@ -669,6 +685,7 @@ pub(crate) async fn get_all_platforms_inner(
inner_path,
use_direct_platform_links: is_crate_root,
current_target,
+ csp_nonce: String::new(),
};
Ok(res.into_response())
}
diff --git a/src/web/error.rs b/src/web/error.rs
index 63d5ebdc4..1f2ec322b 100644
--- a/src/web/error.rs
+++ b/src/web/error.rs
@@ -41,6 +41,7 @@ impl IntoResponse for AxumNope {
title: "The requested resource does not exist",
message: "no such resource".into(),
status: StatusCode::NOT_FOUND,
+ csp_nonce: String::new(),
}
.into_response()
}
@@ -49,6 +50,7 @@ impl IntoResponse for AxumNope {
title: "The requested build does not exist",
message: "no such build".into(),
status: StatusCode::NOT_FOUND,
+ csp_nonce: String::new(),
}
.into_response(),
@@ -59,6 +61,7 @@ impl IntoResponse for AxumNope {
title: "The requested crate does not exist",
message: "no such crate".into(),
status: StatusCode::NOT_FOUND,
+ csp_nonce: String::new(),
}
.into_response()
}
@@ -67,6 +70,7 @@ impl IntoResponse for AxumNope {
title: "The requested owner does not exist",
message: "no such owner".into(),
status: StatusCode::NOT_FOUND,
+ csp_nonce: String::new(),
}
.into_response(),
@@ -77,6 +81,7 @@ impl IntoResponse for AxumNope {
title: "The requested version does not exist",
message: "no such version for this crate".into(),
status: StatusCode::NOT_FOUND,
+ csp_nonce: String::new(),
}
.into_response()
}
@@ -93,6 +98,7 @@ impl IntoResponse for AxumNope {
title: "Bad request",
message: Cow::Owned(source.to_string()),
status: StatusCode::BAD_REQUEST,
+ csp_nonce: String::new(),
}
.into_response(),
AxumNope::InternalError(source) => {
@@ -100,6 +106,7 @@ impl IntoResponse for AxumNope {
title: "Internal Server Error",
message: Cow::Owned(source.to_string()),
status: StatusCode::INTERNAL_SERVER_ERROR,
+ csp_nonce: String::new(),
};
crate::utils::report_error(&source);
diff --git a/src/web/features.rs b/src/web/features.rs
index 047efcc9a..78ccf312f 100644
--- a/src/web/features.rs
+++ b/src/web/features.rs
@@ -5,17 +5,21 @@ use crate::{
cache::CachePolicy,
error::{AxumNope, AxumResult},
extractors::{DbConnection, Path},
+ filters,
headers::CanonicalUrl,
match_version, MetaData, ReqVersion,
},
};
use anyhow::anyhow;
+use askama::Template;
use axum::response::IntoResponse;
use serde::Serialize;
use std::collections::{HashMap, VecDeque};
const DEFAULT_NAME: &str = "default";
+#[derive(Template)]
+#[template(path = "crate/features.html")]
#[derive(Debug, Clone, Serialize)]
struct FeaturesPage {
metadata: MetaData,
@@ -24,10 +28,11 @@ struct FeaturesPage {
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 {
@@ -82,6 +87,7 @@ pub(crate) async fn build_features_handler(
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..7e749306f 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,15 +53,21 @@ impl Header for CanonicalUrl {
}
}
-impl Serialize for CanonicalUrl {
- fn serialize(&self, serializer: S) -> Result
- where
- S: serde::Serializer,
- {
- serializer.serialize_str(&self.build_full_uri().to_string())
+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
+// S: serde::Serializer,
+// {
+// serializer.serialize_str(&self.build_full_uri().to_string())
+// }
+// }
+
#[cfg(test)]
mod tests {
use super::*;
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 2db225a02..e42376e29 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -2,9 +2,13 @@
pub mod page;
+// mod tmp;
+
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 askama::Template;
use axum_extra::middleware::option_layer;
use serde_json::Value;
use tracing::{info, instrument};
@@ -24,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;
@@ -696,6 +700,8 @@ impl MetaData {
}
}
+#[derive(Template)]
+#[template(path = "error.html")]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) struct AxumErrorPage {
/// The title of the page
@@ -704,10 +710,11 @@ pub(crate) struct AxumErrorPage {
pub message: Cow<'static, str>,
#[serde(skip)]
pub status: StatusCode,
+ pub csp_nonce: String,
}
impl_axum_webpage! {
- AxumErrorPage = "error.html",
+ AxumErrorPage,
status = |err| err.status,
}
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..6729c4340 100644
--- a/src/web/page/templates.rs
+++ b/src/web/page/templates.rs
@@ -1,19 +1,42 @@
use crate::error::Result;
+use crate::web::rustdoc::RustdocPage;
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 askama::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) => {
+ #[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 }
+ }
+ }
+
+ 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");
/// 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 +54,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 +73,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 +85,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,173 +94,207 @@ 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)?;
+pub mod filters {
+ use super::IconType;
+ use chrono::{DateTime, Utc};
+ use std::borrow::Cow;
+ use std::fmt;
+ use std::str::FromStr;
+
+ // Copied from `tera`.
+ #[inline]
+ pub fn escape_html(input: &str) -> askama::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),
+ }
+ }
- let mut files = Vec::new();
- for entry in WalkDir::new(&root) {
- let entry = entry?;
- let path = entry.path();
+ // Not using shrink_to_fit() on purpose
+ Ok(Cow::Owned(output))
+ }
- if !entry.metadata()?.is_file() {
- continue;
+ // Copied from `tera`.
+ #[inline]
+ pub fn escape_xml(input: &str) -> askama::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))
+ }
- // 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())));
+ /// Prettily format a timestamp
+ // TODO: This can be replaced by chrono
+ #[allow(clippy::unnecessary_wraps)]
+ pub fn timeformat(value: &str, is_relative: bool) -> askama::Result {
+ let fmt = if is_relative {
+ let value = DateTime::parse_from_rfc3339(value)
+ .unwrap()
+ .with_timezone(&Utc);
+
+ crate::web::duration_to_str(value)
+ } else {
+ const TIMES: &[&str] = &["seconds", "minutes", "hours"];
+
+ let mut value = f64::from_str(value).unwrap();
+ let mut chosen_time = &TIMES[0];
+
+ for time in &TIMES[1..] {
+ if value / 60.0 >= 1.0 {
+ chosen_time = time;
+ value /= 60.0;
+ } else {
+ break;
+ }
+ }
+
+ // TODO: This formatting section can be optimized, two string allocations aren't needed
+ let mut value = format!("{value:.1}");
+ if value.ends_with(".0") {
+ value.truncate(value.len() - 2);
+ }
+
+ format!("{value} {chosen_time}")
+ };
+
+ Ok(fmt)
}
- Ok(files)
-}
+ /// Print a value to stdout
+ #[allow(clippy::unnecessary_wraps)]
+ pub fn dbg(value: T) -> askama::Result {
+ let value = value.to_string();
+ println!("{value}");
-/// Simple function that returns the pre-defined value.
-struct ReturnValue {
- name: &'static str,
- value: Value,
-}
+ Ok(value)
+ }
-impl ReturnValue {
- fn add_function_to(tera: &mut Tera, name: &'static str, value: Value) {
- tera.register_function(name, Self { name, value })
+ /// Dedent a string by removing all leading whitespace
+ #[allow(clippy::unnecessary_wraps)]
+ pub fn dedent(
+ value: T,
+ levels: Option,
+ ) -> askama::Result {
+ let string = value.to_string();
+
+ let unindented = if let Some(levels) = levels {
+ 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;
+ }
+ }
+
+ line
+ })
+ .collect::>()
+ .join("\n")
+ } else {
+ string
+ .lines()
+ .map(|l| l.trim_start())
+ .collect::>()
+ .join("\n")
+ };
+
+ Ok(unindented)
}
-}
-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())
+ pub fn fas(value: &str, fw: bool, spin: bool, extra: &str) -> askama::Result {
+ IconType::Strong.render(value, fw, spin, extra)
}
-}
-/// 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 {
- const TIMES: &[&str] = &["seconds", "minutes", "hours"];
-
- let mut value = value.as_f64().unwrap();
- let mut chosen_time = &TIMES[0];
-
- for time in &TIMES[1..] {
- if value / 60.0 >= 1.0 {
- chosen_time = time;
- value /= 60.0;
- } else {
- break;
- }
- }
+ pub fn far(value: &str, fw: bool, spin: bool, extra: &str) -> askama::Result {
+ IconType::Regular.render(value, fw, spin, extra)
+ }
- // TODO: This formatting section can be optimized, two string allocations aren't needed
- let mut value = format!("{value:.1}");
- if value.ends_with(".0") {
- value.truncate(value.len() - 2);
- }
+ pub fn fab(value: &str, fw: bool, spin: bool, extra: &str) -> askama::Result {
+ IconType::Brand.render(value, fw, spin, extra)
+ }
- format!("{value} {chosen_time}")
- };
+ pub fn highlight(code: &str, lang: &str) -> askama::Result {
+ let highlighted_code = crate::web::highlight::with_lang(Some(lang), code);
+ Ok(format!("{}
", highlighted_code))
+ }
- Ok(Value::String(fmt))
-}
+ pub fn slugify>(code: T) -> askama::Result {
+ Ok(slug::slugify(code.as_ref()))
+ }
-/// Print a tera value to stdout
-#[allow(clippy::unnecessary_wraps)]
-fn dbg(value: &Value, _args: &HashMap) -> TeraResult {
- println!("{value:?}");
+ pub fn round(value: &f32, precision: u32) -> askama::Result {
+ struct Rounder(f32, u32);
- Ok(value.clone())
-}
+ impl fmt::Display for Rounder {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(formatter, "{1:.*}", self.1 as usize, self.0)
+ }
+ }
+ Ok(format!("{}", Rounder(*value, precision)))
+ }
-/// 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 date(value: &DateTime, format: &str) -> askama::Result {
+ Ok(format!("{}", value.format(format)))
+ }
- 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 filesizeformat(num: usize) -> askama::Result {
+ if num > 1_000_000_000 {
+ Ok(format!("{}.{} GB", num / 1_000_000_000, num / 100_000_000 % 10))
+ } else if num > 1_000_000 {
+ Ok(format!("{}.{} MB", num / 1_000_000, num / 100_000 % 10))
+ } else if num > 1_000 {
+ Ok(format!("{}.{} KB", num / 1_000, num / 100 % 10))
+ } else {
+ Ok(format!("{num} B"))
+ }
+ }
- line
- })
- .collect::>()
- .join("\n")
- } else {
- string
- .lines()
- .map(|l| l.trim_start())
- .collect::>()
- .join("\n")
- };
+ pub fn unwrap(value: &Option) -> askama::Result<&T> {
+ Ok(value.as_ref().expect("`unwrap` filter failed"))
+ }
+
+ pub fn split_first<'a>(value: &'a str, pat: &str) -> askama::Result> {
+ Ok(value.split(pat).next())
+ }
+
+ pub fn to_string(value: &T) -> askama::Result {
+ Ok(value.to_string())
+ }
- Ok(Value::String(unindented))
+ pub fn json_encode(value: &T) -> askama::Result {
+ Ok(serde_json::to_string(value).expect("`encode_json` failed"))
+ }
+
+ pub fn as_f32(value: &i32) -> askama::Result {
+ Ok(*value as f32)
+ }
}
enum IconType {
@@ -260,9 +315,9 @@ 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) -> askama::Result {
+ let class = if fw { "fa-svg fa-svg-fw" } else { "fa-svg" };
let type_ = match self {
IconType::Strong => font_awesome_as_a_crate::Type::Solid,
@@ -273,14 +328,10 @@ impl tera::Filter for IconType {
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)
- {
+ 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 +340,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..5737daa38 100644
--- a/src/web/page/web_page.rs
+++ b/src/web/page/web_page.rs
@@ -1,6 +1,5 @@
use super::TemplateData;
use crate::web::{csp::Csp, error::AxumNope};
-use anyhow::Error;
use axum::{
body::Body,
extract::Request as AxumRequest,
@@ -10,12 +9,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 set_csp_nonce(&mut self, csp_nonce: String);
+}
#[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 +25,12 @@ 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 set_csp_nonce(&mut self, csp_nonce: String) {
+ self.csp_nonce = csp_nonce;
+ }
+ }
- (
- $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,68 +95,16 @@ 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,
}
fn render_response(
mut response: AxumResponse,
- templates: Arc,
+ _: Arc,
csp_nonce: String,
) -> BoxFuture<'static, AxumResponse> {
- async move {
- if let Some(render) = response.extensions_mut().remove::() {
- let DelayedTemplateRender {
- template,
- mut context,
- cpu_intensive_rendering,
- } = render;
- context.insert("csp_nonce", &csp_nonce);
-
- let rendered = if cpu_intensive_rendering {
- templates
- .render_in_threadpool(move |templates| {
- templates
- .templates
- .render(&template, &context)
- .map_err(Into::into)
- })
- .await
- } else {
- templates
- .templates
- .render(&template, &context)
- .map_err(Error::new)
- };
-
- let rendered = match rendered {
- Ok(content) => content,
- Err(err) => {
- if response.status().is_server_error() {
- // avoid infinite loop if error.html somehow fails to load
- panic!("error while serving error page: {err:?}");
- } else {
- return render_response(
- AxumNope::InternalError(err).into_response(),
- templates,
- csp_nonce,
- )
- .await;
- }
- }
- };
- let content_length = rendered.len();
- *response.body_mut() = Body::from(rendered);
- response
- .headers_mut()
- .insert(CONTENT_LENGTH, content_length.into());
- response
- } else {
- response
- }
- }
- .boxed()
+ async move { response }.boxed()
}
pub(crate) async fn render_templates_middleware(req: AxumRequest, next: Next) -> AxumResponse {
diff --git a/src/web/releases.rs b/src/web/releases.rs
index e7486813a..fca98b3f4 100644
--- a/src/web/releases.rs
+++ b/src/web/releases.rs
@@ -10,11 +10,14 @@ 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,
+ ReqVersion,
},
BuildQueue, Config, InstanceMetrics,
};
use anyhow::{anyhow, bail, Context as _, Result};
+use askama::Template;
use axum::{
extract::{Extension, Query},
response::{IntoResponse, Response as AxumResponse},
@@ -293,13 +296,16 @@ async fn get_search_results(
})
}
+#[derive(Template)]
+#[template(path = "core/home.html")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct HomePage {
recent_releases: Vec,
+ csp_nonce: String,
}
impl_axum_webpage! {
- HomePage = "core/home.html",
+ HomePage,
cache_policy = |_| CachePolicy::ShortInCdnAndBrowser,
}
@@ -307,25 +313,36 @@ pub(crate) async fn home_page(mut conn: DbConnection) -> AxumResult,
+ 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(Template)]
+#[template(path = "releases/releases.html")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct ViewReleases {
releases: Vec,
@@ -335,11 +352,10 @@ 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 }
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
@@ -400,6 +416,7 @@ pub(crate) async fn releases_handler(
show_previous_page,
page_number,
owner: None,
+ csp_nonce: String::new(),
})
}
@@ -439,6 +456,8 @@ pub(crate) async fn owner_handler(Path(owner): Path) -> AxumResult,
counts: Vec,
failures: Vec,
+ csp_nonce: String,
}
-impl_axum_webpage! {
- ReleaseActivity = "releases/activity.html",
-}
+impl_axum_webpage! { ReleaseActivity }
pub(crate) async fn activity_handler(mut conn: DbConnection) -> AxumResult {
let rows: Vec<_> = sqlx::query!(
@@ -724,19 +746,21 @@ 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 }
pub(crate) async fn build_queue_handler(
Extension(build_queue): Extension>,
@@ -772,6 +796,7 @@ pub(crate) async fn build_queue_handler(
description: "crate documentation scheduled to build & deploy",
queue,
active_deployments,
+ csp_nonce: String::new(),
})
}
diff --git a/src/web/routes.rs b/src/web/routes.rs
index ced014a42..02df8ac08 100644
--- a/src/web/routes.rs
+++ b/src/web/routes.rs
@@ -1,6 +1,7 @@
use super::{
cache::CachePolicy, error::AxumNope, metrics::request_recorder, statics::build_static_router,
};
+use askama::Template;
use axum::{
extract::Request as AxumHttpRequest,
handler::Handler as AxumHandler,
@@ -271,13 +272,19 @@ pub(super) fn build_axum_routes() -> AxumRouter {
.route(
"/-/storage-change-detection.html",
get_internal(|| async {
+ #[derive(Template)]
+ #[template(path = "storage-change-detection.html")]
#[derive(Debug, Clone, serde::Serialize)]
- struct StorageChangeDetection {}
+ 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 56a0986e5..5621a7884 100644
--- a/src/web/rustdoc.rs
+++ b/src/web/rustdoc.rs
@@ -257,21 +257,21 @@ 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,
+pub struct RustdocPage {
+ pub latest_path: String,
+ pub permalink_path: String,
+ pub latest_version: String,
+ pub target: 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 {
@@ -279,20 +279,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();
@@ -593,7 +588,7 @@ pub(crate) async fn rustdoc_html_server_handler(
templates
.render_in_threadpool({
let metrics = metrics.clone();
- move |templates| {
+ move || {
let metadata = krate.metadata.clone();
Ok(RustdocPage {
latest_path,
@@ -611,7 +606,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 bc9927efb..6b311f176 100644
--- a/src/web/sitemap.rs
+++ b/src/web/sitemap.rs
@@ -6,31 +6,43 @@ use crate::{
web::{
error::{AxumNope, AxumResult},
extractors::{DbConnection, Path},
+ page::templates::filters,
AxumErrorPage,
},
Config,
};
-use axum::{extract::Extension, http::StatusCode, response::IntoResponse};
+use askama::Template;
+use axum::{
+ extract::Extension,
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
use chrono::{TimeZone, Utc};
use futures_util::stream::TryStreamExt;
use serde::Serialize;
use std::sync::Arc;
/// sitemap index
+#[derive(Template)]
+#[template(path = "core/sitemapindex.xml")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
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)]
@@ -41,13 +53,16 @@ struct SitemapRow {
}
/// The sitemap
+#[derive(Template)]
+#[template(path = "core/sitemap.xml")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct SitemapXml {
releases: Vec,
+ csp_nonce: String,
}
impl_axum_webpage! {
- SitemapXml = "core/sitemap.xml",
+ SitemapXml,
content_type = "application/xml",
}
@@ -91,9 +106,14 @@ pub(crate) async fn sitemap_handler(
.try_collect()
.await?;
- Ok(SitemapXml { releases })
+ Ok(SitemapXml {
+ releases,
+ csp_nonce: String::new(),
+ })
}
+#[derive(Template)]
+#[template(path = "core/about/builds.html")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct AboutBuilds {
/// The current version of rustc that docs.rs is using to build crates
@@ -102,9 +122,10 @@ struct AboutBuilds {
limits: Limits,
/// Just for the template, since this isn't shared with AboutPage
active_tab: &'static str,
+ csp_nonce: String,
}
-impl_axum_webpage!(AboutBuilds = "core/about/builds.html");
+impl_axum_webpage!(AboutBuilds);
pub(crate) async fn about_builds_handler(
Extension(pool): Extension,
@@ -120,17 +141,42 @@ 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,
+ }
+
+ impl $ty {
+ fn into_response(&self) -> Response {
+ // match self.render() {
+ // Ok(body) => {
+ // let headers = [(
+ // http::header::CONTENT_TYPE,
+ // http::HeaderValue::from_static(::MIME_TYPE),
+ // )];
+
+ // (headers, body).into_response()
+ // }
+ // Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
+ // }
+ StatusCode::INTERNAL_SERVER_ERROR.into_response()
+ }
+ }
+ };
}
-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 {
@@ -138,9 +184,27 @@ 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",
+ }
+ .into_response(),
+ "badges" => AboutPageBadges {
+ active_tab: "badges",
+ }
+ .into_response(),
+ "metadata" => AboutPageMetadata {
+ active_tab: "metadata",
+ }
+ .into_response(),
+ "redirections" => AboutPageRedirection {
+ active_tab: "redirections",
+ }
+ .into_response(),
+ "download" => AboutPageDownload {
+ active_tab: "download",
+ }
+ .into_response(),
_ => {
let msg = "This /about page does not exist. \
Perhaps you are interested in creating it?";
@@ -148,16 +212,12 @@ pub(crate) async fn about_handler(subpage: Option>) -> AxumResultDocs.rs documentation
diff --git a/templates/base.html b/templates/base.html
index 6f3fb9ba8..9ead709a1 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -6,12 +6,12 @@
-
+
{%- block meta -%}{%- endblock meta -%}
{# Docs.rs styles #}
-
-
+
+
@@ -20,8 +20,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..3de59502f 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 -%}
@@ -31,20 +31,20 @@ 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 highlight("rust")|dedent(3) %}
#[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 highlight("rust")|dedent(3) -%}
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 @@
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 highlight("toml")|dedent -%}
[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 .
- {%- 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 @@
- {{ 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: