Skip to content

Commit c717cec

Browse files
Only put platforms list is less than 6, otherwise load with AJAX
1 parent 5326a2b commit c717cec

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};
@@ -24,6 +27,7 @@ use serde::Deserialize;
2427
use serde::{ser::Serializer, Serialize};
2528
use serde_json::Value;
2629
use std::sync::Arc;
30+
use tracing::{instrument, trace};
2731

2832
// TODO: Add target name and versions
2933

@@ -475,6 +479,167 @@ pub(crate) async fn get_all_releases(
475479
Ok(res.into_response())
476480
}
477481

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