Skip to content

Commit 20a14d4

Browse files
Only put platforms list is less than 6, otherwise load with AJAX
1 parent ea7eb5c commit 20a14d4

File tree

6 files changed

+234
-41
lines changed

6 files changed

+234
-41
lines changed

src/web/crate_details.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use super::{markdown, match_version, MatchSemver, MetaData};
22
use crate::utils::{get_correct_docsrs_style_file, report_error, spawn_blocking};
3+
use crate::web::rustdoc::RustdocHtmlParams;
4+
use crate::web::{axum_cached_redirect, match_version_axum};
35
use crate::{
46
db::Pool,
57
impl_axum_webpage,
@@ -15,6 +17,7 @@ use crate::{
1517
use anyhow::{Context, Result};
1618
use axum::{
1719
extract::{Extension, Path},
20+
http::Uri,
1821
response::{IntoResponse, Response as AxumResponse},
1922
};
2023
use chrono::{DateTime, Utc};
@@ -23,6 +26,7 @@ use serde::Deserialize;
2326
use serde::{ser::Serializer, Serialize};
2427
use serde_json::Value;
2528
use std::sync::Arc;
29+
use tracing::{instrument, trace};
2630

2731
// TODO: Add target name and versions
2832

@@ -466,6 +470,167 @@ pub(crate) async fn get_all_releases(
466470
Ok(res.into_response())
467471
}
468472

473+
#[derive(Debug, Clone, PartialEq, Serialize)]
474+
struct ShortMetadata {
475+
name: String,
476+
version_or_latest: String,
477+
doc_targets: Vec<String>,
478+
}
479+
480+
#[derive(Debug, Clone, PartialEq, Serialize)]
481+
struct PlatformList {
482+
metadata: ShortMetadata,
483+
inner_path: String,
484+
use_direct_platform_links: bool,
485+
current_target: String,
486+
}
487+
488+
impl_axum_webpage! {
489+
PlatformList = "rustdoc/platforms.html",
490+
cpu_intensive_rendering = true,
491+
}
492+
493+
#[tracing::instrument]
494+
pub(crate) async fn get_all_platforms(
495+
Path(params): Path<RustdocHtmlParams>,
496+
Extension(pool): Extension<Pool>,
497+
uri: Uri,
498+
) -> AxumResult<AxumResponse> {
499+
// since we directly use the Uri-path and not the extracted params from the router,
500+
// we have to percent-decode the string here.
501+
let original_path = percent_encoding::percent_decode(uri.path().as_bytes())
502+
.decode_utf8()
503+
.map_err(|_| AxumNope::BadRequest)?;
504+
let mut req_path: Vec<&str> = original_path.split('/').collect();
505+
506+
let release_found = match_version_axum(&pool, &params.name, Some(&params.version)).await?;
507+
trace!(?release_found, "found release");
508+
509+
// Remove the empty start, "releases", the name and the version from the path
510+
req_path.drain(..4).for_each(drop);
511+
512+
// Convenience function to allow for easy redirection
513+
#[instrument]
514+
fn redirect(
515+
name: &str,
516+
vers: &str,
517+
path: &[&str],
518+
cache_policy: CachePolicy,
519+
) -> AxumResult<AxumResponse> {
520+
trace!("redirect");
521+
// Format and parse the redirect url
522+
Ok(axum_cached_redirect(
523+
encode_url_path(&format!("/platforms/{}/{}/{}", name, vers, path.join("/"))),
524+
cache_policy,
525+
)?
526+
.into_response())
527+
}
528+
529+
let (version, version_or_latest) = match release_found.version {
530+
MatchSemver::Exact((version, _)) => {
531+
// Redirect when the requested crate name isn't correct
532+
if let Some(name) = release_found.corrected_name {
533+
return redirect(&name, &version, &req_path, CachePolicy::NoCaching);
534+
}
535+
536+
(version.clone(), version)
537+
}
538+
539+
MatchSemver::Latest((version, _)) => {
540+
// Redirect when the requested crate name isn't correct
541+
if let Some(name) = release_found.corrected_name {
542+
return redirect(&name, "latest", &req_path, CachePolicy::NoCaching);
543+
}
544+
545+
(version, "latest".to_string())
546+
}
547+
548+
// Redirect when the requested version isn't correct
549+
MatchSemver::Semver((v, _)) => {
550+
// to prevent cloudfront caching the wrong artifacts on URLs with loose semver
551+
// versions, redirect the browser to the returned version instead of loading it
552+
// immediately
553+
return redirect(&params.name, &v, &req_path, CachePolicy::ForeverInCdn);
554+
}
555+
};
556+
557+
let (name, doc_targets, releases, default_target): (String, Vec<String>, Vec<Release>, String) =
558+
spawn_blocking({
559+
let pool = pool.clone();
560+
move || {
561+
let mut conn = pool.get()?;
562+
let query = "
563+
SELECT
564+
crates.id,
565+
crates.name,
566+
releases.default_target,
567+
releases.doc_targets
568+
FROM releases
569+
INNER JOIN crates ON releases.crate_id = crates.id
570+
WHERE crates.name = $1 AND releases.version = $2;";
571+
572+
let rows = conn.query(query, &[&params.name, &version])?;
573+
574+
let krate = if rows.is_empty() {
575+
return Err(AxumNope::CrateNotFound.into());
576+
} else {
577+
&rows[0]
578+
};
579+
580+
// get releases, sorted by semver
581+
let releases = releases_for_crate(&mut *conn, krate.get("id"))?;
582+
583+
Ok((
584+
krate.get("name"),
585+
MetaData::parse_doc_targets(krate.get("doc_targets")),
586+
releases,
587+
krate.get("default_target"),
588+
))
589+
}
590+
})
591+
.await?;
592+
593+
let latest_release = releases
594+
.iter()
595+
.find(|release| release.version.pre.is_empty() && !release.yanked)
596+
.unwrap_or(&releases[0]);
597+
598+
// The path within this crate version's rustdoc output
599+
let (target, inner_path) = {
600+
let mut inner_path = req_path.clone();
601+
602+
let target = if inner_path.len() > 1 && doc_targets.iter().any(|s| s == inner_path[0]) {
603+
inner_path.remove(0)
604+
} else {
605+
""
606+
};
607+
608+
(target, inner_path.join("/"))
609+
};
610+
611+
let current_target = if latest_release.build_status {
612+
if target.is_empty() {
613+
default_target
614+
} else {
615+
target.to_owned()
616+
}
617+
} else {
618+
String::new()
619+
};
620+
621+
let res = PlatformList {
622+
metadata: ShortMetadata {
623+
name,
624+
version_or_latest: version_or_latest.to_string(),
625+
doc_targets,
626+
},
627+
inner_path,
628+
use_direct_platform_links: true,
629+
current_target,
630+
};
631+
Ok(res.into_response())
632+
}
633+
469634
#[cfg(test)]
470635
mod tests {
471636
use super::*;

src/web/routes.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,15 @@ pub(super) fn build_axum_routes() -> AxumRouter {
181181
get_internal(super::crate_details::crate_details_handler),
182182
)
183183
.route(
184-
"/:name/releases",
184+
"/platforms/:name/:version/:target/",
185+
get_internal(super::crate_details::get_all_platforms),
186+
)
187+
.route(
188+
"/platforms/:name/:version/:target/*path",
189+
get_internal(super::crate_details::get_all_platforms),
190+
)
191+
.route(
192+
"/releases/list/:name",
185193
get_internal(super::crate_details::get_all_releases),
186194
)
187195
.route_with_tsr(

src/web/rustdoc.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -349,17 +349,17 @@ impl RustdocPage {
349349
}
350350
}
351351

352-
#[derive(Clone, Deserialize)]
352+
#[derive(Clone, Deserialize, Debug)]
353353
pub(crate) struct RustdocHtmlParams {
354-
name: String,
355-
version: String,
354+
pub(crate) name: String,
355+
pub(crate) version: String,
356356
// both target and path are only used for matching the route.
357357
// The actual path is read from the request `Uri` because
358358
// we have some static filenames directly in the routes.
359359
#[allow(dead_code)]
360-
target: Option<String>,
360+
pub(crate) target: Option<String>,
361361
#[allow(dead_code)]
362-
path: Option<String>,
362+
pub(crate) path: Option<String>,
363363
}
364364

365365
/// Serves documentation generated by rustdoc.

static/menu.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ const updateMenuPositionForSubMenu = (currentMenuSupplier) => {
55
subMenu?.style.setProperty('--menu-x', `${currentMenu.getBoundingClientRect().x}px`);
66
}
77

8-
function generateReleaseList(data, crateName) {
9-
}
8+
const loadedMenus = new Set();
109

11-
let loadReleases = function() {
12-
const releaseListElem = document.getElementById('releases-list');
13-
// To prevent reloading the list unnecessarily.
14-
loadReleases = function() {};
10+
function loadReleases(menu, id, msg, path, extra) {
11+
if (loadedMenus.has(id)) {
12+
return;
13+
}
14+
loadedMenus.add(id);
15+
if (!menu.querySelector(".rotate")) {
16+
return;
17+
}
18+
const releaseListElem = document.getElementById(id);
1519
if (!releaseListElem) {
1620
// We're not in a documentation page, so no need to do anything.
1721
return;
@@ -25,11 +29,11 @@ let loadReleases = function() {
2529
if (xhttp.status === 200) {
2630
releaseListElem.innerHTML = xhttp.responseText;
2731
} else {
28-
console.error(`Failed to load release list: [${xhttp.status}] ${xhttp.responseText}`);
29-
document.getElementById('releases-list').innerHTML = "Failed to load release list";
32+
console.error(`Failed to load ${msg}: [${xhttp.status}] ${xhttp.responseText}`);
33+
document.getElementById(id).innerHTML = `Failed to load ${msg}`;
3034
}
3135
};
32-
xhttp.open("GET", `/${crateName}/releases`, true);
36+
xhttp.open("GET", `/${path}/${crateName}${extra}`, true);
3337
xhttp.send();
3438
};
3539

@@ -81,7 +85,18 @@ let loadReleases = function() {
8185
currentMenu = newMenu;
8286
newMenu.className += " pure-menu-active";
8387
backdrop.style.display = "block";
84-
loadReleases();
88+
if (newMenu.querySelector("#releases-list")) {
89+
loadReleases(newMenu, "releases-list", "release list", "releases/list", "");
90+
} else if (newMenu.querySelector("#platforms")) {
91+
loadReleases(
92+
newMenu,
93+
"platforms",
94+
"platforms list",
95+
"platforms",
96+
// We get everything except the first crate name.
97+
"/" + window.location.pathname.split("/").slice(2).join("/")
98+
);
99+
}
85100
}
86101
function menuOnClick(e) {
87102
if (this.getAttribute("href") != "#") {

templates/rustdoc/platforms.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{%- for target in metadata.doc_targets -%}
2+
{#
3+
The crate-detail page is the only page where we want to allow google to follow
4+
the target-links. On that page we also don't have to use `/target-redirect/`
5+
because the documentation root page is guaranteed to exist for all targets.
6+
#}
7+
{%- if use_direct_platform_links -%}
8+
{%- set target_url = "/" ~ metadata.name ~ "/" ~ metadata.version_or_latest ~ "/" ~ target ~ "/" ~ inner_path -%}
9+
{%- set target_no_follow = "" -%}
10+
{%- else -%}
11+
{%- set target_url = "/crate/" ~ metadata.name ~ "/" ~ metadata.version_or_latest ~ "/target-redirect/" ~ target ~ "/" ~ inner_path -%}
12+
{%- set target_no_follow = "nofollow" -%}
13+
{%- endif -%}
14+
{%- if current_target is defined and current_target == target -%}
15+
{%- set current = " current" -%}
16+
{%- else -%}
17+
{%- set current = "" -%}
18+
{%- endif -%}
19+
20+
<li class="pure-menu-item">
21+
<a href="{{ target_url | safe }}" class="pure-menu-link{{ current | safe }}" data-fragment="retain" rel="{{ target_no_follow }}">
22+
{{- target -}}
23+
</a>
24+
</li>
25+
{%- endfor -%}

templates/rustdoc/topbar.html

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -209,31 +209,11 @@
209209

210210
{# Build the dropdown list showing available targets #}
211211
<ul class="pure-menu-children" id="platforms">
212-
{%- for target in metadata.doc_targets -%}
213-
{#
214-
The crate-detail page is the only page where we want to allow google to follow
215-
the target-links. On that page we also don't have to use `/target-redirect/`
216-
because the documentation root page is guaranteed to exist for all targets.
217-
#}
218-
{%- if use_direct_platform_links -%}
219-
{%- set target_url = "/" ~ metadata.name ~ "/" ~ metadata.version_or_latest ~ "/" ~ target ~ "/" ~ inner_path -%}
220-
{%- set target_no_follow = "" -%}
221-
{%- else -%}
222-
{%- set target_url = "/crate/" ~ metadata.name ~ "/" ~ metadata.version_or_latest ~ "/target-redirect/" ~ target ~ "/" ~ inner_path -%}
223-
{%- set target_no_follow = "nofollow" -%}
224-
{%- endif -%}
225-
{%- if current_target is defined and current_target == target -%}
226-
{%- set current = " current" -%}
227-
{%- else -%}
228-
{%- set current = "" -%}
229-
{%- endif -%}
230-
231-
<li class="pure-menu-item">
232-
<a href="{{ target_url | safe }}" class="pure-menu-link{{ current | safe }}" data-fragment="retain" rel="{{ target_no_follow }}">
233-
{{- target -}}
234-
</a>
235-
</li>
236-
{%- endfor -%}
212+
{%- if metadata.doc_targets|length < 6 -%}
213+
{%- include "rustdoc/targets.tml" -%}
214+
{%- else -%}
215+
<span class="rotate">{{ "spinner" | fas }}</span>
216+
{%- endif -%}
237217
</ul>
238218
</li>{#
239219
Display the features available in current build

0 commit comments

Comments
 (0)