diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs index a3cbb5756fe..f6788e94431 100644 --- a/src/librustdoc/html/format.rs +++ b/src/librustdoc/html/format.rs @@ -27,6 +27,8 @@ use crate::html::render::cache::ExternalLocation; use crate::html::render::Context; +use super::url_parts_builder::UrlPartsBuilder; + crate trait Print { fn print(self, buffer: &mut Buffer); } @@ -544,9 +546,9 @@ fn to_module_fqp(shortty: ItemType, fqp: &[String]) -> &[String] { ExternalLocation::Remote(ref s) => { is_remote = true; let s = s.trim_end_matches('/'); - let mut s = vec![s]; - s.extend(module_fqp[..].iter().map(String::as_str)); - s + let mut builder = UrlPartsBuilder::singleton(s); + builder.extend(module_fqp.iter().map(String::as_str)); + builder } ExternalLocation::Local => href_relative_parts(module_fqp, relative_to), ExternalLocation::Unknown => return Err(HrefError::DocumentationNotBuilt), @@ -560,22 +562,21 @@ fn to_module_fqp(shortty: ItemType, fqp: &[String]) -> &[String] { if !is_remote { if let Some(root_path) = root_path { let root = root_path.trim_end_matches('/'); - url_parts.insert(0, root); + url_parts.push_front(root); } } debug!(?url_parts); let last = &fqp.last().unwrap()[..]; - let filename; match shortty { ItemType::Module => { url_parts.push("index.html"); } _ => { - filename = format!("{}.{}.html", shortty.as_str(), last); + let filename = format!("{}.{}.html", shortty.as_str(), last); url_parts.push(&filename); } } - Ok((url_parts.join("/"), shortty, fqp.to_vec())) + Ok((url_parts.finish(), shortty, fqp.to_vec())) } crate fn href(did: DefId, cx: &Context<'_>) -> Result<(String, ItemType, Vec), HrefError> { @@ -585,7 +586,7 @@ fn to_module_fqp(shortty: ItemType, fqp: &[String]) -> &[String] { /// Both paths should only be modules. /// This is because modules get their own directories; that is, `std::vec` and `std::vec::Vec` will /// both need `../iter/trait.Iterator.html` to get at the iterator trait. -crate fn href_relative_parts<'a>(fqp: &'a [String], relative_to_fqp: &'a [String]) -> Vec<&'a str> { +crate fn href_relative_parts(fqp: &[String], relative_to_fqp: &[String]) -> UrlPartsBuilder { for (i, (f, r)) in fqp.iter().zip(relative_to_fqp.iter()).enumerate() { // e.g. linking to std::iter from std::vec (`dissimilar_part_count` will be 1) if f != r { @@ -603,7 +604,7 @@ fn to_module_fqp(shortty: ItemType, fqp: &[String]) -> &[String] { iter::repeat("..").take(dissimilar_part_count).collect() // linking to the same module } else { - Vec::new() + UrlPartsBuilder::new() } } diff --git a/src/librustdoc/html/mod.rs b/src/librustdoc/html/mod.rs index 109b0a356db..e1bbc784fd1 100644 --- a/src/librustdoc/html/mod.rs +++ b/src/librustdoc/html/mod.rs @@ -9,6 +9,7 @@ crate mod sources; crate mod static_files; crate mod toc; +mod url_parts_builder; #[cfg(test)] mod tests; diff --git a/src/librustdoc/html/tests.rs b/src/librustdoc/html/tests.rs index 5d537dabd0c..dee9f5e5038 100644 --- a/src/librustdoc/html/tests.rs +++ b/src/librustdoc/html/tests.rs @@ -1,44 +1,44 @@ use crate::html::format::href_relative_parts; -fn assert_relative_path(expected: &[&str], relative_to_fqp: &[&str], fqp: &[&str]) { +fn assert_relative_path(expected: &str, relative_to_fqp: &[&str], fqp: &[&str]) { let relative_to_fqp: Vec = relative_to_fqp.iter().copied().map(String::from).collect(); let fqp: Vec = fqp.iter().copied().map(String::from).collect(); - assert_eq!(expected, href_relative_parts(&fqp, &relative_to_fqp)); + assert_eq!(expected, href_relative_parts(&fqp, &relative_to_fqp).finish()); } #[test] fn href_relative_parts_basic() { let relative_to_fqp = &["std", "vec"]; let fqp = &["std", "iter"]; - assert_relative_path(&["..", "iter"], relative_to_fqp, fqp); + assert_relative_path("../iter", relative_to_fqp, fqp); } #[test] fn href_relative_parts_parent_module() { let relative_to_fqp = &["std", "vec"]; let fqp = &["std"]; - assert_relative_path(&[".."], relative_to_fqp, fqp); + assert_relative_path("..", relative_to_fqp, fqp); } #[test] fn href_relative_parts_different_crate() { let relative_to_fqp = &["std", "vec"]; let fqp = &["core", "iter"]; - assert_relative_path(&["..", "..", "core", "iter"], relative_to_fqp, fqp); + assert_relative_path("../../core/iter", relative_to_fqp, fqp); } #[test] fn href_relative_parts_same_module() { let relative_to_fqp = &["std", "vec"]; let fqp = &["std", "vec"]; - assert_relative_path(&[], relative_to_fqp, fqp); + assert_relative_path("", relative_to_fqp, fqp); } #[test] fn href_relative_parts_child_module() { let relative_to_fqp = &["std"]; let fqp = &["std", "vec"]; - assert_relative_path(&["vec"], relative_to_fqp, fqp); + assert_relative_path("vec", relative_to_fqp, fqp); } #[test] fn href_relative_parts_root() { let relative_to_fqp = &[]; let fqp = &["std"]; - assert_relative_path(&["std"], relative_to_fqp, fqp); + assert_relative_path("std", relative_to_fqp, fqp); } diff --git a/src/librustdoc/html/url_parts_builder.rs b/src/librustdoc/html/url_parts_builder.rs new file mode 100644 index 00000000000..918d5e6bd1b --- /dev/null +++ b/src/librustdoc/html/url_parts_builder.rs @@ -0,0 +1,119 @@ +/// A builder that allows efficiently and easily constructing the part of a URL +/// after the domain: `nightly/core/str/struct.Bytes.html`. +/// +/// This type is a wrapper around the final `String` buffer, +/// but its API is like that of a `Vec` of URL components. +#[derive(Debug)] +crate struct UrlPartsBuilder { + buf: String, +} + +impl UrlPartsBuilder { + /// Create an empty buffer. + crate fn new() -> Self { + Self { buf: String::new() } + } + + /// Create an empty buffer with capacity for the specified number of bytes. + fn with_capacity_bytes(count: usize) -> Self { + Self { buf: String::with_capacity(count) } + } + + /// Create a buffer with one URL component. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```ignore (private-type) + /// let builder = UrlPartsBuilder::singleton("core"); + /// assert_eq!(builder.finish(), "core"); + /// ``` + /// + /// Adding more components afterward. + /// + /// ```ignore (private-type) + /// let mut builder = UrlPartsBuilder::singleton("core"); + /// builder.push("str"); + /// builder.push_front("nightly"); + /// assert_eq!(builder.finish(), "nightly/core/str"); + /// ``` + crate fn singleton(part: &str) -> Self { + Self { buf: part.to_owned() } + } + + /// Push a component onto the buffer. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```ignore (private-type) + /// let mut builder = UrlPartsBuilder::new(); + /// builder.push("core"); + /// builder.push("str"); + /// builder.push("struct.Bytes.html"); + /// assert_eq!(builder.finish(), "core/str/struct.Bytes.html"); + /// ``` + crate fn push(&mut self, part: &str) { + if !self.buf.is_empty() { + self.buf.push('/'); + } + self.buf.push_str(part); + } + + /// Push a component onto the front of the buffer. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```ignore (private-type) + /// let mut builder = UrlPartsBuilder::new(); + /// builder.push("core"); + /// builder.push("str"); + /// builder.push_front("nightly"); + /// builder.push("struct.Bytes.html"); + /// assert_eq!(builder.finish(), "nightly/core/str/struct.Bytes.html"); + /// ``` + crate fn push_front(&mut self, part: &str) { + let is_empty = self.buf.is_empty(); + self.buf.reserve(part.len() + if !is_empty { 1 } else { 0 }); + self.buf.insert_str(0, part); + if !is_empty { + self.buf.insert(part.len(), '/'); + } + } + + /// Get the final `String` buffer. + crate fn finish(self) -> String { + self.buf + } +} + +/// This is just a guess at the average length of a URL part, +/// used for [`String::with_capacity`] calls in the [`FromIterator`] +/// and [`Extend`] impls. +/// +/// This is intentionally on the lower end to avoid overallocating. +const AVG_PART_LENGTH: usize = 5; + +impl<'a> FromIterator<&'a str> for UrlPartsBuilder { + fn from_iter>(iter: T) -> Self { + let iter = iter.into_iter(); + let mut builder = Self::with_capacity_bytes(AVG_PART_LENGTH * iter.size_hint().0); + iter.for_each(|part| builder.push(part)); + builder + } +} + +impl<'a> Extend<&'a str> for UrlPartsBuilder { + fn extend>(&mut self, iter: T) { + let iter = iter.into_iter(); + self.buf.reserve(AVG_PART_LENGTH * iter.size_hint().0); + iter.for_each(|part| self.push(part)); + } +} + +#[cfg(test)] +mod tests; diff --git a/src/librustdoc/html/url_parts_builder/tests.rs b/src/librustdoc/html/url_parts_builder/tests.rs new file mode 100644 index 00000000000..43338c95010 --- /dev/null +++ b/src/librustdoc/html/url_parts_builder/tests.rs @@ -0,0 +1,54 @@ +use super::*; + +fn t(builder: UrlPartsBuilder, expect: &str) { + assert_eq!(builder.finish(), expect); +} + +#[test] +fn empty() { + t(UrlPartsBuilder::new(), ""); +} + +#[test] +fn singleton() { + t(UrlPartsBuilder::singleton("index.html"), "index.html"); +} + +#[test] +fn push_several() { + let mut builder = UrlPartsBuilder::new(); + builder.push("core"); + builder.push("str"); + builder.push("struct.Bytes.html"); + t(builder, "core/str/struct.Bytes.html"); +} + +#[test] +fn push_front_empty() { + let mut builder = UrlPartsBuilder::new(); + builder.push_front("page.html"); + t(builder, "page.html"); +} + +#[test] +fn push_front_non_empty() { + let mut builder = UrlPartsBuilder::new(); + builder.push("core"); + builder.push("str"); + builder.push("struct.Bytes.html"); + builder.push_front("nightly"); + t(builder, "nightly/core/str/struct.Bytes.html"); +} + +#[test] +fn collect() { + t(["core", "str"].into_iter().collect(), "core/str"); + t(["core", "str", "struct.Bytes.html"].into_iter().collect(), "core/str/struct.Bytes.html"); +} + +#[test] +fn extend() { + let mut builder = UrlPartsBuilder::singleton("core"); + builder.extend(["str", "struct.Bytes.html"]); + t(builder, "core/str/struct.Bytes.html"); +}