Skip to content

Commit cd0f931

Browse files
committed
Use Tera templates for rustdoc.
Replaces a format!() call in layout::render with a template expansion. Introduces a `templates` field in SharedContext so parts of rustdoc can share pre-rendered templates. This currently builds in a copy of the single template available, like with static files. However, future work can make this live-loadable with a perma-unstable flag, to make rustdoc developers' work easier.
1 parent 50a4072 commit cd0f931

File tree

10 files changed

+252
-176
lines changed

10 files changed

+252
-176
lines changed

Cargo.lock

+27
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,17 @@ dependencies = [
14821482
"regex",
14831483
]
14841484

1485+
[[package]]
1486+
name = "globwalk"
1487+
version = "0.8.1"
1488+
source = "registry+https://github.com/rust-lang/crates.io-index"
1489+
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
1490+
dependencies = [
1491+
"bitflags",
1492+
"ignore",
1493+
"walkdir",
1494+
]
1495+
14851496
[[package]]
14861497
name = "gsgdt"
14871498
version = "0.1.2"
@@ -4518,6 +4529,7 @@ dependencies = [
45184529
"serde_json",
45194530
"smallvec",
45204531
"tempfile",
4532+
"tera",
45214533
"tracing",
45224534
"tracing-subscriber",
45234535
"tracing-tree",
@@ -5099,6 +5111,21 @@ dependencies = [
50995111
"utf-8",
51005112
]
51015113

5114+
[[package]]
5115+
name = "tera"
5116+
version = "1.10.0"
5117+
source = "registry+https://github.com/rust-lang/crates.io-index"
5118+
checksum = "81060acb882480c8793782eb96bc86f5c83d2fc7175ad46c375c6956ef7afa62"
5119+
dependencies = [
5120+
"globwalk",
5121+
"lazy_static",
5122+
"pest",
5123+
"pest_derive",
5124+
"regex",
5125+
"serde",
5126+
"serde_json",
5127+
]
5128+
51025129
[[package]]
51035130
name = "term"
51045131
version = "0.0.0"

src/librustdoc/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ regex = "1"
2121
rustdoc-json-types = { path = "../rustdoc-json-types" }
2222
tracing = "0.1"
2323
tracing-tree = "0.1.9"
24+
tera = { version = "1.10.0", default-features = false }
2425

2526
[dependencies.tracing-subscriber]
2627
version = "0.2.13"

src/librustdoc/externalfiles.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use std::fs;
44
use std::path::Path;
55
use std::str;
66

7-
#[derive(Clone, Debug)]
7+
use serde::Serialize;
8+
9+
#[derive(Clone, Debug, Serialize)]
810
crate struct ExternalHtml {
911
/// Content that will be included inline in the <head> section of a
1012
/// rendered Markdown file or generated documentation

src/librustdoc/html/layout.rs

+40-174
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use crate::html::escape::Escape;
77
use crate::html::format::{Buffer, Print};
88
use crate::html::render::{ensure_trailing_slash, StylePath};
99

10-
#[derive(Clone)]
10+
use serde::Serialize;
11+
12+
#[derive(Clone, Serialize)]
1113
crate struct Layout {
1214
crate logo: String,
1315
crate favicon: String,
@@ -22,6 +24,7 @@ crate struct Layout {
2224
crate generate_search_filter: bool,
2325
}
2426

27+
#[derive(Serialize)]
2528
crate struct Page<'a> {
2629
crate title: &'a str,
2730
crate css_class: &'a str,
@@ -40,192 +43,55 @@ impl<'a> Page<'a> {
4043
}
4144
}
4245

46+
#[derive(Serialize)]
47+
struct PageLayout<'a> {
48+
static_root_path: &'a str,
49+
page: &'a Page<'a>,
50+
layout: &'a Layout,
51+
style_files: String,
52+
sidebar: String,
53+
content: String,
54+
krate_with_trailing_slash: String,
55+
}
56+
4357
crate fn render<T: Print, S: Print>(
58+
templates: &tera::Tera,
4459
layout: &Layout,
4560
page: &Page<'_>,
4661
sidebar: S,
4762
t: T,
4863
style_files: &[StylePath],
4964
) -> String {
5065
let static_root_path = page.get_static_root_path();
51-
format!(
52-
"<!DOCTYPE html>\
53-
<html lang=\"en\">\
54-
<head>\
55-
<meta charset=\"utf-8\">\
56-
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\
57-
<meta name=\"generator\" content=\"rustdoc\">\
58-
<meta name=\"description\" content=\"{description}\">\
59-
<meta name=\"keywords\" content=\"{keywords}\">\
60-
<title>{title}</title>\
61-
<link rel=\"stylesheet\" type=\"text/css\" href=\"{static_root_path}normalize{suffix}.css\">\
62-
<link rel=\"stylesheet\" type=\"text/css\" href=\"{static_root_path}rustdoc{suffix}.css\" \
63-
id=\"mainThemeStyle\">\
64-
{style_files}\
65-
<script id=\"default-settings\"{default_settings}></script>\
66-
<script src=\"{static_root_path}storage{suffix}.js\"></script>\
67-
<script src=\"{root_path}crates{suffix}.js\"></script>\
68-
<noscript><link rel=\"stylesheet\" href=\"{static_root_path}noscript{suffix}.css\"></noscript>\
69-
{css_extension}\
70-
{favicon}\
71-
{in_header}\
72-
<style type=\"text/css\">\
73-
#crate-search{{background-image:url(\"{static_root_path}down-arrow{suffix}.svg\");}}\
74-
</style>\
75-
</head>\
76-
<body class=\"rustdoc {css_class}\">\
77-
<!--[if lte IE 11]>\
78-
<div class=\"warning\">\
79-
This old browser is unsupported and will most likely display funky \
80-
things.\
81-
</div>\
82-
<![endif]-->\
83-
{before_content}\
84-
<nav class=\"sidebar\">\
85-
<div class=\"sidebar-menu\" role=\"button\">&#9776;</div>\
86-
{logo}\
87-
{sidebar}\
88-
</nav>\
89-
<div class=\"theme-picker\">\
90-
<button id=\"theme-picker\" aria-label=\"Pick another theme!\" aria-haspopup=\"menu\" title=\"themes\">\
91-
<img src=\"{static_root_path}brush{suffix}.svg\" \
92-
width=\"18\" height=\"18\" \
93-
alt=\"Pick another theme!\">\
94-
</button>\
95-
<div id=\"theme-choices\" role=\"menu\"></div>\
96-
</div>\
97-
<nav class=\"sub\">\
98-
<form class=\"search-form\">\
99-
<div class=\"search-container\">\
100-
<div>{filter_crates}\
101-
<input class=\"search-input\" name=\"search\" \
102-
disabled \
103-
autocomplete=\"off\" \
104-
spellcheck=\"false\" \
105-
placeholder=\"Click or press ‘S’ to search, ‘?’ for more options…\" \
106-
type=\"search\">\
107-
</div>\
108-
<button type=\"button\" id=\"help-button\" title=\"help\">?</button>\
109-
<a id=\"settings-menu\" href=\"{root_path}settings.html\" title=\"settings\">\
110-
<img src=\"{static_root_path}wheel{suffix}.svg\" \
111-
width=\"18\" height=\"18\" \
112-
alt=\"Change settings\">\
113-
</a>\
114-
</div>\
115-
</form>\
116-
</nav>\
117-
<section id=\"main\" class=\"content\">{content}</section>\
118-
<section id=\"search\" class=\"content hidden\"></section>\
119-
{after_content}\
120-
<div id=\"rustdoc-vars\" data-root-path=\"{root_path}\" data-current-crate=\"{krate}\" \
121-
data-search-index-js=\"{root_path}search-index{suffix}.js\" \
122-
data-search-js=\"{static_root_path}search{suffix}.js\"></div>\
123-
<script src=\"{static_root_path}main{suffix}.js\"></script>\
124-
{extra_scripts}\
125-
</body>\
126-
</html>",
127-
css_extension = if layout.css_file_extension.is_some() {
66+
let krate_with_trailing_slash = ensure_trailing_slash(&layout.krate).to_string();
67+
let style_files = style_files
68+
.iter()
69+
.filter_map(|t| {
70+
if let Some(stem) = t.path.file_stem() { Some((stem, t.disabled)) } else { None }
71+
})
72+
.filter_map(|t| if let Some(path) = t.0.to_str() { Some((path, t.1)) } else { None })
73+
.map(|t| {
12874
format!(
129-
"<link rel=\"stylesheet\" \
130-
type=\"text/css\" \
131-
href=\"{static_root_path}theme{suffix}.css\">",
132-
static_root_path = static_root_path,
133-
suffix = page.resource_suffix
134-
)
135-
} else {
136-
String::new()
137-
},
138-
content = Buffer::html().to_display(t),
139-
static_root_path = static_root_path,
140-
root_path = page.root_path,
141-
css_class = page.css_class,
142-
logo = {
143-
if layout.logo.is_empty() {
144-
format!(
145-
"<a href='{root}{path}index.html'>\
146-
<div class='logo-container rust-logo'>\
147-
<img src='{static_root_path}rust-logo{suffix}.png' alt='logo'></div></a>",
148-
root = page.root_path,
149-
path = ensure_trailing_slash(&layout.krate),
150-
static_root_path = static_root_path,
151-
suffix = page.resource_suffix
152-
)
153-
} else {
154-
format!(
155-
"<a href='{root}{path}index.html'>\
156-
<div class='logo-container'><img src='{logo}' alt='logo'></div></a>",
157-
root = page.root_path,
158-
path = ensure_trailing_slash(&layout.krate),
159-
logo = layout.logo
160-
)
161-
}
162-
},
163-
title = page.title,
164-
description = Escape(page.description),
165-
keywords = page.keywords,
166-
favicon = if layout.favicon.is_empty() {
167-
format!(
168-
r##"<link rel="icon" type="image/svg+xml" href="{static_root_path}favicon{suffix}.svg">
169-
<link rel="alternate icon" type="image/png" href="{static_root_path}favicon-16x16{suffix}.png">
170-
<link rel="alternate icon" type="image/png" href="{static_root_path}favicon-32x32{suffix}.png">"##,
171-
static_root_path = static_root_path,
172-
suffix = page.resource_suffix
173-
)
174-
} else {
175-
format!(r#"<link rel="shortcut icon" href="{}">"#, layout.favicon)
176-
},
177-
in_header = layout.external_html.in_header,
178-
before_content = layout.external_html.before_content,
179-
after_content = layout.external_html.after_content,
180-
sidebar = Buffer::html().to_display(sidebar),
181-
krate = layout.krate,
182-
default_settings = layout
183-
.default_settings
184-
.iter()
185-
.map(|(k, v)| format!(r#" data-{}="{}""#, k.replace('-', "_"), Escape(v)))
186-
.collect::<String>(),
187-
style_files = style_files
188-
.iter()
189-
.filter_map(|t| {
190-
if let Some(stem) = t.path.file_stem() { Some((stem, t.disabled)) } else { None }
191-
})
192-
.filter_map(|t| {
193-
if let Some(path) = t.0.to_str() { Some((path, t.1)) } else { None }
194-
})
195-
.map(|t| format!(
19675
r#"<link rel="stylesheet" type="text/css" href="{}.css" {} {}>"#,
19776
Escape(&format!("{}{}{}", static_root_path, t.0, page.resource_suffix)),
19877
if t.1 { "disabled" } else { "" },
19978
if t.0 == "light" { "id=\"themeStyle\"" } else { "" }
200-
))
201-
.collect::<String>(),
202-
suffix = page.resource_suffix,
203-
extra_scripts = page
204-
.static_extra_scripts
205-
.iter()
206-
.map(|e| {
207-
format!(
208-
"<script src=\"{static_root_path}{extra_script}.js\"></script>",
209-
static_root_path = static_root_path,
210-
extra_script = e
211-
)
212-
})
213-
.chain(page.extra_scripts.iter().map(|e| {
214-
format!(
215-
"<script src=\"{root_path}{extra_script}.js\"></script>",
216-
root_path = page.root_path,
217-
extra_script = e
218-
)
219-
}))
220-
.collect::<String>(),
221-
filter_crates = if layout.generate_search_filter {
222-
"<select id=\"crate-search\">\
223-
<option value=\"All crates\">All crates</option>\
224-
</select>"
225-
} else {
226-
""
227-
},
228-
)
79+
)
80+
})
81+
.collect::<String>();
82+
let content = Buffer::html().to_display(t); // Note: This must happen before making the sidebar.
83+
let sidebar = Buffer::html().to_display(sidebar);
84+
let teractx = tera::Context::from_serialize(PageLayout {
85+
static_root_path,
86+
page,
87+
layout,
88+
style_files,
89+
sidebar,
90+
content,
91+
krate_with_trailing_slash,
92+
})
93+
.unwrap();
94+
templates.render("page.html", &teractx).unwrap()
22995
}
23096

23197
crate fn redirect(url: &str) -> String {

src/librustdoc/html/render/context.rs

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::cell::RefCell;
22
use std::collections::BTreeMap;
3+
use std::error::Error as StdError;
34
use std::io;
45
use std::path::{Path, PathBuf};
56
use std::rc::Rc;
@@ -29,6 +30,7 @@ use crate::formats::FormatRenderer;
2930
use crate::html::escape::Escape;
3031
use crate::html::format::Buffer;
3132
use crate::html::markdown::{self, plain_text_summary, ErrorCodes, IdMap};
33+
use crate::html::static_files::PAGE;
3234
use crate::html::{layout, sources};
3335

3436
/// Major driving force in all rustdoc rendering. This contains information
@@ -121,6 +123,8 @@ crate struct SharedContext<'tcx> {
121123
/// to `Some(...)`, it'll store redirections and then generate a JSON file at the top level of
122124
/// the crate.
123125
redirections: Option<RefCell<FxHashMap<String, String>>>,
126+
127+
pub(crate) templates: tera::Tera,
124128
}
125129

126130
impl SharedContext<'_> {
@@ -218,6 +222,7 @@ impl<'tcx> Context<'tcx> {
218222

219223
if !self.render_redirect_pages {
220224
layout::render(
225+
&self.shared.templates,
221226
&self.shared.layout,
222227
&page,
223228
|buf: &mut _| print_sidebar(self, it, buf),
@@ -408,6 +413,12 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
408413
let mut issue_tracker_base_url = None;
409414
let mut include_sources = true;
410415

416+
let mut templates = tera::Tera::default();
417+
templates.add_raw_template("page.html", PAGE).map_err(|e| Error {
418+
file: "page.html".into(),
419+
error: format!("{}: {}", e, e.source().map(|e| e.to_string()).unwrap_or_default()),
420+
})?;
421+
411422
// Crawl the crate attributes looking for attributes which control how we're
412423
// going to emit HTML
413424
for attr in krate.module.attrs.lists(sym::doc) {
@@ -454,6 +465,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
454465
errors: receiver,
455466
redirections: if generate_redirect_map { Some(Default::default()) } else { None },
456467
show_type_layout,
468+
templates,
457469
};
458470

459471
// Add the default themes to the `Vec` of stylepaths
@@ -540,6 +552,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
540552
};
541553
let all = self.shared.all.replace(AllTypes::new());
542554
let v = layout::render(
555+
&self.shared.templates,
543556
&self.shared.layout,
544557
&page,
545558
sidebar,
@@ -557,6 +570,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
557570
let sidebar = "<p class=\"location\">Settings</p><div class=\"sidebar-elems\"></div>";
558571
style_files.push(StylePath { path: PathBuf::from("settings.css"), disabled: false });
559572
let v = layout::render(
573+
&self.shared.templates,
560574
&self.shared.layout,
561575
&page,
562576
sidebar,

src/librustdoc/html/render/write_shared.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,14 @@ pub(super) fn write_shared(
460460
})
461461
.collect::<String>()
462462
);
463-
let v = layout::render(&cx.shared.layout, &page, "", content, &cx.shared.style_files);
463+
let v = layout::render(
464+
&cx.shared.templates,
465+
&cx.shared.layout,
466+
&page,
467+
"",
468+
content,
469+
&cx.shared.style_files,
470+
);
464471
cx.shared.fs.write(&dst, v.as_bytes())?;
465472
}
466473
}

0 commit comments

Comments
 (0)