diff --git a/compiler/rustc_ast_passes/src/ast_validation.rs b/compiler/rustc_ast_passes/src/ast_validation.rs index 1822ba6ec9964..efc30121987e2 100644 --- a/compiler/rustc_ast_passes/src/ast_validation.rs +++ b/compiler/rustc_ast_passes/src/ast_validation.rs @@ -23,7 +23,7 @@ use rustc_span::symbol::{kw, sym, Ident}; use rustc_span::Span; use rustc_target::spec::abi; use std::mem; -use std::ops::DerefMut; +use std::ops::{Deref, DerefMut}; const MORE_EXTERN: &str = "for more information, visit https://doc.rust-lang.org/std/keyword.extern.html"; @@ -1714,6 +1714,53 @@ fn deny_equality_constraints( } } } + // Given `A: Foo, A::Bar = RhsTy`, suggest `A: Foo`. + if let TyKind::Path(None, full_path) = &predicate.lhs_ty.kind { + if let [potential_param, potential_assoc] = &full_path.segments[..] { + for param in &generics.params { + if param.ident == potential_param.ident { + for bound in ¶m.bounds { + if let ast::GenericBound::Trait(trait_ref, TraitBoundModifier::None) = bound + { + if let [trait_segment] = &trait_ref.trait_ref.path.segments[..] { + let assoc = pprust::path_to_string(&ast::Path::from_ident( + potential_assoc.ident, + )); + let ty = pprust::ty_to_string(&predicate.rhs_ty); + let (args, span) = match &trait_segment.args { + Some(args) => match args.deref() { + ast::GenericArgs::AngleBracketed(args) => { + let Some(arg) = args.args.last() else { + continue; + }; + ( + format!(", {} = {}", assoc, ty), + arg.span().shrink_to_hi(), + ) + } + _ => continue, + }, + None => ( + format!("<{} = {}>", assoc, ty), + trait_segment.span().shrink_to_hi(), + ), + }; + err.multipart_suggestion( + &format!( + "if `{}::{}` is an associated type you're trying to set, \ + use the associated type binding syntax", + trait_segment.ident, potential_assoc.ident, + ), + vec![(span, args), (predicate.span, String::new())], + Applicability::MaybeIncorrect, + ); + } + } + } + } + } + } + } err.note( "see issue #20041 for more information", ); diff --git a/compiler/rustc_ast_passes/src/lib.rs b/compiler/rustc_ast_passes/src/lib.rs index 47666670b2b63..adc4d117b805f 100644 --- a/compiler/rustc_ast_passes/src/lib.rs +++ b/compiler/rustc_ast_passes/src/lib.rs @@ -6,6 +6,7 @@ #![feature(iter_is_partitioned)] #![feature(box_patterns)] +#![feature(let_else)] #![recursion_limit = "256"] pub mod ast_validation; diff --git a/library/core/src/fmt/mod.rs b/library/core/src/fmt/mod.rs index 80d3270d73cc8..6fc3cd0b7c4ad 100644 --- a/library/core/src/fmt/mod.rs +++ b/library/core/src/fmt/mod.rs @@ -2186,28 +2186,34 @@ impl Display for char { #[stable(feature = "rust1", since = "1.0.0")] impl Pointer for *const T { fn fmt(&self, f: &mut Formatter<'_>) -> Result { - let old_width = f.width; - let old_flags = f.flags; - - // The alternate flag is already treated by LowerHex as being special- - // it denotes whether to prefix with 0x. We use it to work out whether - // or not to zero extend, and then unconditionally set it to get the - // prefix. - if f.alternate() { - f.flags |= 1 << (FlagV1::SignAwareZeroPad as u32); - - if f.width.is_none() { - f.width = Some((usize::BITS / 4) as usize + 2); + /// Since the formatting will be identical for all pointer types, use a non-monomorphized + /// implementation for the actual formatting to reduce the amount of codegen work needed + fn inner(ptr: *const (), f: &mut Formatter<'_>) -> Result { + let old_width = f.width; + let old_flags = f.flags; + + // The alternate flag is already treated by LowerHex as being special- + // it denotes whether to prefix with 0x. We use it to work out whether + // or not to zero extend, and then unconditionally set it to get the + // prefix. + if f.alternate() { + f.flags |= 1 << (FlagV1::SignAwareZeroPad as u32); + + if f.width.is_none() { + f.width = Some((usize::BITS / 4) as usize + 2); + } } - } - f.flags |= 1 << (FlagV1::Alternate as u32); + f.flags |= 1 << (FlagV1::Alternate as u32); + + let ret = LowerHex::fmt(&(ptr as usize), f); - let ret = LowerHex::fmt(&(*self as *const () as usize), f); + f.width = old_width; + f.flags = old_flags; - f.width = old_width; - f.flags = old_flags; + ret + } - ret + inner(*self as *const (), f) } } diff --git a/library/core/src/hash/mod.rs b/library/core/src/hash/mod.rs index 540160bc4c2a4..3ff84cc9672eb 100644 --- a/library/core/src/hash/mod.rs +++ b/library/core/src/hash/mod.rs @@ -164,6 +164,19 @@ mod sip; /// `0xFF` byte to the `Hasher` so that the values `("ab", "c")` and `("a", /// "bc")` hash differently. /// +/// ## Portability +/// +/// Due to differences in endianness and type sizes, data fed by `Hash` to a `Hasher` +/// should not be considered portable across platforms. Additionally the data passed by most +/// standard library types should not be considered stable between compiler versions. +/// +/// This means tests shouldn't probe hard-coded hash values or data fed to a `Hasher` and +/// instead should check consistency with `Eq`. +/// +/// Serialization formats intended to be portable between platforms or compiler versions should +/// either avoid encoding hashes or only rely on `Hash` and `Hasher` implementations that +/// provide additional guarantees. +/// /// [`HashMap`]: ../../std/collections/struct.HashMap.html /// [`HashSet`]: ../../std/collections/struct.HashSet.html /// [`hash`]: Hash::hash diff --git a/library/core/src/iter/traits/iterator.rs b/library/core/src/iter/traits/iterator.rs index 588758fed9cab..f3ef6b3d0185e 100644 --- a/library/core/src/iter/traits/iterator.rs +++ b/library/core/src/iter/traits/iterator.rs @@ -458,8 +458,10 @@ pub trait Iterator { /// In other words, it zips two iterators together, into a single one. /// /// If either iterator returns [`None`], [`next`] from the zipped iterator - /// will return [`None`]. If the first iterator returns [`None`], `zip` will - /// short-circuit and `next` will not be called on the second iterator. + /// will return [`None`]. + /// If the zipped iterator has no more elements to return then each further attempt to advance + /// it will first try to advance the first iterator at most one time and if it still yielded an item + /// try to advance the second iterator at most one time. /// /// # Examples /// diff --git a/library/std/src/thread/mod.rs b/library/std/src/thread/mod.rs index 39b53b51bfa63..343d3ef8dc538 100644 --- a/library/std/src/thread/mod.rs +++ b/library/std/src/thread/mod.rs @@ -1460,9 +1460,12 @@ fn _assert_sync_and_send() { /// The purpose of this API is to provide an easy and portable way to query /// the default amount of parallelism the program should use. Among other things it /// does not expose information on NUMA regions, does not account for -/// differences in (co)processor capabilities, and will not modify the program's -/// global state in order to more accurately query the amount of available -/// parallelism. +/// differences in (co)processor capabilities or current system load, +/// and will not modify the program's global state in order to more accurately +/// query the amount of available parallelism. +/// +/// Where both fixed steady-state and burst limits are available the steady-state +/// capacity will be used to ensure more predictable latencies. /// /// Resource limits can be changed during the runtime of a program, therefore the value is /// not cached and instead recomputed every time this function is called. It should not be diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 7c23117cce3af..04bcade156a9d 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -552,7 +552,7 @@ impl Options { )) .emit(); } - themes.push(StylePath { path: theme_file, disabled: true }); + themes.push(StylePath { path: theme_file }); } } diff --git a/src/librustdoc/error.rs b/src/librustdoc/error.rs index 82d0002b98b18..8eadbf63f33d9 100644 --- a/src/librustdoc/error.rs +++ b/src/librustdoc/error.rs @@ -39,7 +39,10 @@ macro_rules! try_none { match $e { Some(e) => e, None => { - return Err(Error::new(io::Error::new(io::ErrorKind::Other, "not found"), $file)); + return Err(::new( + io::Error::new(io::ErrorKind::Other, "not found"), + $file, + )); } } }}; diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs index 71d7cc1a09dce..3d3fa3aaeaa51 100644 --- a/src/librustdoc/html/layout.rs +++ b/src/librustdoc/html/layout.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; use rustc_data_structures::fx::FxHashMap; +use crate::error::Error; use crate::externalfiles::ExternalHtml; -use crate::html::escape::Escape; use crate::html::format::{Buffer, Print}; use crate::html::render::{ensure_trailing_slash, StylePath}; @@ -50,10 +50,11 @@ struct PageLayout<'a> { static_root_path: &'a str, page: &'a Page<'a>, layout: &'a Layout, - style_files: String, + themes: Vec, sidebar: String, content: String, krate_with_trailing_slash: String, + crate rustdoc_version: &'a str, } crate fn render( @@ -66,29 +67,24 @@ crate fn render( ) -> String { let static_root_path = page.get_static_root_path(); let krate_with_trailing_slash = ensure_trailing_slash(&layout.krate).to_string(); - let style_files = style_files + let mut themes: Vec = style_files .iter() - .filter_map(|t| t.path.file_stem().map(|stem| (stem, t.disabled))) - .filter_map(|t| t.0.to_str().map(|path| (path, t.1))) - .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::(); + .map(StylePath::basename) + .collect::>() + .unwrap_or_default(); + themes.sort(); + let rustdoc_version = rustc_interface::util::version_str().unwrap_or("unknown version"); 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, + themes, sidebar, content, krate_with_trailing_slash, + rustdoc_version, }) .unwrap(); templates.render("page.html", &teractx).unwrap() diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 069862efde640..365d959ad9f3b 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -504,9 +504,9 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { // by the browser as the theme stylesheet. The theme system (hackily) works by // changing the href to this stylesheet. All other themes are disabled to // prevent rule conflicts - scx.style_files.push(StylePath { path: PathBuf::from("light.css"), disabled: false }); - scx.style_files.push(StylePath { path: PathBuf::from("dark.css"), disabled: true }); - scx.style_files.push(StylePath { path: PathBuf::from("ayu.css"), disabled: true }); + scx.style_files.push(StylePath { path: PathBuf::from("light.css") }); + scx.style_files.push(StylePath { path: PathBuf::from("dark.css") }); + scx.style_files.push(StylePath { path: PathBuf::from("ayu.css") }); let dst = output; scx.ensure_dir(&dst)?; @@ -596,9 +596,13 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { page.description = "Settings of Rustdoc"; page.root_path = "./"; - let mut style_files = self.shared.style_files.clone(); let sidebar = "

