diff options
| author | bors <bors@rust-lang.org> | 2021-06-21 09:40:34 +0000 |
|---|---|---|
| committer | bors <bors@rust-lang.org> | 2021-06-21 09:40:34 +0000 |
| commit | 9d93819fa7ae40376ac1f563a858d2ba5f3b0277 (patch) | |
| tree | de7f22d808f9dc836f7dd76f2b436edbba7128e8 /src/librustdoc/html | |
| parent | d789de67dcbd22c017a3a4cccac22510f61bf50c (diff) | |
| parent | cd0f93193c84ddc6698f9b65909da71c084dcb74 (diff) | |
| download | rust-9d93819fa7ae40376ac1f563a858d2ba5f3b0277.tar.gz rust-9d93819fa7ae40376ac1f563a858d2ba5f3b0277.zip | |
Auto merge of #86157 - jsha:tera, r=jyn514,GuillaumeGomez
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. Part of #84419. Demo at https://hoffman-andrews.com/rust/tera/std/string/struct.String.html.
Diffstat (limited to 'src/librustdoc/html')
| -rw-r--r-- | src/librustdoc/html/layout.rs | 214 | ||||
| -rw-r--r-- | src/librustdoc/html/render/context.rs | 14 | ||||
| -rw-r--r-- | src/librustdoc/html/render/write_shared.rs | 9 | ||||
| -rw-r--r-- | src/librustdoc/html/sources.rs | 1 | ||||
| -rw-r--r-- | src/librustdoc/html/static_files.rs | 2 | ||||
| -rw-r--r-- | src/librustdoc/html/templates/STYLE.md | 37 | ||||
| -rw-r--r-- | src/librustdoc/html/templates/page.html | 119 |
7 files changed, 221 insertions, 175 deletions
diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs index d2d1757b900..6ed603c96bb 100644 --- a/src/librustdoc/html/layout.rs +++ b/src/librustdoc/html/layout.rs @@ -7,7 +7,9 @@ use crate::html::escape::Escape; use crate::html::format::{Buffer, Print}; use crate::html::render::{ensure_trailing_slash, StylePath}; -#[derive(Clone)] +use serde::Serialize; + +#[derive(Clone, Serialize)] crate struct Layout { crate logo: String, crate favicon: String, @@ -22,6 +24,7 @@ crate struct Layout { crate generate_search_filter: bool, } +#[derive(Serialize)] crate struct Page<'a> { crate title: &'a str, crate css_class: &'a str, @@ -40,7 +43,19 @@ impl<'a> Page<'a> { } } +#[derive(Serialize)] +struct PageLayout<'a> { + static_root_path: &'a str, + page: &'a Page<'a>, + layout: &'a Layout, + style_files: String, + sidebar: String, + content: String, + krate_with_trailing_slash: String, +} + crate fn render<T: Print, S: Print>( + templates: &tera::Tera, layout: &Layout, page: &Page<'_>, sidebar: S, @@ -48,184 +63,35 @@ crate fn render<T: Print, S: Print>( style_files: &[StylePath], ) -> String { let static_root_path = page.get_static_root_path(); - format!( - "<!DOCTYPE html>\ -<html lang=\"en\">\ -<head>\ - <meta charset=\"utf-8\">\ - <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\ - <meta name=\"generator\" content=\"rustdoc\">\ - <meta name=\"description\" content=\"{description}\">\ - <meta name=\"keywords\" content=\"{keywords}\">\ - <title>{title}</title>\ - <link rel=\"stylesheet\" type=\"text/css\" href=\"{static_root_path}normalize{suffix}.css\">\ - <link rel=\"stylesheet\" type=\"text/css\" href=\"{static_root_path}rustdoc{suffix}.css\" \ - id=\"mainThemeStyle\">\ - {style_files}\ - <script id=\"default-settings\"{default_settings}></script>\ - <script src=\"{static_root_path}storage{suffix}.js\"></script>\ - <script src=\"{root_path}crates{suffix}.js\"></script>\ - <noscript><link rel=\"stylesheet\" href=\"{static_root_path}noscript{suffix}.css\"></noscript>\ - {css_extension}\ - {favicon}\ - {in_header}\ - <style type=\"text/css\">\ - #crate-search{{background-image:url(\"{static_root_path}down-arrow{suffix}.svg\");}}\ - </style>\ -</head>\ -<body class=\"rustdoc {css_class}\">\ - <!--[if lte IE 11]>\ - <div class=\"warning\">\ - This old browser is unsupported and will most likely display funky \ - things.\ - </div>\ - <![endif]-->\ - {before_content}\ - <nav class=\"sidebar\">\ - <div class=\"sidebar-menu\" role=\"button\">☰</div>\ - {logo}\ - {sidebar}\ - </nav>\ - <div class=\"theme-picker\">\ - <button id=\"theme-picker\" aria-label=\"Pick another theme!\" aria-haspopup=\"menu\" title=\"themes\">\ - <img src=\"{static_root_path}brush{suffix}.svg\" \ - width=\"18\" height=\"18\" \ - alt=\"Pick another theme!\">\ - </button>\ - <div id=\"theme-choices\" role=\"menu\"></div>\ - </div>\ - <nav class=\"sub\">\ - <form class=\"search-form\">\ - <div class=\"search-container\">\ - <div>{filter_crates}\ - <input class=\"search-input\" name=\"search\" \ - disabled \ - autocomplete=\"off\" \ - spellcheck=\"false\" \ - placeholder=\"Click or press ‘S’ to search, ‘?’ for more options…\" \ - type=\"search\">\ - </div>\ - <button type=\"button\" id=\"help-button\" title=\"help\">?</button>\ - <a id=\"settings-menu\" href=\"{root_path}settings.html\" title=\"settings\">\ - <img src=\"{static_root_path}wheel{suffix}.svg\" \ - width=\"18\" height=\"18\" \ - alt=\"Change settings\">\ - </a>\ - </div>\ - </form>\ - </nav>\ - <section id=\"main\" class=\"content\">{content}</section>\ - <section id=\"search\" class=\"content hidden\"></section>\ - {after_content}\ - <div id=\"rustdoc-vars\" data-root-path=\"{root_path}\" data-current-crate=\"{krate}\" \ - data-search-index-js=\"{root_path}search-index{suffix}.js\" \ - data-search-js=\"{static_root_path}search{suffix}.js\"></div>\ - <script src=\"{static_root_path}main{suffix}.js\"></script>\ - {extra_scripts}\ -</body>\ -</html>", - css_extension = if layout.css_file_extension.is_some() { + let krate_with_trailing_slash = ensure_trailing_slash(&layout.krate).to_string(); + let 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!( - "<link rel=\"stylesheet\" \ - type=\"text/css\" \ - href=\"{static_root_path}theme{suffix}.css\">", - static_root_path = static_root_path, - suffix = page.resource_suffix - ) - } else { - String::new() - }, - content = Buffer::html().to_display(t), - static_root_path = static_root_path, - root_path = page.root_path, - css_class = page.css_class, - logo = { - if layout.logo.is_empty() { - format!( - "<a href='{root}{path}index.html'>\ - <div class='logo-container rust-logo'>\ - <img src='{static_root_path}rust-logo{suffix}.png' alt='logo'></div></a>", - root = page.root_path, - path = ensure_trailing_slash(&layout.krate), - static_root_path = static_root_path, - suffix = page.resource_suffix - ) - } else { - format!( - "<a href='{root}{path}index.html'>\ - <div class='logo-container'><img src='{logo}' alt='logo'></div></a>", - 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##"<link rel="icon" type="image/svg+xml" href="{static_root_path}favicon{suffix}.svg"> -<link rel="alternate icon" type="image/png" href="{static_root_path}favicon-16x16{suffix}.png"> -<link rel="alternate icon" type="image/png" href="{static_root_path}favicon-32x32{suffix}.png">"##, - static_root_path = static_root_path, - suffix = page.resource_suffix - ) - } else { - format!(r#"<link rel="shortcut icon" href="{}">"#, 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::<String>(), - 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#"<link rel="stylesheet" type="text/css" href="{}.css" {} {}>"#, Escape(&format!("{}{}{}", static_root_path, t.0, page.resource_suffix)), if t.1 { "disabled" } else { "" }, if t.0 == "light" { "id=\"themeStyle\"" } else { "" } - )) - .collect::<String>(), - suffix = page.resource_suffix, - extra_scripts = page - .static_extra_scripts - .iter() - .map(|e| { - format!( - "<script src=\"{static_root_path}{extra_script}.js\"></script>", - static_root_path = static_root_path, - extra_script = e - ) - }) - .chain(page.extra_scripts.iter().map(|e| { - format!( - "<script src=\"{root_path}{extra_script}.js\"></script>", - root_path = page.root_path, - extra_script = e - ) - })) - .collect::<String>(), - filter_crates = if layout.generate_search_filter { - "<select id=\"crate-search\">\ - <option value=\"All crates\">All crates</option>\ - </select>" - } else { - "" - }, - ) + ) + }) + .collect::<String>(); + 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 1898f5feed2..2085739fc46 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<RefCell<FxHashMap<String, String>>>, + + 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 = "<p class=\"location\">Settings</p><div class=\"sidebar-elems\"></div>"; 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 a4188e6b203..840566731d7 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::<String>() ); - 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 5e2a94fe684..80dd7a7a952 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 ca7e5ef8150..00e13d4ec1a 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 00000000000..fff65e3b5ff --- /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 00000000000..9b1bef5e447 --- /dev/null +++ b/src/librustdoc/html/templates/page.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> {#- -#} +<html lang="en"> {#- -#} +<head> {#- -#} + <meta charset="utf-8"> {#- -#} + <meta name="viewport" content="width=device-width, initial-scale=1.0"> {#- -#} + <meta name="generator" content="rustdoc"> {#- -#} + <meta name="description" content="{{page.description}}"> {#- -#} + <meta name="keywords" content="{{page.keywords}}"> {#- -#} + <title>{{page.title}}</title> {#- -#} + <link rel="stylesheet" type="text/css" {# -#} + href="{{static_root_path | safe}}normalize{{page.resource_suffix}}.css"> {#- -#} + <link rel="stylesheet" type="text/css" {# -#} + href="{{static_root_path | safe}}rustdoc{{page.resource_suffix}}.css" {# -#} + id="mainThemeStyle"> {#- -#} + {{- style_files | safe -}} + <script id="default-settings" {# -#} + {% for k, v in layout.default_settings %} + data-{{k}}="{{v}}" + {%- endfor -%} + ></script> {#- -#} + <script src="{{static_root_path | safe}}storage{{page.resource_suffix}}.js"></script> {#- -#} + <script src="{{page.root_path | safe}}crates{{page.resource_suffix}}.js"></script> {#- -#} + <noscript> {#- -#} + <link rel="stylesheet" {# -#} + href="{{static_root_path | safe}}noscript{{page.resource_suffix}}.css"> {#- -#} + </noscript> {#- -#} + {%- if layout.css_file_extension -%} + <link rel="stylesheet" type="text/css" {# -#} + href="{{static_root_path | safe}}theme{{page.resource_suffix}}.css"> {#- -#} + {%- endif -%} + {%- if layout.favicon -%} + <link rel="shortcut icon" href="{{layout.favicon}}"> {#- -#} + {%- else -%} + <link rel="icon" type="image/svg+xml" {# -#} + href="{{static_root_path | safe}}favicon{{page.resource_suffix}}.svg"> {#- -#} + <link rel="alternate icon" type="image/png" {# -#} + href="{{static_root_path | safe}}favicon-16x16{{page.resource_suffix}}.png"> {#- -#} + <link rel="alternate icon" type="image/png" {# -#} + href="{{static_root_path | safe}}favicon-32x32{{page.resource_suffix}}.png"> {#- -#} + {%- endif -%} + {{- layout.external_html.in_header | safe -}} + <style type="text/css"> {#- -#} + #crate-search{ {#- -#} + background-image:url("{{static_root_path | safe}}down-arrow{{page.resource_suffix}}.svg"); {#- -#} + } {#- -#} + </style> {#- -#} +</head> {#- -#} +<body class="rustdoc {{page.css_class}}"> {#- -#} + <!--[if lte IE 11]> {#- -#} + <div class="warning"> {#- -#} + This old browser is unsupported and will most likely display funky things. {#- -#} + </div> {#- -#} + <![endif]--> {#- -#} + {{- layout.external_html.before_content | safe -}} + <nav class="sidebar"> {#- -#} + <div class="sidebar-menu" role="button">☰</div> {#- -#} + <a href='{{page.root_path | safe}}{{krate_with_trailing_slash | safe}}index.html'> {#- -#} + <div class='logo-container rust-logo'> {#- -#} + <img src=' + {%- if layout.logo -%} + {{layout.logo}} + {%- else -%} + {{static_root_path | safe}}rust-logo{{page.resource_suffix}}.png + {%- endif -%} + ' alt='logo'> {#- -#} + </div> {#- -#} + </a> {#- -#} + {{- sidebar | safe -}} + </nav> {#- -#} + <div class="theme-picker"> {#- -#} + <button id="theme-picker" aria-label="Pick another theme!" aria-haspopup="menu" title="themes"> {#- -#} + <img width="18" height="18" alt="Pick another theme!" {# -#} + src="{{static_root_path | safe}}brush{{page.resource_suffix}}.svg"> {#- -#} + </button> {#- -#} + <div id="theme-choices" role="menu"></div> {#- -#} + </div> {#- -#} + <nav class="sub"> {#- -#} + <form class="search-form"> {#- -#} + <div class="search-container"> {#- -#} + <div>{%- if layout.generate_search_filter -%} + <select id="crate-search"> {#- -#} + <option value="All crates">All crates</option> {#- -#} + </select> {#- -#} + {%- endif -%} + <input {# -#} + class="search-input"{# -#} + name="search" {# -#} + disabled {# -#} + autocomplete="off" {# -#} + spellcheck="false" {# -#} + placeholder="Click or press ‘S’ to search, ‘?’ for more options…" {# -#} + type="search"> {#- -#} + </div> {#- -#} + <button type="button" id="help-button" title="help">?</button> {#- -#} + <a id="settings-menu" href="{{page.root_path | safe}}settings.html" title="settings"> {#- -#} + <img width="18" height="18" alt="Change settings" {# -#} + src="{{static_root_path | safe}}wheel{{page.resource_suffix}}.svg"> {#- -#} + </a> {#- -#} + </div> {#- -#} + </form> {#- -#} + </nav> {#- -#} + <section id="main" class="content">{{- content | safe -}}</section> {#- -#} + <section id="search" class="content hidden"></section> {#- -#} + {{- layout.external_html.after_content | safe -}} + <div id="rustdoc-vars" {# -#} + data-root-path="{{page.root_path | safe}}" {# -#} + data-current-crate="{{layout.krate}}" {# -#} + data-search-index-js="{{page.root_path | safe}}search-index{{page.resource_suffix}}.js" {# -#} + data-search-js="{{static_root_path | safe}}search{{page.resource_suffix}}.js"> {#- -#} + </div> + <script src="{{static_root_path | safe}}main{{page.resource_suffix}}.js"></script> {#- -#} + {%- for script in page.static_extra_scripts -%} + <script src="{{static_root_path | safe}}{{script}}.js"></script> {#- -#} + {% endfor %} + {%- for script in page.extra_scripts -%} + <script src="{{page.root_path | safe}}{{script}}.js"></script> {#- -#} + {% endfor %} +</body> {#- -#} +</html> {#- -#} |
