diff --git a/Cargo.lock b/Cargo.lock
index 39d7fc72d..6562117f5 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.52",
+]
+
+[[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"
@@ -802,6 +838,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"
@@ -857,9 +902,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.15.3"
+version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
+checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
[[package]]
name = "byteorder"
@@ -912,9 +957,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
-version = "1.0.89"
+version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723"
+checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
dependencies = [
"jobserver",
"libc",
@@ -934,9 +979,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "chrono"
-version = "0.4.34"
+version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
+checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -945,28 +990,6 @@ dependencies = [
"windows-targets 0.52.4",
]
-[[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"
@@ -996,9 +1019,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.1"
+version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
+checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1006,9 +1029,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.1"
+version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
@@ -1579,6 +1602,7 @@ name = "docs-rs"
version = "0.6.0"
dependencies = [
"anyhow",
+ "askama",
"async-stream",
"aws-config",
"aws-sdk-cloudfront",
@@ -1653,7 +1677,6 @@ dependencies = [
"strum",
"syntect",
"tempfile",
- "tera",
"test-case",
"thiserror",
"thread_local",
@@ -2884,30 +2907,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.2",
-]
-
-[[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"
@@ -3340,22 +3339,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"
@@ -3635,9 +3618,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "lol_html"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10662f7aad081ec900fd735be33076da75e0389400277dc3734e2b0aa02bb115"
+checksum = "a4629ff9c2deeb7aad9b2d0f379fc41937a02f3b739f007732c46af40339dee5"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
@@ -3648,7 +3631,6 @@ dependencies = [
"lazycell",
"memchr",
"mime",
- "safemem",
"selectors",
"thiserror",
]
@@ -4101,15 +4083,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"
@@ -4137,51 +4110,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-[[package]]
-name = "pest"
-version = "2.7.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8"
-dependencies = [
- "memchr",
- "thiserror",
- "ucd-trie",
-]
-
-[[package]]
-name = "pest_derive"
-version = "2.7.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026"
-dependencies = [
- "pest",
- "pest_generator",
-]
-
-[[package]]
-name = "pest_generator"
-version = "2.7.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80"
-dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn 2.0.52",
-]
-
-[[package]]
-name = "pest_meta"
-version = "2.7.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293"
-dependencies = [
- "once_cell",
- "pest",
- "sha2",
-]
-
[[package]]
name = "phf"
version = "0.8.0"
@@ -4232,16 +4160,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"
@@ -4789,9 +4707,9 @@ dependencies = [
[[package]]
name = "reqwest"
-version = "0.11.24"
+version = "0.11.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
+checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946"
dependencies = [
"async-compression",
"base64 0.21.7",
@@ -5034,12 +4952,6 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
-[[package]]
-name = "safemem"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
-
[[package]]
name = "same-file"
version = "1.0.6"
@@ -5315,9 +5227,9 @@ dependencies = [
[[package]]
name = "serde_path_to_error"
-version = "0.1.15"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c"
+checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
dependencies = [
"itoa 1.0.10",
"serde",
@@ -5844,18 +5756,18 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "strum"
-version = "0.26.1"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
+checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
-version = "0.26.1"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
+checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [
"heck",
"proc-macro2",
@@ -5933,20 +5845,20 @@ dependencies = [
[[package]]
name = "system-configuration"
-version = "0.5.1"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42"
dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.4.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
-version = "0.5.0"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
@@ -5986,28 +5898,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"
@@ -6425,12 +6315,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.0.0"
@@ -6449,56 +6333,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"
@@ -6751,9 +6585,9 @@ dependencies = [
[[package]]
name = "whoami"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fec781d48b41f8163426ed18e8fc2864c12937df9ce54c88ede7bd47270893e"
+checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
dependencies = [
"redox_syscall",
"wasite",
diff --git a/Cargo.toml b/Cargo.toml
index 08500bd24..f0e05b86f 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/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 9c2060e1e..4649970c7 100644
--- a/src/web/build_details.rs
+++ b/src/web/build_details.rs
@@ -9,6 +9,7 @@ use crate::{
AsyncStorage, Config,
};
use anyhow::Context as _;
+use askama::Template;
use axum::{extract::Extension, response::IntoResponse};
use chrono::{DateTime, Utc};
use futures_util::TryStreamExt;
@@ -26,6 +27,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,
@@ -33,11 +36,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 {
@@ -115,6 +117,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 1f4621c0f..50c28c53a 100644
--- a/src/web/builds.rs
+++ b/src/web/builds.rs
@@ -10,6 +10,7 @@ use crate::{
Config,
};
use anyhow::Result;
+use askama::Template;
use axum::{
extract::Extension, http::header::ACCESS_CONTROL_ALLOW_ORIGIN, response::IntoResponse, Json,
};
@@ -27,6 +28,8 @@ pub(crate) struct Build {
build_time: DateTime,
}
+#[derive(Template)]
+#[template(path = "crate/builds.html")]
#[derive(Debug, Clone, Serialize)]
struct BuildsPage {
metadata: MetaData,
@@ -34,11 +37,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)>,
@@ -62,6 +64,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 95c364fa0..2435f2c21 100644
--- a/src/web/crate_details.rs
+++ b/src/web/crate_details.rs
@@ -9,11 +9,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},
@@ -383,13 +385,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,
}
@@ -429,7 +434,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
@@ -439,16 +448,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,
}
@@ -513,6 +525,7 @@ pub(crate) async fn get_all_releases(
target,
inner_path,
crate_name: params.name,
+ csp_nonce: String::new(),
};
Ok(res.into_response())
}
@@ -525,16 +538,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,
}
@@ -635,6 +651,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 c63e75dc2..82012a93b 100644
--- a/src/web/features.rs
+++ b/src/web/features.rs
@@ -10,12 +10,15 @@ use crate::{
},
};
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 +27,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 +86,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/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 81be7fc22..df4e87132 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -4,7 +4,9 @@ pub mod page;
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 +26,7 @@ mod markdown;
pub(crate) mod metrics;
mod releases;
mod routes;
-mod rustdoc;
+pub(crate) mod rustdoc;
mod sitemap;
mod source;
mod statics;
@@ -684,6 +686,8 @@ impl MetaData {
}
}
+#[derive(Template)]
+#[template(path = "error.html")]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) struct AxumErrorPage {
/// The title of the page
@@ -692,10 +696,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 9c15360e2..2c730b22a 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,176 @@ 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::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));
}
-
- // 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())));
+ 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))
}
- Ok(files)
-}
+ /// Prettily format a timestamp
+ // TODO: This can be replaced by chrono
+ #[allow(clippy::unnecessary_wraps)]
+ pub fn timeformat(value: &str, is_relative: Option) -> askama::Result {
+ let fmt = if let Some(true) = 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;
+ }
+ }
-/// Simple function that returns the pre-defined value.
-struct ReturnValue {
- name: &'static str,
- value: Value,
-}
+ // 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);
+ }
-impl ReturnValue {
- fn add_function_to(tera: &mut Tera, name: &'static str, value: Value) {
- tera.register_function(name, Self { name, value })
+ format!("{value} {chosen_time}")
+ };
+
+ Ok(fmt)
}
-}
-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())
+ /// Print a value to stdout
+ #[allow(clippy::unnecessary_wraps)]
+ pub fn dbg(value: T) -> askama::Result {
+ let value = value.to_string();
+ println!("{value}");
+
+ Ok(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 {
- 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;
- }
- }
+ /// Dedent a string by removing all leading whitespace
+ #[allow(clippy::unnecessary_wraps)]
+ pub fn dedent(
+ value: String,
+ 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;
+ }
+ }
- // 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);
- }
+ line
+ })
+ .collect::>()
+ .join("\n")
+ } else {
+ string
+ .lines()
+ .map(|l| l.trim_start())
+ .collect::>()
+ .join("\n")
+ };
- format!("{value} {chosen_time}")
- };
+ Ok(unindented)
+ }
- Ok(Value::String(fmt))
-}
+ pub fn fas(value: &str, fw: bool, extra: &str) -> askama::Result {
+ IconType::Strong.render(value, fw, extra)
+ }
-/// Print a tera value to stdout
-#[allow(clippy::unnecessary_wraps)]
-fn dbg(value: &Value, _args: &HashMap) -> TeraResult {
- println!("{value:?}");
+ pub fn far(value: &str, fw: bool, extra: &str) -> askama::Result {
+ IconType::Regular.render(value, fw, extra)
+ }
- Ok(value.clone())
-}
+ pub fn fab(value: &str, fw: bool, extra: &str) -> askama::Result {
+ IconType::Brand.render(value, fw, extra)
+ }
+
+ pub fn highlight(code: &str, lang: &str) -> askama::Result {
+ let highlighted_code = crate::web::highlight::with_lang(Some(lang), code);
+ Ok(format!("{}
", highlighted_code))
+ }
-/// 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 slugify>(code: T) -> askama::Result {
+ Ok(slug::slugify(code.as_ref()))
+ }
- 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 round(value: f32, precision: u32) -> askama::Result {
+ use std::fmt;
- line
- })
- .collect::>()
- .join("\n")
- } else {
- string
- .lines()
- .map(|l| l.trim_start())
- .collect::>()
- .join("\n")
- };
+ struct Rounder(f32, u32);
- Ok(Value::String(unindented))
+ 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)))
+ }
+
+ pub fn date(value: &DateTime, format: &str) -> askama::Result {
+ Ok(format!("{}", value.format(format)))
+ }
}
enum IconType {
@@ -260,9 +284,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, 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,
@@ -271,65 +295,14 @@ impl tera::Filter for IconType {
};
let icon_file_string = font_awesome_as_a_crate::svg(type_, &icon_name[..]).unwrap_or("");
- let (space, class_extra) = match args.get("extra").and_then(|s| s.as_str()) {
- Some(extra) => (" ", extra),
- None => ("", ""),
+ let (space, class_extra) = if !extra.is_empty() {
+ (" ", extra)
+ } else {
+ ("", "")
};
- let icon = format!(
- "\
-{icon_file_string} "
- );
+ let icon = format!("");
- 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 16988d059..3293eeaec 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!(
@@ -718,19 +740,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>,
@@ -766,6 +790,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 392515100..39c948705 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,
@@ -299,13 +300,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 25cb6f2a2..4063895ae 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: