mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-07-10 03:18:48 -07:00
Some updates
This commit is contained in:
Generated
+99
-113
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -13,7 +13,7 @@ grass = "0.13"
|
||||
image = "0.25"
|
||||
lightningcss = "1.0.0-alpha"
|
||||
mime = "0.3"
|
||||
minijinja = { version = "2.2", features = ["loader"] }
|
||||
minijinja = { version = "2.2", features = ["loader", "json"] }
|
||||
minijinja-autoreload = "2.2"
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
objdiff-core = { git = "https://github.com/encounter/objdiff", tag = "v2.0.0-beta.6", features = ["bindings"] }
|
||||
@@ -36,6 +36,6 @@ tower = { version = "0.4", features = ["full"] }
|
||||
tower-http = { version = "0.5", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
treemap = "0.3"
|
||||
url = { version = "2.5", features = ["serde"] }
|
||||
zip = { version = "2.2", default-features = false, features = ["deflate-flate2"] }
|
||||
zstd = "0.13"
|
||||
|
||||
+3
-2
@@ -255,7 +255,8 @@ $unit-size: 0.75rem;
|
||||
margin-bottom: var(--pico-spacing);
|
||||
touch-action: none;
|
||||
// Accessed via JS
|
||||
--font-weight: 400;
|
||||
--font-size: 1rem;
|
||||
--font-weight: normal;
|
||||
--font-size: 0.875rem;
|
||||
--font-family: var(--pico-font-family);
|
||||
--tooltip-background: var(--pico-tooltip-background-color);
|
||||
}
|
||||
|
||||
+59
-28
@@ -10,48 +10,75 @@ type Unit = {
|
||||
|
||||
const unitBounds = (unit: Unit, width: number, height: number) => {
|
||||
return {
|
||||
x: (unit.x / 100) * width,
|
||||
y: (unit.y / 100) * height,
|
||||
w: (unit.w / 100) * width,
|
||||
h: (unit.h / 100) * height,
|
||||
x: unit.x * width,
|
||||
y: unit.y * height,
|
||||
w: unit.w * width,
|
||||
h: unit.h * height,
|
||||
};
|
||||
}
|
||||
|
||||
const TOOLTIP_PADDING = 10;
|
||||
const TOOLTIP_MARGIN = 10;
|
||||
const BORDER_RADIUS = 5;
|
||||
const PADDING_W = 10;
|
||||
const PADDING_H = 5;
|
||||
const MARGIN = 5;
|
||||
|
||||
const drawTooltip = (ctx: CanvasRenderingContext2D, unit: Unit, width: number, height: number) => {
|
||||
const style = getComputedStyle(ctx.canvas);
|
||||
const fontWeight = style.getPropertyValue('--font-weight') || 'normal';
|
||||
const fontSize = style.getPropertyValue('--font-size') || '16px';
|
||||
const fontFamily = style.getPropertyValue('--font-family') || 'sans-serif';
|
||||
const tooltipBackground = style.getPropertyValue('--tooltip-background') || "#fff"
|
||||
ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
const {x, y, w, h} = unitBounds(unit, width, height);
|
||||
const text = `${unit.name} • ${unit.fuzzy_match_percent.toFixed(2)}%`;
|
||||
const m = ctx.measureText(text);
|
||||
const hw = m.actualBoundingBoxRight + m.actualBoundingBoxLeft + TOOLTIP_PADDING * 2;
|
||||
const bh = m.actualBoundingBoxAscent + m.actualBoundingBoxDescent + TOOLTIP_PADDING * 2;
|
||||
let bx = x + (w - hw) / 2 + TOOLTIP_PADDING;
|
||||
let by = y - TOOLTIP_MARGIN;
|
||||
const bw = m.actualBoundingBoxRight + m.actualBoundingBoxLeft + PADDING_W * 2;
|
||||
const bh = m.fontBoundingBoxAscent + m.fontBoundingBoxDescent + PADDING_H * 2;
|
||||
const margin = isTouch ? MARGIN * 2 : MARGIN;
|
||||
let bx = x + (w - bw) / 2 + PADDING_W;
|
||||
let by = y - bh - margin;
|
||||
let invY = false;
|
||||
if (bx + bw > width) {
|
||||
bx = width - bw;
|
||||
}
|
||||
if (bx < 0) {
|
||||
bx = 0;
|
||||
}
|
||||
if (bx + hw > width) {
|
||||
bx = width - hw;
|
||||
if (by < 0) {
|
||||
by = y + h + margin;
|
||||
invY = true;
|
||||
}
|
||||
if (by - bh < 0) {
|
||||
by = y + h + bh + TOOLTIP_MARGIN;
|
||||
ctx.fillStyle = tooltipBackground;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(bx, by, bw, bh, BORDER_RADIUS);
|
||||
// Arrow
|
||||
const ax = x + w / 2;
|
||||
if (invY) {
|
||||
ctx.moveTo(ax, y + h);
|
||||
ctx.lineTo(ax + margin, y + h + margin);
|
||||
ctx.lineTo(ax - margin, y + h + margin);
|
||||
} else {
|
||||
ctx.moveTo(ax, y);
|
||||
ctx.lineTo(ax + margin, y - margin);
|
||||
ctx.lineTo(ax - margin, y - margin);
|
||||
}
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
|
||||
ctx.fillRect(bx, by - m.actualBoundingBoxAscent - TOOLTIP_PADDING * 2, hw, bh);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fillText(text, bx + TOOLTIP_PADDING, by - TOOLTIP_PADDING);
|
||||
ctx.fillText(text, bx + PADDING_W, by + bh / 2);
|
||||
};
|
||||
|
||||
let hovered = null;
|
||||
let dirty = false;
|
||||
let isTouch = false;
|
||||
|
||||
const draw = (canvas: HTMLCanvasElement, units: Unit[]) => {
|
||||
if (!canvas.getContext) {
|
||||
return;
|
||||
}
|
||||
const ratio = window.devicePixelRatio;
|
||||
const {width, height} = canvas.getBoundingClientRect();
|
||||
const ratio = window.devicePixelRatio;
|
||||
const renderWidth = width * ratio;
|
||||
const renderHeight = height * ratio;
|
||||
if (!dirty && canvas.width === renderWidth && canvas.height === renderHeight) {
|
||||
@@ -59,25 +86,23 @@ const draw = (canvas: HTMLCanvasElement, units: Unit[]) => {
|
||||
return;
|
||||
}
|
||||
dirty = false;
|
||||
// High DPI support
|
||||
if (canvas.width !== renderWidth || canvas.height !== renderHeight) {
|
||||
canvas.width = renderWidth;
|
||||
canvas.height = renderHeight;
|
||||
}
|
||||
const style = getComputedStyle(canvas);
|
||||
const fontWeight = style.getPropertyValue('--font-weight') || 'normal';
|
||||
const fontSize = style.getPropertyValue('--font-size') || '16px';
|
||||
const fontFamily = style.getPropertyValue('--font-family') || 'sans-serif';
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
||||
ctx.setTransform(ratio, 0, 0, ratio, 0, 0); // Scale to device pixel ratio
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = "#000";
|
||||
for (const unit of units) {
|
||||
const {x, y, w, h} = unitBounds(unit, width, height);
|
||||
ctx.fillStyle = unit.color;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, w, h);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
if (hovered) {
|
||||
const {x, y, w, h} = unitBounds(hovered, width, height);
|
||||
@@ -123,9 +148,15 @@ const drawGraph = (id: string, units: Unit[]) => {
|
||||
dirty = true;
|
||||
queueDraw();
|
||||
};
|
||||
canvas.addEventListener("mousemove", handleHover);
|
||||
canvas.addEventListener("mousemove", (e) => {
|
||||
isTouch = false;
|
||||
handleHover(e);
|
||||
});
|
||||
canvas.addEventListener("mouseleave", handleLeave);
|
||||
canvas.addEventListener("touchmove", (e) => handleHover(e.touches[0]));
|
||||
canvas.addEventListener("touchmove", (e) => {
|
||||
isTouch = true;
|
||||
handleHover(e.touches[0]);
|
||||
});
|
||||
canvas.addEventListener("touchend", handleLeave);
|
||||
draw(canvas, units);
|
||||
};
|
||||
|
||||
+4
-4
@@ -73,11 +73,11 @@ fn transform(
|
||||
) -> Result<CodegenReturn> {
|
||||
let source_type = SourceType::from_path(path)?;
|
||||
let allocator = Allocator::default();
|
||||
let parsed = Parser::new(&allocator, &source_text, source_type).parse();
|
||||
let parsed = Parser::new(&allocator, source_text, source_type).parse();
|
||||
let program = allocator.alloc(parsed.program);
|
||||
|
||||
let (symbols, scopes) = SemanticBuilder::new(&source_text, source_type)
|
||||
.build(&program)
|
||||
let (symbols, scopes) = SemanticBuilder::new(source_text, source_type)
|
||||
.build(program)
|
||||
.semantic
|
||||
.into_symbol_table_and_scope_tree();
|
||||
|
||||
@@ -91,7 +91,7 @@ fn transform(
|
||||
&allocator,
|
||||
path,
|
||||
source_type,
|
||||
&source_text,
|
||||
source_text,
|
||||
parsed.trivias.clone(),
|
||||
transform_options,
|
||||
)
|
||||
|
||||
+53
-4
@@ -1,10 +1,12 @@
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use std::{convert::Infallible, net::SocketAddr, str::FromStr, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
http::{header, HeaderMap, StatusCode},
|
||||
async_trait,
|
||||
extract::{ConnectInfo, FromRequestParts, OriginalUri},
|
||||
http::{header, request::Parts, HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
Extension, Router,
|
||||
};
|
||||
use bytes::BytesMut;
|
||||
use mime::Mime;
|
||||
@@ -14,9 +16,9 @@ use crate::AppState;
|
||||
|
||||
mod css;
|
||||
mod graph;
|
||||
mod js;
|
||||
mod project;
|
||||
mod report;
|
||||
mod js;
|
||||
|
||||
pub fn build_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
@@ -76,3 +78,50 @@ impl<T: Message> IntoResponse for Protobuf<T> {
|
||||
([(header::CONTENT_TYPE, APPLICATION_PROTOBUF)], bytes.freeze()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extractor for the full URI of the request, including the scheme and authority.
|
||||
/// Uses the `x-forwarded-proto` and `x-forwarded-host` headers if present.
|
||||
pub struct FullUri(pub Uri);
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for FullUri
|
||||
where S: Send + Sync
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let uri = Extension::<OriginalUri>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_or_else(|_| parts.uri.clone(), |Extension(OriginalUri(uri))| uri);
|
||||
let mut builder = Uri::builder();
|
||||
if let Some(scheme) =
|
||||
parts.headers.get("x-forwarded-proto").and_then(|value| value.to_str().ok())
|
||||
{
|
||||
builder = builder.scheme(scheme);
|
||||
} else if let Some(scheme) = uri.scheme().cloned() {
|
||||
builder = builder.scheme(scheme);
|
||||
} else {
|
||||
// TODO: native https?
|
||||
builder = builder.scheme("http");
|
||||
}
|
||||
if let Some(host) =
|
||||
parts.headers.get("x-forwarded-host").and_then(|value| value.to_str().ok())
|
||||
{
|
||||
builder = builder.authority(host);
|
||||
} else if let Some(host) =
|
||||
parts.headers.get(header::HOST).and_then(|value| value.to_str().ok())
|
||||
{
|
||||
builder = builder.authority(host);
|
||||
} else if let Some(authority) = uri.authority().cloned() {
|
||||
builder = builder.authority(authority);
|
||||
} else if let Ok(ConnectInfo(socket_addr)) =
|
||||
ConnectInfo::<SocketAddr>::from_request_parts(parts, state).await
|
||||
{
|
||||
builder = builder.authority(socket_addr.to_string());
|
||||
}
|
||||
if let Some(path_and_query) = uri.path_and_query().cloned() {
|
||||
builder = builder.path_and_query(path_and_query);
|
||||
}
|
||||
Ok(FullUri(builder.build().unwrap_or(uri)))
|
||||
}
|
||||
}
|
||||
|
||||
+73
-32
@@ -2,21 +2,23 @@ use std::{iter, str::FromStr, time::Instant};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::{header, HeaderMap, StatusCode},
|
||||
extract::{ Path, Query, State},
|
||||
http::{header, HeaderMap, StatusCode, Uri},
|
||||
response::{Html, IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use image::ImageFormat;
|
||||
use mime::Mime;
|
||||
use objdiff_core::bindings::report::Measures;
|
||||
use objdiff_core::bindings::report::{Measures, ReportCategory};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use super::{graph::layout_units, parse_accept, AppError, Protobuf, PROTOBUF};
|
||||
use super::{graph::layout_units, parse_accept, AppError, FullUri, Protobuf, PROTOBUF};
|
||||
use crate::{
|
||||
handlers::graph::{render_image, render_svg, unit_color},
|
||||
models::{Project, ProjectInfo, ReportFile},
|
||||
templates::render,
|
||||
util::UrlExt,
|
||||
AppState,
|
||||
};
|
||||
|
||||
@@ -55,12 +57,14 @@ struct ReportTemplateContext<'a> {
|
||||
version: &'a str,
|
||||
measures: TemplateMeasures,
|
||||
units: &'a [ReportTemplateUnit<'a>],
|
||||
versions: &'a [String],
|
||||
versions: &'a [ReportTemplateVersion<'a>],
|
||||
prev_commit: Option<&'a str>,
|
||||
next_commit: Option<&'a str>,
|
||||
categories: &'a [ReportCategoryItem<'a>],
|
||||
current_category: &'a ReportCategoryItem<'a>,
|
||||
units_json: String,
|
||||
canonical_path: &'a str,
|
||||
canonical_url: &'a str,
|
||||
image_url: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -74,14 +78,17 @@ pub struct ReportTemplateUnit<'a> {
|
||||
h: f32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Copy, Clone)]
|
||||
#[derive(Serialize, Clone)]
|
||||
struct ReportCategoryItem<'a> {
|
||||
id: &'a str,
|
||||
name: &'a str,
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl Default for ReportCategoryItem<'static> {
|
||||
fn default() -> Self { Self { id: "all", name: "All" } }
|
||||
#[derive(Serialize, Clone)]
|
||||
struct ReportTemplateVersion<'a> {
|
||||
id: &'a str,
|
||||
path: String,
|
||||
}
|
||||
|
||||
/// Duplicate of Measures to avoid omitting empty fields
|
||||
@@ -159,13 +166,14 @@ fn extract_extension(params: ReportParams) -> (ReportParams, Option<String>) {
|
||||
} else if let Some((repo, ext)) = params.repo.rsplit_once('.') {
|
||||
return (ReportParams { repo: repo.to_string(), ..params }, Some(ext.to_string()));
|
||||
}
|
||||
return (params, None);
|
||||
(params, None)
|
||||
}
|
||||
|
||||
pub async fn get_report(
|
||||
Path(params): Path<ReportParams>,
|
||||
Query(query): Query<ReportQuery>,
|
||||
headers: HeaderMap,
|
||||
FullUri(uri): FullUri,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Response, AppError> {
|
||||
let start = Instant::now();
|
||||
@@ -225,9 +233,10 @@ pub async fn get_report(
|
||||
&report,
|
||||
&project_info,
|
||||
measures,
|
||||
¤t_category,
|
||||
current_category,
|
||||
&units,
|
||||
&state,
|
||||
uri,
|
||||
)?;
|
||||
let elapsed = start.elapsed();
|
||||
rendered = rendered.replace("[[time]]", &format!("{}ms", elapsed.as_millis()));
|
||||
@@ -274,15 +283,15 @@ const EMPTY_MEASURES: Measures = Measures {
|
||||
fn apply_category<'a>(
|
||||
report: &'a ReportFile,
|
||||
query: &ReportQuery,
|
||||
) -> Result<(&'a Measures, ReportCategoryItem<'a>, Vec<ReportTemplateUnit<'a>>)> {
|
||||
) -> Result<(&'a Measures, Option<&'a ReportCategory>, Vec<ReportTemplateUnit<'a>>)> {
|
||||
let mut measures = report.report.measures.as_ref().unwrap_or(&EMPTY_MEASURES);
|
||||
let mut current_category = ReportCategoryItem::default();
|
||||
let mut current_category = None;
|
||||
let mut category_id_filter = None;
|
||||
if let Some(category) =
|
||||
query.category.as_ref().and_then(|id| report.report.categories.iter().find(|c| c.id == *id))
|
||||
{
|
||||
measures = category.measures.as_ref().unwrap_or(&EMPTY_MEASURES);
|
||||
current_category = ReportCategoryItem { id: &category.id, name: &category.name };
|
||||
current_category = Some(category);
|
||||
category_id_filter = Some(category.id.clone());
|
||||
}
|
||||
let (w, h) = query.size();
|
||||
@@ -313,10 +322,10 @@ fn apply_category<'a>(
|
||||
name: &unit.name,
|
||||
fuzzy_match_percent: match_percent,
|
||||
color: unit_color(match_percent),
|
||||
x: bounds.x * 100.0,
|
||||
y: bounds.y * 100.0,
|
||||
w: bounds.w * 100.0,
|
||||
h: bounds.h * 100.0,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
w: bounds.w,
|
||||
h: bounds.h,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -327,35 +336,67 @@ fn render_template(
|
||||
report: &ReportFile,
|
||||
project_info: &ProjectInfo,
|
||||
measures: &Measures,
|
||||
current_category: &ReportCategoryItem,
|
||||
current_category: Option<&ReportCategory>,
|
||||
units: &[ReportTemplateUnit],
|
||||
state: &AppState,
|
||||
uri: Uri,
|
||||
) -> Result<String> {
|
||||
let categories = iter::once(ReportCategoryItem::default())
|
||||
.chain(
|
||||
report
|
||||
.report
|
||||
.categories
|
||||
let request_url = Url::parse(&uri.to_string()).context("Failed to parse URI")?;
|
||||
let project_base_path =
|
||||
format!("/{}/{}", project_info.project.owner, project_info.project.repo);
|
||||
let canonical_url = request_url.with_path(&format!(
|
||||
"/{}/{}/{}/{}",
|
||||
project_info.project.owner, project_info.project.repo, report.version, report.commit.sha
|
||||
));
|
||||
let image_url = canonical_url.with_path(&format!("{}.png", canonical_url.path()));
|
||||
|
||||
let versions = project_info
|
||||
.report_versions
|
||||
.iter()
|
||||
.map(|c| ReportCategoryItem { id: &c.id, name: &c.name }),
|
||||
)
|
||||
.map(|version| {
|
||||
let version_url = request_url.with_path(&format!(
|
||||
"/{}/{}/{}/{}",
|
||||
project_info.project.owner, project_info.project.repo, version, report.commit.sha
|
||||
));
|
||||
ReportTemplateVersion { id: version, path: version_url.path_and_query().to_string() }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let units_json = serde_json::to_string(&units).context("Failed to serialize units")?;
|
||||
|
||||
let all_url = canonical_url.set_query("category", None);
|
||||
let all_category =
|
||||
ReportCategoryItem { id: "all", name: "All", path: all_url.path_and_query().to_string() };
|
||||
let current_category = current_category
|
||||
.map(|c| {
|
||||
let path =
|
||||
canonical_url.set_query("category", Some(&c.id)).path_and_query().to_string();
|
||||
ReportCategoryItem { id: &c.id, name: &c.name, path }
|
||||
})
|
||||
.unwrap_or_else(|| all_category.clone());
|
||||
let categories = iter::once(all_category)
|
||||
.chain(report.report.categories.iter().map(|c| {
|
||||
let path =
|
||||
canonical_url.set_query("category", Some(&c.id)).path_and_query().to_string();
|
||||
ReportCategoryItem { id: &c.id, name: &c.name, path }
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
render(&state.templates, "report.html", ReportTemplateContext {
|
||||
project: &report.project,
|
||||
project_name: &report.project.name(),
|
||||
project_short_name: &report.project.short_name(),
|
||||
project_short_name: report.project.short_name(),
|
||||
project_url: &report.project.repo_url(),
|
||||
project_path: &format!("/{}/{}", report.project.owner, report.project.repo),
|
||||
project_path: &project_base_path,
|
||||
commit: &report.commit.sha,
|
||||
version: &report.version,
|
||||
measures: TemplateMeasures::from(measures),
|
||||
units,
|
||||
versions: &project_info.report_versions,
|
||||
versions: &versions,
|
||||
prev_commit: project_info.prev_commit.as_deref(),
|
||||
next_commit: project_info.next_commit.as_deref(),
|
||||
categories: &categories,
|
||||
current_category,
|
||||
units_json,
|
||||
current_category: ¤t_category,
|
||||
canonical_path: canonical_url.path_and_query(),
|
||||
canonical_url: canonical_url.as_ref(),
|
||||
image_url: image_url.as_ref(),
|
||||
})
|
||||
}
|
||||
|
||||
+5
-1
@@ -5,6 +5,7 @@ mod github;
|
||||
mod handlers;
|
||||
mod models;
|
||||
mod templates;
|
||||
mod util;
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
@@ -51,7 +52,10 @@ async fn main() {
|
||||
// Run our service
|
||||
let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, state.config.server.port));
|
||||
tracing::info!("Listening on {}", addr);
|
||||
axum::serve(TcpListener::bind(addr).await.expect("bind error"), app(state).into_make_service())
|
||||
axum::serve(
|
||||
TcpListener::bind(addr).await.expect("bind error"),
|
||||
app(state).into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await
|
||||
.expect("server error");
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
use url::Url;
|
||||
|
||||
pub trait UrlExt {
|
||||
fn set_query(&self, key: &str, value: Option<&str>) -> Url;
|
||||
fn with_path(&self, path: &str) -> Url;
|
||||
fn path_and_query(&self) -> &str;
|
||||
}
|
||||
|
||||
impl UrlExt for Url {
|
||||
#[inline]
|
||||
fn set_query(&self, key: &str, value: Option<&str>) -> Url {
|
||||
let mut out = self.clone();
|
||||
let mut pairs = out.query_pairs_mut();
|
||||
pairs.clear();
|
||||
let mut updated = false;
|
||||
for (k, v) in self.query_pairs() {
|
||||
if k == key {
|
||||
if let Some(value) = value {
|
||||
if value.is_empty() {
|
||||
pairs.append_key_only(&k);
|
||||
} else {
|
||||
pairs.append_pair(&k, value);
|
||||
}
|
||||
}
|
||||
updated = true;
|
||||
} else if v.is_empty() {
|
||||
pairs.append_key_only(&k);
|
||||
} else {
|
||||
pairs.append_pair(&k, &v);
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
if let Some(value) = value {
|
||||
pairs.append_pair(key, value);
|
||||
}
|
||||
}
|
||||
drop(pairs);
|
||||
out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn with_path(&self, path: &str) -> Url {
|
||||
let mut out = self.clone();
|
||||
out.set_path(path);
|
||||
out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn path_and_query(&self) -> &str { &self[url::Position::BeforePath..] }
|
||||
}
|
||||
@@ -7,15 +7,15 @@
|
||||
<meta name="darkreader-lock">
|
||||
<meta name="description" content="Decompilation progress report for {{ project_name }}{% if current_category.id != 'all' %} ({{ current_category.name }}){% endif %}">
|
||||
<title>{{ project_short_name }}{% if current_category.id != "all" %} ({{ current_category.name }}){% endif %} • Progress Report</title>
|
||||
<link rel="stylesheet" href="/css/main.min.css?2">
|
||||
<link rel="stylesheet" href="/css/main.min.css?3">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.2"
|
||||
integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="/js/graph.min.js?2"></script>
|
||||
<meta property="og:type" content="website">
|
||||
<script src="/js/graph.min.js?5"></script>
|
||||
<meta property="og:title" content="{{ project_short_name }}{% if current_category.id != 'all' %} ({{ current_category.name }}){% endif %} is {{ measures.matched_code_percent | round(2) }}% decompiled">
|
||||
<meta property="og:description" content="Decompilation progress report for {{ project_name }}{% if current_category.id != 'all' %} ({{ current_category.name }}){% endif %}">
|
||||
<meta property="og:image" content="https://decomp.dev{{ project_path | safe }}/{{ version }}/{{ commit }}.png">
|
||||
<meta property="og:image" content="{{ image_url | safe }}">
|
||||
<meta property="og:url" content="{{ canonical_url | safe }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@@ -37,7 +37,7 @@
|
||||
<summary>{{ version }}</summary>
|
||||
<ul dir="rtl">
|
||||
{% for version in versions %}
|
||||
<li><a href="{{ project_path | safe }}/{{ version }}">{{ version }}</a></li>
|
||||
<li><a href="{{ version.path | safe }}">{{ version.id }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
@@ -94,17 +94,17 @@
|
||||
<summary>{{ current_category.name }}</summary>
|
||||
<ul>
|
||||
{% for category in categories %}
|
||||
<li><a href="?category={{ category.id }}">{{ category.name }}</a></li>
|
||||
<li><a href="{{ category.path | safe }}">{{ category.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
<script>
|
||||
document.write('<canvas id="graph" width="100%" data-hx-preserve="true"></canvas>');
|
||||
drawGraph("graph", {{ units_json | safe }});
|
||||
drawGraph("graph", {{ units | tojson }});
|
||||
</script>
|
||||
<noscript>
|
||||
<img id="graph" src="{{ project_path | safe }}/{{ version }}/{{ commit }}.png" alt="Progress graph">
|
||||
<img id="graph" src="{{ image_url | safe }}" alt="Progress graph">
|
||||
</noscript>
|
||||
<h6 class="report-header">Links</h6>
|
||||
<div>
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<svg viewBox="0 0 {{ w }} {{ h }}" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>.unit { stroke: #000; stroke-width: 1; }</style>
|
||||
{% for unit in units %}
|
||||
<rect class="unit" width="{{ unit.w }}%" height="{{ unit.h }}%" x="{{ unit.x }}%" y="{{ unit.y }}%" fill="{{ unit.color }}" />
|
||||
<rect class="unit" width="{{ unit.w * 100.0 }}%" height="{{ unit.h * 100.0 }}%" x="{{ unit.x * 100.0 }}%" y="{{ unit.y * 100.0 }}%" fill="{{ unit.color }}" />
|
||||
{% endfor %}
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 386 B |
Reference in New Issue
Block a user