Skip to content

Commit

Permalink
rustdoc: Add UrlPartsBuilder
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
camelid committed Dec 13, 2021
1 parent 1796de7 commit 4bac09f
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 17 deletions.
19 changes: 10 additions & 9 deletions src/librustdoc/html/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ use crate::html::escape::Escape;
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);
}
Expand Down Expand Up @@ -544,9 +546,9 @@ crate fn href_with_root_path(
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),
Expand All @@ -560,22 +562,21 @@ crate fn href_with_root_path(
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<String>), HrefError> {
Expand All @@ -585,7 +586,7 @@ crate fn href(did: DefId, cx: &Context<'_>) -> Result<(String, ItemType, Vec<Str
/// 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 {
Expand All @@ -603,7 +604,7 @@ crate fn href_relative_parts<'a>(fqp: &'a [String], relative_to_fqp: &'a [String
iter::repeat("..").take(dissimilar_part_count).collect()
// linking to the same module
} else {
Vec::new()
UrlPartsBuilder::new()
}
}

Expand Down
1 change: 1 addition & 0 deletions src/librustdoc/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ crate mod render;
crate mod sources;
crate mod static_files;
crate mod toc;
mod url_parts_builder;

#[cfg(test)]
mod tests;
16 changes: 8 additions & 8 deletions src/librustdoc/html/tests.rs
Original file line number Diff line number Diff line change
@@ -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<String> = relative_to_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]
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);
}
119 changes: 119 additions & 0 deletions src/librustdoc/html/url_parts_builder.rs
Original file line number Diff line number Diff line change
@@ -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;
54 changes: 54 additions & 0 deletions src/librustdoc/html/url_parts_builder/tests.rs
Original file line number Diff line number Diff line change
@@ -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");
}

0 comments on commit 4bac09f

Please sign in to comment.