",
- root = page.root_path,
- path = ensure_trailing_slash(&layout.krate),
- logo = layout.logo
- )
- }
- },
- title = page.title,
- description = Escape(page.description),
- keywords = page.keywords,
- favicon = if layout.favicon.is_empty() {
- format!(
- r##"
-
-"##,
- static_root_path = static_root_path,
- suffix = page.resource_suffix
- )
- } else {
- format!(r#""#, layout.favicon)
- },
- in_header = layout.external_html.in_header,
- before_content = layout.external_html.before_content,
- after_content = layout.external_html.after_content,
- sidebar = Buffer::html().to_display(sidebar),
- krate = layout.krate,
- default_settings = layout
- .default_settings
- .iter()
- .map(|(k, v)| format!(r#" data-{}="{}""#, k.replace('-', "_"), Escape(v)))
- .collect::(),
- style_files = style_files
- .iter()
- .filter_map(|t| {
- if let Some(stem) = t.path.file_stem() { Some((stem, t.disabled)) } else { None }
- })
- .filter_map(|t| {
- if let Some(path) = t.0.to_str() { Some((path, t.1)) } else { None }
- })
- .map(|t| format!(
r#""#,
Escape(&format!("{}{}{}", static_root_path, t.0, page.resource_suffix)),
if t.1 { "disabled" } else { "" },
if t.0 == "light" { "id=\"themeStyle\"" } else { "" }
- ))
- .collect::(),
- suffix = page.resource_suffix,
- extra_scripts = page
- .static_extra_scripts
- .iter()
- .map(|e| {
- format!(
- "",
- static_root_path = static_root_path,
- extra_script = e
- )
- })
- .chain(page.extra_scripts.iter().map(|e| {
- format!(
- "",
- root_path = page.root_path,
- extra_script = e
- )
- }))
- .collect::(),
- filter_crates = if layout.generate_search_filter {
- ""
- } else {
- ""
- },
- )
+ )
+ })
+ .collect::();
+ let content = Buffer::html().to_display(t); // Note: This must happen before making the sidebar.
+ let sidebar = Buffer::html().to_display(sidebar);
+ let teractx = tera::Context::from_serialize(PageLayout {
+ static_root_path,
+ page,
+ layout,
+ style_files,
+ sidebar,
+ content,
+ krate_with_trailing_slash,
+ })
+ .unwrap();
+ templates.render("page.html", &teractx).unwrap()
}
crate fn redirect(url: &str) -> String {
diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs
index 1898f5feed2c..2085739fc46e 100644
--- a/src/librustdoc/html/render/context.rs
+++ b/src/librustdoc/html/render/context.rs
@@ -1,5 +1,6 @@
use std::cell::RefCell;
use std::collections::BTreeMap;
+use std::error::Error as StdError;
use std::io;
use std::path::{Path, PathBuf};
use std::rc::Rc;
@@ -29,6 +30,7 @@ use crate::formats::FormatRenderer;
use crate::html::escape::Escape;
use crate::html::format::Buffer;
use crate::html::markdown::{self, plain_text_summary, ErrorCodes, IdMap};
+use crate::html::static_files::PAGE;
use crate::html::{layout, sources};
/// Major driving force in all rustdoc rendering. This contains information
@@ -121,6 +123,8 @@ crate struct SharedContext<'tcx> {
/// to `Some(...)`, it'll store redirections and then generate a JSON file at the top level of
/// the crate.
redirections: Option>>,
+
+ pub(crate) templates: tera::Tera,
}
impl SharedContext<'_> {
@@ -218,6 +222,7 @@ impl<'tcx> Context<'tcx> {
if !self.render_redirect_pages {
layout::render(
+ &self.shared.templates,
&self.shared.layout,
&page,
|buf: &mut _| print_sidebar(self, it, buf),
@@ -408,6 +413,12 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
let mut issue_tracker_base_url = None;
let mut include_sources = true;
+ let mut templates = tera::Tera::default();
+ templates.add_raw_template("page.html", PAGE).map_err(|e| Error {
+ file: "page.html".into(),
+ error: format!("{}: {}", e, e.source().map(|e| e.to_string()).unwrap_or_default()),
+ })?;
+
// Crawl the crate attributes looking for attributes which control how we're
// going to emit HTML
for attr in krate.module.attrs.lists(sym::doc) {
@@ -454,6 +465,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
errors: receiver,
redirections: if generate_redirect_map { Some(Default::default()) } else { None },
show_type_layout,
+ templates,
};
// Add the default themes to the `Vec` of stylepaths
@@ -540,6 +552,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
};
let all = self.shared.all.replace(AllTypes::new());
let v = layout::render(
+ &self.shared.templates,
&self.shared.layout,
&page,
sidebar,
@@ -557,6 +570,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
let sidebar = "
Settings
";
style_files.push(StylePath { path: PathBuf::from("settings.css"), disabled: false });
let v = layout::render(
+ &self.shared.templates,
&self.shared.layout,
&page,
sidebar,
diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs
index a4188e6b203b..840566731d78 100644
--- a/src/librustdoc/html/render/write_shared.rs
+++ b/src/librustdoc/html/render/write_shared.rs
@@ -460,7 +460,14 @@ pub(super) fn write_shared(
})
.collect::()
);
- let v = layout::render(&cx.shared.layout, &page, "", content, &cx.shared.style_files);
+ let v = layout::render(
+ &cx.shared.templates,
+ &cx.shared.layout,
+ &page,
+ "",
+ content,
+ &cx.shared.style_files,
+ );
cx.shared.fs.write(&dst, v.as_bytes())?;
}
}
diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs
index 5e2a94fe6845..80dd7a7a952f 100644
--- a/src/librustdoc/html/sources.rs
+++ b/src/librustdoc/html/sources.rs
@@ -136,6 +136,7 @@ impl SourceCollector<'_, 'tcx> {
static_extra_scripts: &[&format!("source-script{}", self.scx.resource_suffix)],
};
let v = layout::render(
+ &self.scx.templates,
&self.scx.layout,
&page,
"",
diff --git a/src/librustdoc/html/static_files.rs b/src/librustdoc/html/static_files.rs
index ca7e5ef81508..00e13d4ec1aa 100644
--- a/src/librustdoc/html/static_files.rs
+++ b/src/librustdoc/html/static_files.rs
@@ -64,6 +64,8 @@ crate static RUST_FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg");
crate static RUST_FAVICON_PNG_16: &[u8] = include_bytes!("static/favicon-16x16.png");
crate static RUST_FAVICON_PNG_32: &[u8] = include_bytes!("static/favicon-32x32.png");
+crate static PAGE: &str = include_str!("templates/page.html");
+
/// The built-in themes given to every documentation site.
crate mod themes {
/// The "light" theme, selected by default when no setting is available. Used as the basis for
diff --git a/src/librustdoc/html/templates/STYLE.md b/src/librustdoc/html/templates/STYLE.md
new file mode 100644
index 000000000000..fff65e3b5ff2
--- /dev/null
+++ b/src/librustdoc/html/templates/STYLE.md
@@ -0,0 +1,37 @@
+# Style for Templates
+
+This directory has templates in the [Tera templating language](teradoc), which is very
+similar to [Jinja2](jinjadoc) and [Django](djangodoc) templates, and also to [Askama](askamadoc).
+
+[teradoc]: https://tera.netlify.app/docs/#templates
+[jinjadoc]: https://jinja.palletsprojects.com/en/3.0.x/templates/
+[djangodoc]: https://docs.djangoproject.com/en/3.2/topics/templates/
+[askamadoc]: https://docs.rs/askama/0.10.5/askama/
+
+We want our rendered output to have as little unnecessary whitespace as
+possible, so that pages load quickly. To achieve that we use Tera's
+[whitespace control] features. At the end of most lines, we put an empty comment
+tag with the whitespace control characters: `{#- -#}`. This causes all
+whitespace between the end of the line and the beginning of the next, including
+indentation, to be omitted on render. Sometimes we want to preserve a single
+space. In those cases we put the space at the end of the line, followed by
+`{# -#}`, which is a directive to remove following whitespace but not preceding.
+We also use the whitespace control characters in most instances of tags with
+control flow, for example `{%- if foo -%}`.
+
+[whitespace control]: https://tera.netlify.app/docs/#whitespace-control
+
+We want our templates to be readable, so we use indentation and newlines
+liberally. We indent by four spaces after opening an HTML tag _or_ a Tera
+tag. In most cases an HTML tag should be followed by a newline, but if the
+tag has simple contents and fits with its close tag on a single line, the
+contents don't necessarily need a new line.
+
+Tera templates support quite sophisticated control flow. To keep our templates
+simple and understandable, we use only a subset: `if` and `for`. In particular
+we avoid [assignments in the template logic](assignments) and [Tera
+macros](macros). This also may make things easier if we switch to a different
+Jinja-style template system, like Askama, in the future.
+
+[assignments]: https://tera.netlify.app/docs/#assignments
+[macros]: https://tera.netlify.app/docs/#macros
diff --git a/src/librustdoc/html/templates/page.html b/src/librustdoc/html/templates/page.html
new file mode 100644
index 000000000000..9b1bef5e4476
--- /dev/null
+++ b/src/librustdoc/html/templates/page.html
@@ -0,0 +1,119 @@
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {{page.title}} {#- -#}
+ {#- -#}
+ {#- -#}
+ {{- style_files | safe -}}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {%- if layout.css_file_extension -%}
+ {#- -#}
+ {%- endif -%}
+ {%- if layout.favicon -%}
+ {#- -#}
+ {%- else -%}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {%- endif -%}
+ {{- layout.external_html.in_header | safe -}}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {#- -#}
+ {{- layout.external_html.before_content | safe -}}
+ {#- -#}
+