Settings

"; - style_files.push(StylePath { path: PathBuf::from("settings.css"), disabled: false }); + let theme_names: Vec = self + .shared + .style_files + .iter() + .map(StylePath::basename) + .collect::>()?; let v = layout::render( &self.shared.templates, &self.shared.layout, @@ -607,9 +611,9 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { settings( self.shared.static_root_path.as_deref().unwrap_or("./"), &self.shared.resource_suffix, - &self.shared.style_files, + theme_names, )?, - &style_files, + &self.shared.style_files, ); self.shared.fs.write(settings_file, v)?; if let Some(ref redirections) = self.shared.redirections { diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 20a200f0484bd..08022d526fefb 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -64,7 +64,6 @@ use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; use crate::clean::{self, ItemId, RenderedLink, SelfTy}; -use crate::docfs::PathError; use crate::error::Error; use crate::formats::cache::Cache; use crate::formats::item_type::ItemType; @@ -173,8 +172,12 @@ impl Serialize for TypeWithKind { crate struct StylePath { /// The path to the theme crate path: PathBuf, - /// What the `disabled` attribute should be set to in the HTML tag - crate disabled: bool, +} + +impl StylePath { + pub fn basename(&self) -> Result { + Ok(try_none!(try_none!(self.path.file_stem(), &self.path).to_str(), &self.path).to_string()) + } } fn write_srclink(cx: &Context<'_>, item: &clean::Item, buf: &mut Buffer) { @@ -353,7 +356,7 @@ enum Setting { js_data_name: &'static str, description: &'static str, default_value: &'static str, - options: Vec<(String, String)>, + options: Vec, }, } @@ -393,10 +396,9 @@ impl Setting { options .iter() .map(|opt| format!( - "", - opt.0, - if opt.0 == default_value { "selected" } else { "" }, - opt.1, + "", + if opt == default_value { "selected" } else { "" }, + name = opt, )) .collect::(), root_path, @@ -421,18 +423,7 @@ impl> From<(&'static str, Vec)> for Setting { } } -fn settings(root_path: &str, suffix: &str, themes: &[StylePath]) -> Result { - let theme_names: Vec<(String, String)> = themes - .iter() - .map(|entry| { - let theme = - try_none!(try_none!(entry.path.file_stem(), &entry.path).to_str(), &entry.path) - .to_string(); - - Ok((theme.clone(), theme)) - }) - .collect::>()?; - +fn settings(root_path: &str, suffix: &str, theme_names: Vec) -> Result { // (id, explanation, default value) let settings: &[Setting] = &[ ( @@ -469,10 +460,11 @@ fn settings(root_path: &str, suffix: &str, themes: &[StylePath]) -> ResultRustdoc settings\ \
{}
\ - ", + \ + ", settings.iter().map(|s| s.display(root_path, suffix)).collect::(), - root_path, - suffix + root_path = root_path, + suffix = suffix )) } diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index 2d3b2490677e9..0d5ba8e80d242 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -181,42 +181,34 @@ pub(super) fn write_shared( cx.write_shared(SharedResource::InvocationSpecific { basename: p }, content, &options.emit) }; - fn add_background_image_to_css( - cx: &Context<'_>, - css: &mut String, - rule: &str, - file: &'static str, - ) { - css.push_str(&format!( - "{} {{ background-image: url({}); }}", - rule, - SharedResource::ToolchainSpecific { basename: file } + // Given "foo.svg", return e.g. "url(\"foo1.58.0.svg\")" + fn ver_url(cx: &Context<'_>, basename: &'static str) -> String { + format!( + "url(\"{}\")", + SharedResource::ToolchainSpecific { basename } .path(cx) .file_name() .unwrap() .to_str() .unwrap() - )) + ) } - // Add all the static files. These may already exist, but we just - // overwrite them anyway to make sure that they're fresh and up-to-date. - let mut rustdoc_css = static_files::RUSTDOC_CSS.to_owned(); - add_background_image_to_css( - cx, - &mut rustdoc_css, - "details.undocumented[open] > summary::before, \ - details.rustdoc-toggle[open] > summary::before, \ - details.rustdoc-toggle[open] > summary.hideme::before", - "toggle-minus.svg", - ); - add_background_image_to_css( + // We use the AUTOREPLACE mechanism to inject into our static JS and CSS certain + // values that are only known at doc build time. Since this mechanism is somewhat + // surprising when reading the code, please limit it to rustdoc.css. + write_minify( + "rustdoc.css", + static_files::RUSTDOC_CSS + .replace( + "/* AUTOREPLACE: */url(\"toggle-minus.svg\")", + &ver_url(cx, "toggle-minus.svg"), + ) + .replace("/* AUTOREPLACE: */url(\"toggle-plus.svg\")", &ver_url(cx, "toggle-plus.svg")) + .replace("/* AUTOREPLACE: */url(\"down-arrow.svg\")", &ver_url(cx, "down-arrow.svg")), cx, - &mut rustdoc_css, - "details.undocumented > summary::before, details.rustdoc-toggle > summary::before", - "toggle-plus.svg", - ); - write_minify("rustdoc.css", rustdoc_css, cx, options)?; + options, + )?; // Add all the static files. These may already exist, but we just // overwrite them anyway to make sure that they're fresh and up-to-date. @@ -228,12 +220,12 @@ pub(super) fn write_shared( let mut themes: FxHashSet = FxHashSet::default(); for entry in &cx.shared.style_files { - let theme = try_none!(try_none!(entry.path.file_stem(), &entry.path).to_str(), &entry.path); + let theme = entry.basename()?; let extension = try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path); // Handle the official themes - match theme { + match theme.as_str() { "light" => write_minify("light.css", static_files::themes::LIGHT, cx, options)?, "dark" => write_minify("dark.css", static_files::themes::DARK, cx, options)?, "ayu" => write_minify("ayu.css", static_files::themes::AYU, cx, options)?, @@ -265,26 +257,7 @@ pub(super) fn write_shared( let mut themes: Vec<&String> = themes.iter().collect(); themes.sort(); - // FIXME: this should probably not be a toolchain file since it depends on `--theme`. - // But it seems a shame to copy it over and over when it's almost always the same. - // Maybe we can change the representation to move this out of main.js? - write_minify( - "main.js", - static_files::MAIN_JS - .replace( - "/* INSERT THEMES HERE */", - &format!(" = {}", serde_json::to_string(&themes).unwrap()), - ) - .replace( - "/* INSERT RUSTDOC_VERSION HERE */", - &format!( - "rustdoc {}", - rustc_interface::util::version_str().unwrap_or("unknown version") - ), - ), - cx, - options, - )?; + write_minify("main.js", static_files::MAIN_JS, cx, options)?; write_minify("search.js", static_files::SEARCH_JS, cx, options)?; write_minify("settings.js", static_files::SETTINGS_JS, cx, options)?; @@ -292,18 +265,7 @@ pub(super) fn write_shared( write_minify("source-script.js", static_files::sidebar::SOURCE_SCRIPT, cx, options)?; } - { - write_minify( - "storage.js", - format!( - "var resourcesSuffix = \"{}\";{}", - cx.shared.resource_suffix, - static_files::STORAGE_JS - ), - cx, - options, - )?; - } + write_minify("storage.js", static_files::STORAGE_JS, cx, options)?; if cx.shared.layout.scrape_examples_extension { cx.write_minify( diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 479b5210f9e09..fceb508bc4ff5 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -828,6 +828,7 @@ h2.small-section-header > .anchor { background-color: transparent; background-size: 20px; background-position: calc(100% - 1px) 56%; + background-image: /* AUTOREPLACE: */url("down-arrow.svg"); } .search-container > .top-button { position: absolute; @@ -1610,6 +1611,16 @@ details.rustdoc-toggle[open] > summary.hideme > span { display: none; } +details.undocumented[open] > summary::before, +details.rustdoc-toggle[open] > summary::before, +details.rustdoc-toggle[open] > summary.hideme::before { + background-image: /* AUTOREPLACE: */url("toggle-minus.svg"); +} + +details.undocumented > summary::before, details.rustdoc-toggle > summary::before { + background-image: /* AUTOREPLACE: */url("toggle-plus.svg"); +} + details.rustdoc-toggle[open] > summary::before, details.rustdoc-toggle[open] > summary.hideme::before { width: 17px; diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index c9fa72cbaab01..5661d4973342f 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -37,14 +37,29 @@ if (!DOMTokenList.prototype.remove) { }; } -(function () { - var rustdocVars = document.getElementById("rustdoc-vars"); - if (rustdocVars) { - window.rootPath = rustdocVars.attributes["data-root-path"].value; - window.currentCrate = rustdocVars.attributes["data-current-crate"].value; - window.searchJS = rustdocVars.attributes["data-search-js"].value; - window.searchIndexJS = rustdocVars.attributes["data-search-index-js"].value; +// Get a value from the rustdoc-vars div, which is used to convey data from +// Rust to the JS. If there is no such element, return null. +function getVar(name) { + var el = document.getElementById("rustdoc-vars"); + if (el) { + return el.attributes["data-" + name].value; + } else { + return null; } +} + +// Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL +// for a resource under the root-path, with the resource-suffix. +function resourcePath(basename, extension) { + return getVar("root-path") + basename + getVar("resource-suffix") + extension; +} + + +(function () { + window.rootPath = getVar("root-path"); + window.currentCrate = getVar("current-crate"); + window.searchJS = resourcePath("search", ".js"); + window.searchIndexJS = resourcePath("search-index", ".js"); var sidebarVars = document.getElementById("sidebar-vars"); if (sidebarVars) { window.sidebarCurrent = { @@ -115,7 +130,7 @@ function hideThemeButtonState() { (function () { var themeChoices = getThemesElement(); var themePicker = getThemePickerElement(); - var availableThemes/* INSERT THEMES HERE */; + var availableThemes = getVar("themes").split(","); function switchThemeButtonState() { if (themeChoices.style.display === "block") { @@ -980,7 +995,7 @@ function hideThemeButtonState() { var rustdoc_version = document.createElement("span"); rustdoc_version.className = "bottom"; var rustdoc_version_code = document.createElement("code"); - rustdoc_version_code.innerText = "/* INSERT RUSTDOC_VERSION HERE */"; + rustdoc_version_code.innerText = "rustdoc " + getVar("rustdoc-version"); rustdoc_version.appendChild(rustdoc_version_code); container.appendChild(rustdoc_version); diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js index 78ed17e6899e9..606c237aea7d0 100644 --- a/src/librustdoc/html/static/js/storage.js +++ b/src/librustdoc/html/static/js/storage.js @@ -1,5 +1,3 @@ -// From rust: -/* global resourcesSuffix */ var darkThemes = ["dark", "ayu"]; window.currentTheme = document.getElementById("themeStyle"); window.mainTheme = document.getElementById("mainThemeStyle"); @@ -107,9 +105,8 @@ function getCurrentValue(name) { } function switchTheme(styleElem, mainStyleElem, newTheme, saveTheme) { - var fullBasicCss = "rustdoc" + resourcesSuffix + ".css"; - var fullNewTheme = newTheme + resourcesSuffix + ".css"; - var newHref = mainStyleElem.href.replace(fullBasicCss, fullNewTheme); + var newHref = mainStyleElem.href.replace( + /\/rustdoc([^/]*)\.css/, "/" + newTheme + "$1" + ".css"); // If this new value comes from a system setting or from the previously // saved theme, no need to save it. diff --git a/src/librustdoc/html/templates/page.html b/src/librustdoc/html/templates/page.html index cf57d4cf3aa43..2a783c6da57e4 100644 --- a/src/librustdoc/html/templates/page.html +++ b/src/librustdoc/html/templates/page.html @@ -12,7 +12,16 @@ {#- -#} - {{- style_files | safe -}} + {%- for theme in themes -%} + + {%- endfor -%}