rustdoc: Add UrlPartsBuilder

This is a type for efficiently and easily constructing the part of a URL
after the domain: `nightly/core/str/struct.Bytes.html`.

It allows simplifying some code and avoiding some allocations in the
`href_*` functions.

It will also allow making `Cache.paths` et al. use `Symbol` without
having to allocate `String`s in the `href_*` functions. `String`s would
be necessary otherwise because `Symbol::as_str()` returns `SymbolStr`,
whose `Deref<Target = str>` impl requires the `str` to not outlive it.
This is the primary motivation for the addition of `UrlPartsBuilder`.
This commit is contained in:
Noah Lev 2021-12-13 12:42:01 -08:00
parent 1796de7bb1
commit 4bac09f58f
5 changed files with 192 additions and 17 deletions

View File

@ -27,6 +27,8 @@
use crate::html::render::cache::ExternalLocation; use crate::html::render::cache::ExternalLocation;
use crate::html::render::Context; use crate::html::render::Context;
use super::url_parts_builder::UrlPartsBuilder;
crate trait Print { crate trait Print {
fn print(self, buffer: &mut Buffer); fn print(self, buffer: &mut Buffer);
} }
@ -544,9 +546,9 @@ fn to_module_fqp(shortty: ItemType, fqp: &[String]) -> &[String] {
ExternalLocation::Remote(ref s) => { ExternalLocation::Remote(ref s) => {
is_remote = true; is_remote = true;
let s = s.trim_end_matches('/'); let s = s.trim_end_matches('/');
let mut s = vec![s]; let mut builder = UrlPartsBuilder::singleton(s);
s.extend(module_fqp[..].iter().map(String::as_str)); builder.extend(module_fqp.iter().map(String::as_str));
s builder
} }
ExternalLocation::Local => href_relative_parts(module_fqp, relative_to), ExternalLocation::Local => href_relative_parts(module_fqp, relative_to),
ExternalLocation::Unknown => return Err(HrefError::DocumentationNotBuilt), ExternalLocation::Unknown => return Err(HrefError::DocumentationNotBuilt),
@ -560,22 +562,21 @@ fn to_module_fqp(shortty: ItemType, fqp: &[String]) -> &[String] {
if !is_remote { if !is_remote {
if let Some(root_path) = root_path { if let Some(root_path) = root_path {
let root = root_path.trim_end_matches('/'); let root = root_path.trim_end_matches('/');
url_parts.insert(0, root); url_parts.push_front(root);
} }
} }
debug!(?url_parts); debug!(?url_parts);
let last = &fqp.last().unwrap()[..]; let last = &fqp.last().unwrap()[..];
let filename;
match shortty { match shortty {
ItemType::Module => { ItemType::Module => {
url_parts.push("index.html"); url_parts.push("index.html");
} }
_ => { _ => {
filename = format!("{}.{}.html", shortty.as_str(), last); let filename = format!("{}.{}.html", shortty.as_str(), last);
url_parts.push(&filename); 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<String>), HrefError> { crate fn href(did: DefId, cx: &Context<'_>) -> Result<(String, ItemType, Vec<String>), HrefError> {
@ -585,7 +586,7 @@ fn to_module_fqp(shortty: ItemType, fqp: &[String]) -> &[String] {
/// Both paths should only be modules. /// Both paths should only be modules.
/// This is because modules get their own directories; that is, `std::vec` and `std::vec::Vec` will /// 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. /// 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() { 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) // e.g. linking to std::iter from std::vec (`dissimilar_part_count` will be 1)
if f != r { if f != r {
@ -603,7 +604,7 @@ fn to_module_fqp(shortty: ItemType, fqp: &[String]) -> &[String] {
iter::repeat("..").take(dissimilar_part_count).collect() iter::repeat("..").take(dissimilar_part_count).collect()
// linking to the same module // linking to the same module
} else { } else {
Vec::new() UrlPartsBuilder::new()
} }
} }

View File

@ -9,6 +9,7 @@
crate mod sources; crate mod sources;
crate mod static_files; crate mod static_files;
crate mod toc; crate mod toc;
mod url_parts_builder;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View File

@ -1,44 +1,44 @@
use crate::html::format::href_relative_parts; 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<String> = relative_to_fqp.iter().copied().map(String::from).collect(); let relative_to_fqp: Vec<String> = relative_to_fqp.iter().copied().map(String::from).collect();
let fqp: Vec<String> = fqp.iter().copied().map(String::from).collect(); let fqp: Vec<String> = 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] #[test]
fn href_relative_parts_basic() { fn href_relative_parts_basic() {
let relative_to_fqp = &["std", "vec"]; let relative_to_fqp = &["std", "vec"];
let fqp = &["std", "iter"]; let fqp = &["std", "iter"];
assert_relative_path(&["..", "iter"], relative_to_fqp, fqp); assert_relative_path("../iter", relative_to_fqp, fqp);
} }
#[test] #[test]
fn href_relative_parts_parent_module() { fn href_relative_parts_parent_module() {
let relative_to_fqp = &["std", "vec"]; let relative_to_fqp = &["std", "vec"];
let fqp = &["std"]; let fqp = &["std"];
assert_relative_path(&[".."], relative_to_fqp, fqp); assert_relative_path("..", relative_to_fqp, fqp);
} }
#[test] #[test]
fn href_relative_parts_different_crate() { fn href_relative_parts_different_crate() {
let relative_to_fqp = &["std", "vec"]; let relative_to_fqp = &["std", "vec"];
let fqp = &["core", "iter"]; let fqp = &["core", "iter"];
assert_relative_path(&["..", "..", "core", "iter"], relative_to_fqp, fqp); assert_relative_path("../../core/iter", relative_to_fqp, fqp);
} }
#[test] #[test]
fn href_relative_parts_same_module() { fn href_relative_parts_same_module() {
let relative_to_fqp = &["std", "vec"]; let relative_to_fqp = &["std", "vec"];
let fqp = &["std", "vec"]; let fqp = &["std", "vec"];
assert_relative_path(&[], relative_to_fqp, fqp); assert_relative_path("", relative_to_fqp, fqp);
} }
#[test] #[test]
fn href_relative_parts_child_module() { fn href_relative_parts_child_module() {
let relative_to_fqp = &["std"]; let relative_to_fqp = &["std"];
let fqp = &["std", "vec"]; let fqp = &["std", "vec"];
assert_relative_path(&["vec"], relative_to_fqp, fqp); assert_relative_path("vec", relative_to_fqp, fqp);
} }
#[test] #[test]
fn href_relative_parts_root() { fn href_relative_parts_root() {
let relative_to_fqp = &[]; let relative_to_fqp = &[];
let fqp = &["std"]; let fqp = &["std"];
assert_relative_path(&["std"], relative_to_fqp, fqp); assert_relative_path("std", relative_to_fqp, fqp);
} }

View File

@ -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<T: IntoIterator<Item = &'a str>>(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<T: IntoIterator<Item = &'a str>>(&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;

View File

@ -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");
}