Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bcd6ada5d | |||
| 7cb1dfa2d1 | |||
| 67e63019c5 | |||
| 368034a177 | |||
| cdc2e40803 |
1472
Cargo.lock
generated
1472
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
54
Cargo.toml
54
Cargo.toml
@@ -40,7 +40,6 @@ mutex_atomic = "deny"
|
||||
needless_raw_strings = "deny"
|
||||
str_to_string = "deny"
|
||||
string_add = "deny"
|
||||
string_to_string = "deny"
|
||||
use_debug = "allow"
|
||||
verbose_file_reads = "deny"
|
||||
large_types_passed_by_value = "deny"
|
||||
@@ -57,6 +56,7 @@ unimplemented = "deny"
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
type_complexity = "allow"
|
||||
implicit_clone = "deny"
|
||||
|
||||
|
||||
#
|
||||
@@ -70,13 +70,18 @@ md-footnote = { path = "crates/lib/md-footnote" }
|
||||
md-dev = { path = "crates/lib/md-dev" }
|
||||
|
||||
service-webpage = { path = "crates/service/service-webpage" }
|
||||
service-pile = { path = "crates/service/service-pile" }
|
||||
service-assets = { path = "crates/service/service-assets" }
|
||||
|
||||
pile-client = { git = "https://git.betalupi.com/Mark/pile.git", rev = "90c5584513acde6f30f76d70c426cf6987643c1a" }
|
||||
|
||||
|
||||
#
|
||||
# MARK: Server
|
||||
#
|
||||
axum = { version = "0.8.6", features = ["macros", "multipart"] }
|
||||
tower-http = { version = "0.6.6", features = ["trace", "compression-full"] }
|
||||
tower = { version = "0.5.2" }
|
||||
axum = { version = "0.8.8", features = ["macros", "multipart"] }
|
||||
tower-http = { version = "0.6.8", features = ["trace", "compression-full"] }
|
||||
tower = { version = "0.5.3" }
|
||||
serde_urlencoded = { version = "0.7.1" }
|
||||
utoipa = "5.4.0"
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = [
|
||||
@@ -88,7 +93,7 @@ maud = { version = "0.27.0", features = ["axum"] }
|
||||
grass = { version = "0.13.4", features = ["macro"] }
|
||||
markdown-it = "0.6.1"
|
||||
emojis = "0.8.0"
|
||||
reqwest = { version = "0.12.24", default-features = false, features = [
|
||||
reqwest = { version = "0.12.28", default-features = false, features = [
|
||||
"http2",
|
||||
"rustls-tls",
|
||||
"rustls-tls-webpki-roots", # Need to recompile to update
|
||||
@@ -99,7 +104,7 @@ reqwest = { version = "0.12.24", default-features = false, features = [
|
||||
"charset",
|
||||
"blocking",
|
||||
] }
|
||||
servable = { version = "0.0.3", features = ["image", "htmx-2.0.8"] }
|
||||
servable = { version = "0.0.7", features = ["image", "htmx-2.0.8"] }
|
||||
#servable = { path = "../servable/crates/servable", features = [
|
||||
# "image",
|
||||
# "htmx-2.0.8",
|
||||
@@ -109,19 +114,19 @@ servable = { version = "0.0.3", features = ["image", "htmx-2.0.8"] }
|
||||
#
|
||||
# MARK: Async & Parallelism
|
||||
#
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
|
||||
#
|
||||
# MARK: CLI & logging
|
||||
#
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] }
|
||||
tracing-loki = { version = "0.2.6", features = [
|
||||
"rustls",
|
||||
"compat-0-2-1",
|
||||
], default-features = false }
|
||||
clap = { version = "4.5.51", features = ["derive"] }
|
||||
anstyle = { version = "1.0.13" }
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
anstyle = { version = "1.0.14" }
|
||||
envy = "0.4.2"
|
||||
dotenvy = "0.15.7"
|
||||
|
||||
@@ -129,8 +134,8 @@ dotenvy = "0.15.7"
|
||||
# MARK: Serialization & formats
|
||||
#
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
toml = "0.9.8"
|
||||
serde_json = "1.0.149"
|
||||
toml = "0.9.12"
|
||||
serde_yaml = "0.9"
|
||||
base64 = "0.22.1"
|
||||
|
||||
@@ -138,19 +143,20 @@ base64 = "0.22.1"
|
||||
# MARK: Misc helpers
|
||||
#
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
thiserror = "2.0.12"
|
||||
thiserror = "2.0.18"
|
||||
itertools = "0.14.0"
|
||||
anyhow = "1.0.97"
|
||||
url = { version = "2.5.7", features = ["serde"] }
|
||||
anyhow = "1.0.102"
|
||||
url = { version = "2.5.8", features = ["serde"] }
|
||||
num = "0.4.3"
|
||||
chrono = "0.4.42"
|
||||
lru = "0.16.2"
|
||||
chrono = "0.4.44"
|
||||
lru = "0.16.3"
|
||||
parking_lot = "0.12.5"
|
||||
lazy_static = "1.5.0"
|
||||
image = "0.25.8"
|
||||
image = "0.25.10"
|
||||
scraper = "0.24.0"
|
||||
futures = "0.3.31"
|
||||
tempfile = "3.23.0"
|
||||
futures = "0.3.32"
|
||||
tempfile = "3.27.0"
|
||||
mime = "0.3.17"
|
||||
|
||||
# md_* test utilities
|
||||
prettydiff = "0.9.0"
|
||||
@@ -159,7 +165,7 @@ testing = "18.0.0"
|
||||
#
|
||||
# Macro utilities
|
||||
#
|
||||
proc-macro2 = "1.0.95"
|
||||
syn = "2.0.101"
|
||||
quote = "1.0.40"
|
||||
proc-macro2 = "1.0.106"
|
||||
syn = "2.0.117"
|
||||
quote = "1.0.45"
|
||||
paste = "1.0.15"
|
||||
|
||||
@@ -10,7 +10,10 @@ workspace = true
|
||||
[dependencies]
|
||||
toolbox = { workspace = true, features = ["cli", "loki"] }
|
||||
libservice = { workspace = true }
|
||||
|
||||
service-webpage = { workspace = true }
|
||||
service-assets = { workspace = true }
|
||||
service-pile = { workspace = true }
|
||||
|
||||
tracing = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::{Context, Result};
|
||||
use libservice::{Service, ServiceConnectInfo, ToService};
|
||||
use service_assets::AssetService;
|
||||
use service_pile::PileService;
|
||||
use service_webpage::WebpageService;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
@@ -70,6 +72,16 @@ pub struct RouterState {}
|
||||
/// If state is none, dry-init
|
||||
pub async fn make_service(_state: Option<Arc<RouterState>>) -> Result<impl ToService> {
|
||||
let service_webpage = WebpageService::new();
|
||||
let service_assets = AssetService::new();
|
||||
let service_pile = PileService::new()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))
|
||||
.context("while initializing pile datasets")?;
|
||||
|
||||
Ok(Service::new().merge(service_webpage).to_service().trace())
|
||||
Ok(Service::new()
|
||||
.merge(service_webpage)
|
||||
.nest("/assets", service_assets)
|
||||
.nest("/pile", service_pile)
|
||||
.to_service()
|
||||
.trace())
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ impl BlockRule for FootnoteDefinitionScanner {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -8,21 +8,16 @@ use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::Subscribe
|
||||
// MARK: loglevel
|
||||
//
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, ValueEnum)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, ValueEnum, Default)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
#[default]
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Default for LogLevel {
|
||||
fn default() -> Self {
|
||||
Self::Info
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LogLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -47,6 +42,7 @@ pub struct LoggingConfig {
|
||||
|
||||
// Libs
|
||||
pub libservice: LogLevel,
|
||||
pub servable: LogLevel,
|
||||
pub toolbox: LogLevel,
|
||||
|
||||
// Bins
|
||||
@@ -72,15 +68,22 @@ impl From<LoggingConfig> for EnvFilter {
|
||||
format!("axum={}", conf.silence),
|
||||
format!("selectors={}", conf.silence),
|
||||
format!("html5ever={}", conf.silence),
|
||||
format!("tantivy={}", conf.silence),
|
||||
format!("aws_smithy_runtime={}", conf.silence),
|
||||
format!("aws_smithy_http_client={}", conf.silence),
|
||||
format!("aws_sdk_s3={}", conf.silence),
|
||||
format!("aws_sigv4={}", conf.silence),
|
||||
//
|
||||
// Libs
|
||||
//
|
||||
format!("toolbox={}", conf.toolbox),
|
||||
format!("libservice={}", conf.libservice),
|
||||
format!("servable={}", conf.servable),
|
||||
//
|
||||
// Bins
|
||||
//
|
||||
format!("service_webpage={}", conf.service),
|
||||
format!("service_pile={}", conf.service),
|
||||
format!("webpage={}", conf.webpage),
|
||||
conf.other.to_string(),
|
||||
]
|
||||
@@ -188,6 +191,7 @@ impl LogFilterPreset {
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Error,
|
||||
servable: LogLevel::Error,
|
||||
toolbox: LogLevel::Error,
|
||||
|
||||
// Bins
|
||||
@@ -201,6 +205,7 @@ impl LogFilterPreset {
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Warn,
|
||||
servable: LogLevel::Warn,
|
||||
toolbox: LogLevel::Warn,
|
||||
|
||||
// Bins
|
||||
@@ -214,6 +219,7 @@ impl LogFilterPreset {
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Info,
|
||||
servable: LogLevel::Info,
|
||||
toolbox: LogLevel::Info,
|
||||
|
||||
// Bins
|
||||
@@ -227,6 +233,7 @@ impl LogFilterPreset {
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Debug,
|
||||
servable: LogLevel::Debug,
|
||||
toolbox: LogLevel::Debug,
|
||||
|
||||
// Bins
|
||||
@@ -240,6 +247,7 @@ impl LogFilterPreset {
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Trace,
|
||||
servable: LogLevel::Trace,
|
||||
toolbox: LogLevel::Trace,
|
||||
|
||||
// Bins
|
||||
@@ -253,6 +261,7 @@ impl LogFilterPreset {
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Trace,
|
||||
servable: LogLevel::Trace,
|
||||
toolbox: LogLevel::Trace,
|
||||
|
||||
// Bins
|
||||
@@ -266,6 +275,7 @@ impl LogFilterPreset {
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Trace,
|
||||
servable: LogLevel::Trace,
|
||||
toolbox: LogLevel::Trace,
|
||||
|
||||
// Bins
|
||||
@@ -306,19 +316,14 @@ pub enum LoggingTarget {
|
||||
}
|
||||
|
||||
/// How to print logs
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Default)]
|
||||
pub enum LoggingFormat {
|
||||
#[default]
|
||||
Ansi,
|
||||
AnsiNoColor,
|
||||
Json,
|
||||
}
|
||||
|
||||
impl Default for LoggingFormat {
|
||||
fn default() -> Self {
|
||||
Self::Ansi
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoggingInitializer {
|
||||
pub app_name: &'static str,
|
||||
|
||||
|
||||
22
crates/service/service-assets/Cargo.toml
Normal file
22
crates/service/service-assets/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "service-assets"
|
||||
version = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
libservice = { workspace = true }
|
||||
|
||||
axum = { workspace = true }
|
||||
maud = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
servable = { workspace = true }
|
||||
mime = { workspace = true }
|
||||
grass = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
28
crates/service/service-assets/css/fira.scss
Normal file
28
crates/service/service-assets/css/fira.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@import "fontawesome/fontawesome";
|
||||
@import "fontawesome/brands";
|
||||
@import "fontawesome/regular";
|
||||
@import "fontawesome/solid";
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Light.woff2") format("woff2");
|
||||
font-weight: light;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Medium.woff2") format("woff2");
|
||||
font-weight: medium;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
}
|
||||
28
crates/service/service-assets/css/fontawesome.scss
vendored
Normal file
28
crates/service/service-assets/css/fontawesome.scss
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
@import "fontawesome/fontawesome";
|
||||
@import "fontawesome/brands";
|
||||
@import "fontawesome/regular";
|
||||
@import "fontawesome/solid";
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Light.woff2") format("woff2");
|
||||
font-weight: light;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Medium.woff2") format("woff2");
|
||||
font-weight: medium;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
}
|
||||
55
crates/service/service-assets/src/assets/firacode.rs
Normal file
55
crates/service/service-assets/src/assets/firacode.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use servable::{ServableWithRoute, StaticAsset};
|
||||
|
||||
pub static FONT_FIRACODE_BOLD: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/fonts/FiraCode-Bold.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Bold.woff2"),
|
||||
mime: mime::FONT_WOFF2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_LIGHT: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/fonts/FiraCode-Light.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Light.woff2"),
|
||||
mime: mime::FONT_WOFF2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_MEDIUM: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/fonts/FiraCode-Medium.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Medium.woff2"),
|
||||
mime: mime::FONT_WOFF2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_REGULAR: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/fonts/FiraCode-Regular.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Regular.woff2"),
|
||||
mime: mime::FONT_WOFF2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_SEMIBOLD: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/fonts/FiraCode-SemiBold.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-SemiBold.woff2"),
|
||||
mime: mime::FONT_WOFF2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_VF: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/fonts/FiraCode-VF.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-VF.woff2"),
|
||||
mime: mime::FONT_WOFF2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
68
crates/service/service-assets/src/assets/fontawesome.rs
Normal file
68
crates/service/service-assets/src/assets/fontawesome.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use mime::Mime;
|
||||
use servable::{ServableWithRoute, StaticAsset};
|
||||
|
||||
pub static FONT_FA_BRANDS_WOFF2: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/fonts/fa/fa-brands-400.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-brands-400.woff2"),
|
||||
mime: mime::FONT_WOFF2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FA_REGULAR_WOFF2: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/fonts/fa/fa-regular-400.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-regular-400.woff2"),
|
||||
mime: mime::FONT_WOFF2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FA_SOLID_WOFF2: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/fonts/fa/fa-solid-900.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-solid-900.woff2"),
|
||||
mime: mime::FONT_WOFF2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
pub static FONT_FA_BRANDS_TTF: LazyLock<ServableWithRoute<StaticAsset>> = LazyLock::new(|| {
|
||||
ServableWithRoute::new(
|
||||
|| "/fonts/fa/fa-brands-400.ttf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-brands-400.ttf"),
|
||||
mime: Mime::from_str("font/ttf").unwrap(),
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
pub static FONT_FA_REGULAR_TTF: LazyLock<ServableWithRoute<StaticAsset>> = LazyLock::new(|| {
|
||||
ServableWithRoute::new(
|
||||
|| "/fonts/fa/fa-regular-400.ttf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-regular-400.ttf"),
|
||||
mime: Mime::from_str("font/ttf").unwrap(),
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
pub static FONT_FA_SOLID_TTF: LazyLock<ServableWithRoute<StaticAsset>> = LazyLock::new(|| {
|
||||
ServableWithRoute::new(
|
||||
|| "/fonts/fa/fa-solid-900.ttf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-solid-900.ttf"),
|
||||
mime: Mime::from_str("font/ttf").unwrap(),
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
)
|
||||
});
|
||||
44
crates/service/service-assets/src/assets/mod.rs
Normal file
44
crates/service/service-assets/src/assets/mod.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use servable::{CACHE_BUST_STR, ServableWithRoute, StaticAsset};
|
||||
|
||||
mod firacode;
|
||||
pub use firacode::*;
|
||||
|
||||
mod fontawesome;
|
||||
pub use fontawesome::*;
|
||||
|
||||
pub static HTMX: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/htmx-2.0.8.js".into(),
|
||||
servable::HTMX_2_0_8.with_ttl(None),
|
||||
);
|
||||
|
||||
pub static HTMX_JSON: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/htmx-json-1.19.12.js".into(),
|
||||
servable::EXT_JSON_1_19_12,
|
||||
);
|
||||
|
||||
pub static IMG_ICON: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/img/icon.png".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/icon.png"),
|
||||
mime: mime::IMAGE_PNG,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static CSS_FIRA: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| format!("/css/{}/fira.css", *CACHE_BUST_STR),
|
||||
StaticAsset {
|
||||
bytes: grass::include!("crates/service/service-assets/css/fira.scss").as_bytes(),
|
||||
mime: mime::TEXT_CSS,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static CSS_FONTAWESOME: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| format!("/css/{}/fontawesome.css", *CACHE_BUST_STR),
|
||||
StaticAsset {
|
||||
bytes: grass::include!("crates/service/service-assets/css/fontawesome.scss").as_bytes(),
|
||||
mime: mime::TEXT_CSS,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
20
crates/service/service-assets/src/components/mod.rs
Normal file
20
crates/service/service-assets/src/components/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
pub mod fa;
|
||||
pub mod misc;
|
||||
|
||||
pub const LAZY_IMAGE_JS: &str = "
|
||||
window.onload = function() {
|
||||
var imgs = document.querySelectorAll('.img-placeholder');
|
||||
|
||||
imgs.forEach(img => {
|
||||
img.style.border = 'none';
|
||||
img.style.filter = 'blur(10px)';
|
||||
img.style.transition = 'filter 0.3s';
|
||||
|
||||
var lg = new Image();
|
||||
lg.src = img.dataset.large;
|
||||
lg.onload = function () {
|
||||
img.src = img.dataset.large;
|
||||
img.style.filter = 'blur(0px)';
|
||||
};
|
||||
})
|
||||
}";
|
||||
71
crates/service/service-assets/src/lib.rs
Normal file
71
crates/service/service-assets/src/lib.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use axum::Router;
|
||||
use libservice::ToService;
|
||||
use servable::ServableRouter;
|
||||
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
||||
|
||||
pub mod assets;
|
||||
pub mod components;
|
||||
|
||||
pub struct AssetService {}
|
||||
|
||||
impl AssetService {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToService for AssetService {
|
||||
#[inline]
|
||||
fn make_router(&self) -> Option<Router<()>> {
|
||||
use assets::*;
|
||||
let router = ServableRouter::new()
|
||||
.add_page_with_route(&HTMX)
|
||||
.add_page_with_route(&HTMX_JSON)
|
||||
.add_page_with_route(&IMG_ICON)
|
||||
// fira
|
||||
.add_page_with_route(&CSS_FIRA)
|
||||
.add_page_with_route(&FONT_FIRACODE_BOLD)
|
||||
.add_page_with_route(&FONT_FIRACODE_LIGHT)
|
||||
.add_page_with_route(&FONT_FIRACODE_MEDIUM)
|
||||
.add_page_with_route(&FONT_FIRACODE_REGULAR)
|
||||
.add_page_with_route(&FONT_FIRACODE_SEMIBOLD)
|
||||
.add_page_with_route(&FONT_FIRACODE_VF)
|
||||
// fa
|
||||
.add_page_with_route(&CSS_FONTAWESOME)
|
||||
.add_page_with_route(&FONT_FA_BRANDS_WOFF2)
|
||||
.add_page_with_route(&FONT_FA_REGULAR_WOFF2)
|
||||
.add_page_with_route(&FONT_FA_SOLID_WOFF2)
|
||||
.add_page_with_route(&*FONT_FA_BRANDS_TTF)
|
||||
.add_page_with_route(&*FONT_FA_REGULAR_TTF)
|
||||
.add_page_with_route(&*FONT_FA_SOLID_TTF)
|
||||
.into_router();
|
||||
|
||||
let compression: CompressionLayer = CompressionLayer::new()
|
||||
.br(true)
|
||||
.deflate(true)
|
||||
.gzip(true)
|
||||
.zstd(true)
|
||||
.compress_when(DefaultPredicate::new());
|
||||
|
||||
Some(router.layer(compression))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn service_name(&self) -> Option<String> {
|
||||
Some("assets".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn server_builds_without_panic() {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let router = AssetService {}.make_router();
|
||||
assert!(router.is_some())
|
||||
});
|
||||
}
|
||||
28
crates/service/service-pile/Cargo.toml
Normal file
28
crates/service/service-pile/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "service-pile"
|
||||
version = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
libservice = { workspace = true }
|
||||
service-assets = { workspace = true }
|
||||
|
||||
pile-client = { workspace = true }
|
||||
|
||||
tracing = { workspace = true }
|
||||
grass = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
maud = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
servable = { workspace = true }
|
||||
url = { workspace = true }
|
||||
mime = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
183
crates/service/service-pile/css/main.scss
Normal file
183
crates/service/service-pile/css/main.scss
Normal file
@@ -0,0 +1,183 @@
|
||||
@import "text";
|
||||
|
||||
:root {
|
||||
// Misc colors
|
||||
--bgColor: #121212;
|
||||
--lightBgColor: #3a3f46;
|
||||
--fgColor: #ebebeb;
|
||||
--metaColor: #6199bb;
|
||||
--lightMetaColor: #638c86;
|
||||
--linkColor: #e4dab3;
|
||||
--codeBgColor: #292929;
|
||||
--codeFgColor: var(--fgColor);
|
||||
|
||||
// Main colors
|
||||
--grey: #696969;
|
||||
|
||||
// Accent colors, used only manally
|
||||
--green: #a2c579;
|
||||
--magenta: #ad79c5;
|
||||
--orange: #e86a33;
|
||||
--yellow: #e8bc00;
|
||||
--pink: #fa9f83;
|
||||
}
|
||||
|
||||
::selection,
|
||||
::-moz-selection {
|
||||
color: var(--bgColor);
|
||||
background: var(--metaColor);
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
font-size: 62.5%;
|
||||
scrollbar-color: var(--metaColor) var(--bgColor);
|
||||
scrollbar-width: auto;
|
||||
background: var(--bgColor);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Fira";
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.35;
|
||||
max-width: 64rem;
|
||||
margin: auto;
|
||||
overflow-wrap: break-word;
|
||||
background: var(--bgColor);
|
||||
color: var(--fgColor);
|
||||
}
|
||||
|
||||
div.wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.wrapper {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading spinner (three dots)
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
}
|
||||
.htmx-request .htmx-indicator,
|
||||
.htmx-request.htmx-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
@keyframes dot-bounce {
|
||||
0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
|
||||
40% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.search-meta {
|
||||
font-size: 1.2rem;
|
||||
color: var(--grey);
|
||||
margin: 0 0 1.5em 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#search-results {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border: 1px solid var(--lightBgColor);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.result-item-thumb {
|
||||
width: 64px;
|
||||
flex-shrink: 0;
|
||||
background: var(--lightBgColor);
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.result-item-info {
|
||||
padding: 0.5em 0.8em;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.15em;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-item-key {
|
||||
font-family: monospace;
|
||||
font-size: 1.3rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
color: var(--fgColor);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--metaColor);
|
||||
}
|
||||
}
|
||||
|
||||
.result-item-link {
|
||||
font-family: monospace;
|
||||
font-size: 1.1rem;
|
||||
color: var(--linkColor);
|
||||
text-decoration: none;
|
||||
align-self: flex-start;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.result-sentinel {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
#preview-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
border: 1px solid var(--lightBgColor);
|
||||
background: var(--bgColor);
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.6);
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 480px;
|
||||
max-height: 480px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
29
crates/service/service-pile/css/text.scss
Normal file
29
crates/service/service-pile/css/text.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
h1 {
|
||||
font-size: 3.5rem;
|
||||
margin-top: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-top: 1ex;
|
||||
margin-bottom: 0.5ex;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
border-radius: .3rem;
|
||||
padding: 0 .2ex 0 .2ex;
|
||||
color: var(--linkColor);
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: var(--linkColor);
|
||||
color: var(--bgColor);
|
||||
transition: 150ms;
|
||||
}
|
||||
37
crates/service/service-pile/src/lib.rs
Normal file
37
crates/service/service-pile/src/lib.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use axum::Router;
|
||||
use libservice::ToService;
|
||||
use pile_client::PileClient;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod pages;
|
||||
mod routes;
|
||||
|
||||
pub const PILE_PREFIX: &str = "/pile";
|
||||
pub const ASSET_PREFIX: &str = "/assets";
|
||||
|
||||
pub struct PileService {
|
||||
client: Arc<PileClient>,
|
||||
}
|
||||
|
||||
impl PileService {
|
||||
pub async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let endpoint = std::env::var("PILE_ENDPOINT")?;
|
||||
let api_key = std::env::var("PILE_API_KEY").ok();
|
||||
let client = PileClient::new(&endpoint, api_key.as_deref())?;
|
||||
Ok(Self {
|
||||
client: Arc::new(client),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToService for PileService {
|
||||
#[inline]
|
||||
fn make_router(&self) -> Option<Router<()>> {
|
||||
Some(routes::router(self.client.clone()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn service_name(&self) -> Option<String> {
|
||||
Some("pile".to_owned())
|
||||
}
|
||||
}
|
||||
110
crates/service/service-pile/src/pages/index.rs
Normal file
110
crates/service/service-pile/src/pages/index.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use maud::{Markup, html};
|
||||
use servable::{HtmlPage, PageMetadata, RenderContext};
|
||||
use service_assets::assets::{CSS_FIRA, CSS_FONTAWESOME, HTMX};
|
||||
use std::{pin::Pin, sync::LazyLock};
|
||||
|
||||
use crate::{ASSET_PREFIX, PILE_PREFIX, routes::CSS_PILE};
|
||||
|
||||
pub static INDEX: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
HtmlPage::default()
|
||||
.with_style_linked(CSS_PILE.route_at(PILE_PREFIX))
|
||||
.with_style_linked(CSS_FIRA.route_at(ASSET_PREFIX))
|
||||
.with_style_linked(CSS_FONTAWESOME.route_at(ASSET_PREFIX))
|
||||
.with_script_linked(HTMX.route_at(ASSET_PREFIX))
|
||||
.with_meta(PageMetadata {
|
||||
title: "Pile".into(),
|
||||
author: None,
|
||||
description: None,
|
||||
image: None,
|
||||
})
|
||||
.with_render(render)
|
||||
});
|
||||
|
||||
fn render<'a>(
|
||||
_page: &'a HtmlPage,
|
||||
_ctx: &'a RenderContext,
|
||||
) -> Pin<Box<dyn Future<Output = Markup> + Send + Sync + 'a>> {
|
||||
Box::pin(async {
|
||||
html! {
|
||||
div class="wrapper" style="margin-top:3ex;" {
|
||||
div {
|
||||
div style="
|
||||
text-align:center;
|
||||
padding-top:30px;
|
||||
padding-bottom:60px;
|
||||
" {
|
||||
h1 class="brand" {
|
||||
span class="fa fa-solid fa-book" aria-hidden="true" {}
|
||||
" Library search"
|
||||
}
|
||||
|
||||
div style="max-width:500px;margin:0 auto;padding:.4em 1em;" {
|
||||
form {
|
||||
input
|
||||
class="search-input"
|
||||
id="search"
|
||||
name="q"
|
||||
type="text"
|
||||
placeholder="Type to search..."
|
||||
style="
|
||||
-moz-box-sizing: border-box !important;
|
||||
box-sizing: border-box !important;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 1px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
padding: 10px 16px;
|
||||
font-size: 17px;
|
||||
width: 100%;
|
||||
box-shadow: 0 0 0 1px var(--color-border),0 0 0 1px var(--color-border);
|
||||
transition: box-shadow 150ms ease-in-out;
|
||||
"
|
||||
autofocus=""
|
||||
autocomplete="off"
|
||||
hx-get=(format!("{PILE_PREFIX}/search"))
|
||||
hx-trigger="load, keyup changed delay:100ms"
|
||||
hx-target="#search-results"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#search-spinner"
|
||||
{}
|
||||
}
|
||||
|
||||
div id="search-spinner" class="htmx-indicator dot-spinner" {
|
||||
span {}
|
||||
span {}
|
||||
span {}
|
||||
}
|
||||
|
||||
div id="search-results" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div id="preview-overlay" {
|
||||
img id="preview-overlay-img" src="" alt="" {}
|
||||
}
|
||||
|
||||
script { (maud::PreEscaped("
|
||||
function showPreview(el) {
|
||||
var ov = document.getElementById('preview-overlay');
|
||||
var img = document.getElementById('preview-overlay-img');
|
||||
img.src = el.dataset.preview;
|
||||
var rect = el.getBoundingClientRect();
|
||||
var size = 480;
|
||||
var left = rect.right + 12;
|
||||
if (left + size > window.innerWidth) { left = rect.left - size - 12; }
|
||||
ov.style.left = left + 'px';
|
||||
ov.style.top = Math.max(8, rect.top - size / 2 + rect.height / 2) + 'px';
|
||||
ov.style.display = 'block';
|
||||
}
|
||||
function hidePreview() {
|
||||
var ov = document.getElementById('preview-overlay');
|
||||
ov.style.display = 'none';
|
||||
document.getElementById('preview-overlay-img').src = '';
|
||||
}
|
||||
")) }
|
||||
}
|
||||
})
|
||||
}
|
||||
37
crates/service/service-pile/src/pages/mod.rs
Normal file
37
crates/service/service-pile/src/pages/mod.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use maud::html;
|
||||
use reqwest::StatusCode;
|
||||
use servable::{HtmlPage, PageMetadata};
|
||||
use service_assets::assets::{CSS_FIRA, CSS_FONTAWESOME};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
mod index;
|
||||
pub use index::INDEX;
|
||||
|
||||
use crate::{ASSET_PREFIX, PILE_PREFIX, routes::CSS_PILE};
|
||||
|
||||
pub static NOT_FOUND: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
HtmlPage::default()
|
||||
.with_style_linked(CSS_PILE.route_at(PILE_PREFIX))
|
||||
.with_style_linked(CSS_FIRA.route_at(ASSET_PREFIX))
|
||||
.with_style_linked(CSS_FONTAWESOME.route_at(ASSET_PREFIX))
|
||||
.with_meta(PageMetadata {
|
||||
title: "Page not found".into(),
|
||||
author: None,
|
||||
description: None,
|
||||
image: None,
|
||||
})
|
||||
.with_render(move |_page, _ctx| {
|
||||
Box::pin(async {
|
||||
html! {
|
||||
div class="wrapper" {
|
||||
div style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh" {
|
||||
p style="font-weight:bold;font-size:50pt;margin:0;" { "404" }
|
||||
p style="font-size:13pt;margin:0;color:var(--grey);" { "(page not found)" }
|
||||
a style="font-size:12pt;margin:10pt;padding:5px;" href="/" {"<- Back to site"}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.with_code(StatusCode::NOT_FOUND)
|
||||
});
|
||||
231
crates/service/service-pile/src/routes/mod.rs
Normal file
231
crates/service/service-pile/src/routes/mod.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use axum::Router;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::routing::get;
|
||||
use maud::{Markup, html};
|
||||
use pile_client::PileClient;
|
||||
use servable::{CACHE_BUST_STR, ServableRouter, ServableWithRoute, StaticAsset};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::PILE_PREFIX;
|
||||
use crate::pages;
|
||||
|
||||
const PAGE_SIZE: usize = 50;
|
||||
|
||||
pub(super) fn router(client: Arc<PileClient>) -> Router<()> {
|
||||
let compression: CompressionLayer = CompressionLayer::new()
|
||||
.br(true)
|
||||
.deflate(true)
|
||||
.gzip(true)
|
||||
.zstd(true)
|
||||
.compress_when(DefaultPredicate::new());
|
||||
|
||||
let search_router: Router<()> = Router::new()
|
||||
.route("/search", get(search_handler))
|
||||
.with_state(client.clone());
|
||||
|
||||
let api_router: Router<()> = client.dataset("books").proxy_router();
|
||||
|
||||
build_server()
|
||||
.into_router()
|
||||
.merge(search_router)
|
||||
.nest("/api", api_router)
|
||||
.layer(compression)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct SearchQuery {
|
||||
#[serde(default)]
|
||||
q: String,
|
||||
#[serde(default)]
|
||||
page: usize,
|
||||
}
|
||||
|
||||
async fn search_handler(
|
||||
State(client): State<Arc<PileClient>>,
|
||||
Query(params): Query<SearchQuery>,
|
||||
) -> Markup {
|
||||
let start = Instant::now();
|
||||
let query = params.q.trim().to_lowercase();
|
||||
let page = params.page;
|
||||
let mut query_invalid = false;
|
||||
let mut list_error = false;
|
||||
|
||||
let mut all_keys: Vec<(String, String)> = Vec::new();
|
||||
let mut filtered_total = 0;
|
||||
let mut grand_total = 0;
|
||||
|
||||
match query.is_empty() {
|
||||
true => {
|
||||
match client
|
||||
.dataset("books")
|
||||
.list_items(page * PAGE_SIZE, PAGE_SIZE)
|
||||
.await
|
||||
{
|
||||
Err(error) => {
|
||||
list_error = true;
|
||||
warn!(message = "error while listing items", ?error);
|
||||
}
|
||||
|
||||
Ok(resp) => {
|
||||
all_keys = resp
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| (item.source, item.key))
|
||||
.collect();
|
||||
filtered_total = resp.total;
|
||||
grand_total = resp.total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false => match client.dataset("books").lookup(&query, Some(512)).await {
|
||||
Err(_error) => {
|
||||
query_invalid = true;
|
||||
}
|
||||
|
||||
Ok(resp) => {
|
||||
let mut results = resp.results;
|
||||
results.sort_unstable_by(|a, b| f32::total_cmp(&b.score, &a.score));
|
||||
|
||||
filtered_total = results.len();
|
||||
grand_total = results.len();
|
||||
all_keys = results.into_iter().map(|r| (r.source, r.key)).collect();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// For empty query the server already paginated; for non-empty we slice locally.
|
||||
let page_items: Vec<&(String, String)> = if query.is_empty() {
|
||||
all_keys.iter().collect()
|
||||
} else {
|
||||
all_keys
|
||||
.iter()
|
||||
.skip(page * PAGE_SIZE)
|
||||
.take(PAGE_SIZE)
|
||||
.collect()
|
||||
};
|
||||
|
||||
let has_more = (page + 1) * PAGE_SIZE < filtered_total;
|
||||
let next_page = page + 1;
|
||||
|
||||
let encoded_q: String = url::form_urlencoded::byte_serialize(query.as_bytes()).collect();
|
||||
let next_url = format!("{PILE_PREFIX}/search?q={}&page={}", encoded_q, next_page);
|
||||
let elapsed_ms = start.elapsed().as_millis();
|
||||
|
||||
let mut msg = Vec::new();
|
||||
if query_invalid {
|
||||
msg.push("invalid query");
|
||||
}
|
||||
if list_error {
|
||||
msg.push("list error");
|
||||
}
|
||||
if filtered_total == 0 {
|
||||
msg.push("no results");
|
||||
}
|
||||
|
||||
if page == 0 {
|
||||
html! {
|
||||
div id="search-results" {
|
||||
p class="search-meta" {
|
||||
"Filtered " (filtered_total) "/" (grand_total) " items in " (elapsed_ms) "ms"
|
||||
@if !msg.is_empty() {
|
||||
span style="color:var(--orange)" {
|
||||
(format!("\u{00A0}\u{00A0}({})", msg.join(", ")))
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="result-grid" {
|
||||
@for (source, key) in &page_items {
|
||||
(result_item(source, key))
|
||||
}
|
||||
@if has_more {
|
||||
div
|
||||
class="result-sentinel"
|
||||
hx-get=(next_url)
|
||||
hx-trigger="revealed"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="this"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
@for (source, key) in &page_items {
|
||||
(result_item(source, key))
|
||||
}
|
||||
@if has_more {
|
||||
div
|
||||
class="result-sentinel"
|
||||
hx-get=(next_url)
|
||||
hx-trigger="revealed"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="this"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn result_item(source: &str, key: &str) -> Markup {
|
||||
let enc_source: String = url::form_urlencoded::byte_serialize(source.as_bytes()).collect();
|
||||
let enc_key: String = url::form_urlencoded::byte_serialize(key.as_bytes()).collect();
|
||||
let enc_path: String =
|
||||
url::form_urlencoded::byte_serialize("$.pdf.pages[0]".as_bytes()).collect();
|
||||
let thumb_url =
|
||||
format!("{PILE_PREFIX}/api/field?source={enc_source}&key={enc_key}&path={enc_path}");
|
||||
let item_url =
|
||||
format!("{PILE_PREFIX}/api/item?source={enc_source}&key={enc_key}&download=false");
|
||||
html! {
|
||||
div class="result-item" {
|
||||
div class="result-item-thumb"
|
||||
data-preview=(thumb_url)
|
||||
onmouseenter="showPreview(this)"
|
||||
onmouseleave="hidePreview()"
|
||||
{
|
||||
img src=(thumb_url) alt="" onerror="this.style.visibility='hidden'" {}
|
||||
}
|
||||
div class="result-item-info" {
|
||||
span class="result-item-key"
|
||||
data-key=(key)
|
||||
onclick="navigator.clipboard.writeText(this.dataset.key)"
|
||||
title="Click to copy"
|
||||
{ (key) }
|
||||
a class="result-item-link" href=(item_url) target="_blank" { "item" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static CSS_PILE: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| format!("/css/{}/main.css", *CACHE_BUST_STR),
|
||||
StaticAsset {
|
||||
bytes: grass::include!("crates/service/service-pile/css/main.scss").as_bytes(),
|
||||
mime: mime::TEXT_CSS,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
fn build_server() -> ServableRouter {
|
||||
ServableRouter::new()
|
||||
.with_404(&pages::NOT_FOUND)
|
||||
.add_page("/", &pages::INDEX)
|
||||
//
|
||||
.add_page_with_route(&CSS_PILE)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn server_builds_without_panic() {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let _server = build_server();
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
libservice = { workspace = true }
|
||||
service-assets = { workspace = true }
|
||||
|
||||
md-footnote = { workspace = true }
|
||||
|
||||
@@ -18,7 +19,6 @@ axum = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
maud = { workspace = true }
|
||||
emojis = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
@@ -28,3 +28,4 @@ reqwest = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
servable = { workspace = true }
|
||||
mime = { workspace = true }
|
||||
|
||||
@@ -3,36 +3,6 @@
|
||||
@import "images";
|
||||
@import "special";
|
||||
|
||||
@import "fontawesome/fontawesome";
|
||||
@import "fontawesome/brands";
|
||||
@import "fontawesome/regular";
|
||||
@import "fontawesome/solid";
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Light.woff2") format("woff2");
|
||||
font-weight: light;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Medium.woff2") format("woff2");
|
||||
font-weight: medium;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
// Misc colors
|
||||
--bgColor: #121212;
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use markdown_it::parser::inline::{InlineRule, InlineState};
|
||||
use markdown_it::{Node, NodeValue, Renderer};
|
||||
use maud::Render;
|
||||
|
||||
use crate::components::fa::FAIcon;
|
||||
use service_assets::components::fa::FAIcon;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InlineEmote(String);
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
pub mod fa;
|
||||
pub mod mangle;
|
||||
pub mod md;
|
||||
pub mod misc;
|
||||
|
||||
@@ -31,4 +31,4 @@ A snippet of the [_Endless Sky_][es] map is below.
|
||||
|
||||
<br/>
|
||||
|
||||
<img class="img-placeholder" src="/assets/img/betalupi.png?t=maxdim(50,50)" data-large="/assets/img/betalupi.png" style="width:100%;height=10rem;"></img>
|
||||
<img class="img-placeholder" src="/static/img/betalupi.png?t=maxdim(50,50)" data-large="/static/img/betalupi.png" style="width:100%;height=10rem;"></img>
|
||||
|
||||
@@ -9,15 +9,16 @@ use maud::{Markup, PreEscaped, html};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use servable::{DeviceType, HtmlPage, RenderContext};
|
||||
use service_assets::{
|
||||
assets::{CSS_FIRA, CSS_FONTAWESOME, IMG_ICON},
|
||||
components::misc::FarLink,
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{
|
||||
components::{
|
||||
md::{Markdown, meta_from_markdown},
|
||||
misc::FarLink,
|
||||
},
|
||||
components::md::{Markdown, meta_from_markdown},
|
||||
pages::{LAZY_IMAGE_JS, backlinks, footer},
|
||||
routes::{IMG_ICON, MAIN_CSS},
|
||||
routes::MAIN_CSS,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -189,13 +190,15 @@ pub static HANDOUTS: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
let mut meta = meta_from_markdown(&md).unwrap().unwrap();
|
||||
|
||||
if meta.image.is_none() {
|
||||
meta.image = Some(IMG_ICON.route().into());
|
||||
meta.image = Some(IMG_ICON.route_at("/assets"));
|
||||
}
|
||||
|
||||
let html = PreEscaped(md.render());
|
||||
|
||||
HtmlPage::default()
|
||||
.with_style_linked(MAIN_CSS.route())
|
||||
.with_style_linked(CSS_FIRA.route_at("/assets"))
|
||||
.with_style_linked(CSS_FONTAWESOME.route_at("/assets"))
|
||||
.with_script_inline(LAZY_IMAGE_JS)
|
||||
.with_meta(meta)
|
||||
.with_render(move |page, ctx| {
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
use maud::{Markup, html};
|
||||
use servable::{HtmlPage, PageMetadata, RenderContext};
|
||||
use service_assets::{
|
||||
assets::{CSS_FIRA, CSS_FONTAWESOME, IMG_ICON},
|
||||
components::{fa::FAIcon, misc::FarLink},
|
||||
};
|
||||
use std::{pin::Pin, sync::LazyLock};
|
||||
|
||||
use crate::{
|
||||
components::{
|
||||
fa::FAIcon,
|
||||
mangle::{MangledBetaEmail, MangledGoogleEmail},
|
||||
md::Markdown,
|
||||
misc::FarLink,
|
||||
},
|
||||
pages::{LAZY_IMAGE_JS, footer},
|
||||
routes::{IMG_ICON, MAIN_CSS},
|
||||
routes::{IMG_COVER_SMALL, MAIN_CSS},
|
||||
};
|
||||
|
||||
pub static INDEX: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
HtmlPage::default()
|
||||
.with_style_linked(MAIN_CSS.route())
|
||||
.with_style_linked(CSS_FIRA.route_at("/assets"))
|
||||
.with_style_linked(CSS_FONTAWESOME.route_at("/assets"))
|
||||
.with_script_inline(LAZY_IMAGE_JS)
|
||||
.with_meta(PageMetadata {
|
||||
title: "Betalupi: About".into(),
|
||||
author: Some("Mark".into()),
|
||||
description: None,
|
||||
image: Some(IMG_ICON.route().into()),
|
||||
image: Some(IMG_ICON.route_at("/assets")),
|
||||
})
|
||||
.with_render(render)
|
||||
});
|
||||
@@ -38,8 +42,8 @@ fn render<'a>(
|
||||
|
||||
img
|
||||
class="img-placeholder"
|
||||
src="/assets/img/cover-small.jpg?t=maxdim(20,20)"
|
||||
data-large="/assets/img/cover-small.jpg"
|
||||
src=(format!("{}?t=maxdim(20,20)", IMG_COVER_SMALL.route()))
|
||||
data-large=(IMG_COVER_SMALL.route())
|
||||
style="image-rendering:pixelated;float:left;margin:10px;display:block;width:25%;"
|
||||
{}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ This is a heavily opinionated bookmarks toolbar.
|
||||
- [tokei](https://github.com/XAMPPRocky/tokei): count lines of code
|
||||
- [delta](https://github.com/dandavison/delta): pretty pager for diffs
|
||||
- [dust](https://github.com/dandavison/delta): `du`, but better
|
||||
- [bandwhich](https://github.com/imsnif/bandwhich): network stats
|
||||
|
||||
<br></br>
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@ use chrono::TimeDelta;
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
use reqwest::StatusCode;
|
||||
use servable::{HtmlPage, PageMetadata, RenderContext};
|
||||
use service_assets::{
|
||||
assets::{CSS_FIRA, CSS_FONTAWESOME, IMG_ICON},
|
||||
components::{fa::FAIcon, misc::FarLink},
|
||||
};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::{
|
||||
components::{
|
||||
fa::FAIcon,
|
||||
md::{Markdown, meta_from_markdown},
|
||||
misc::FarLink,
|
||||
},
|
||||
routes::{IMG_ICON, MAIN_CSS},
|
||||
components::md::{Markdown, meta_from_markdown},
|
||||
routes::MAIN_CSS,
|
||||
};
|
||||
|
||||
mod handouts;
|
||||
@@ -43,6 +43,8 @@ fn page_from_markdown(md: impl Into<String>, default_image: Option<String>) -> H
|
||||
HtmlPage::default()
|
||||
.with_script_inline(LAZY_IMAGE_JS)
|
||||
.with_style_linked(MAIN_CSS.route())
|
||||
.with_style_linked(CSS_FIRA.route_at("/assets"))
|
||||
.with_style_linked(CSS_FONTAWESOME.route_at("/assets"))
|
||||
.with_meta(meta)
|
||||
.with_render(move |_page, ctx| {
|
||||
let html = html.clone();
|
||||
@@ -162,7 +164,7 @@ pub fn footer() -> Markup {
|
||||
// MARK: pages
|
||||
//
|
||||
|
||||
pub const LINKS: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
pub static LINKS: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
/*
|
||||
Dead links:
|
||||
|
||||
@@ -170,28 +172,33 @@ pub const LINKS: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
http://www.3dprintmath.com/
|
||||
*/
|
||||
|
||||
page_from_markdown(include_str!("links.md"), Some(IMG_ICON.route().into()))
|
||||
page_from_markdown(include_str!("links.md"), Some(IMG_ICON.route_at("/assets")))
|
||||
});
|
||||
|
||||
pub const BETALUPI: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
page_from_markdown(include_str!("betalupi.md"), Some(IMG_ICON.route().into()))
|
||||
pub static BETALUPI: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
page_from_markdown(
|
||||
include_str!("betalupi.md"),
|
||||
Some(IMG_ICON.route_at("/assets")),
|
||||
)
|
||||
});
|
||||
|
||||
pub const HTWAH_TYPESETTING: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
pub static HTWAH_TYPESETTING: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
page_from_markdown(
|
||||
include_str!("htwah-typesetting.md"),
|
||||
Some(IMG_ICON.route().into()),
|
||||
Some(IMG_ICON.route_at("/assets")),
|
||||
)
|
||||
});
|
||||
|
||||
pub static NOT_FOUND: LazyLock<HtmlPage> = LazyLock::new(|| {
|
||||
HtmlPage::default()
|
||||
.with_style_linked(MAIN_CSS.route())
|
||||
.with_style_linked(CSS_FIRA.route_at("/assets"))
|
||||
.with_style_linked(CSS_FONTAWESOME.route_at("/assets"))
|
||||
.with_meta(PageMetadata {
|
||||
title: "Page not found".into(),
|
||||
author: None,
|
||||
description: None,
|
||||
image: Some(IMG_ICON.route().into()),
|
||||
image: Some(IMG_ICON.route_at("/assets")),
|
||||
})
|
||||
.with_render(
|
||||
move |_page, _ctx| {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use axum::Router;
|
||||
use servable::{
|
||||
CACHE_BUST_STR, Redirect, ServableRouter, ServableWithRoute, StaticAsset, mime::MimeType,
|
||||
};
|
||||
use servable::{CACHE_BUST_STR, Redirect, ServableRouter, ServableWithRoute, StaticAsset};
|
||||
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
||||
|
||||
use crate::pages;
|
||||
@@ -17,163 +15,29 @@ pub(super) fn router() -> Router<()> {
|
||||
build_server().into_router().layer(compression)
|
||||
}
|
||||
|
||||
pub static HTMX: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/htmx-2.0.8.js".into(),
|
||||
servable::HTMX_2_0_8.with_ttl(None),
|
||||
);
|
||||
|
||||
pub static HTMX_JSON: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/htmx-json-1.19.12.js".into(),
|
||||
servable::EXT_JSON_1_19_12,
|
||||
);
|
||||
|
||||
pub static MAIN_CSS: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| format!("/assets/{}/css/main.css", *CACHE_BUST_STR),
|
||||
|| format!("/static/{}/css/main.css", *CACHE_BUST_STR),
|
||||
StaticAsset {
|
||||
bytes: grass::include!("css/main.scss").as_bytes(),
|
||||
mime: MimeType::Css,
|
||||
bytes: grass::include!("crates/service/service-webpage/css/main.scss").as_bytes(),
|
||||
mime: mime::TEXT_CSS,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static IMG_COVER_SMALL: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/img/cover-small.jpg".into(),
|
||||
|| "/static/img/cover-small.jpg".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/images/cover-small.jpg"),
|
||||
mime: MimeType::Jpg,
|
||||
mime: mime::IMAGE_JPEG,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static IMG_BETALUPI: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/img/betalupi.png".into(),
|
||||
|| "/static/img/betalupi.png".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/images/betalupi-map.png"),
|
||||
mime: MimeType::Png,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static IMG_ICON: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/img/icon.png".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/images/icon.png"),
|
||||
mime: MimeType::Png,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
//
|
||||
// MARK: fonts
|
||||
//
|
||||
|
||||
pub static FONT_FIRACODE_BOLD: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/FiraCode-Bold.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Bold.woff2"),
|
||||
mime: MimeType::Woff2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_LIGHT: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/FiraCode-Light.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Light.woff2"),
|
||||
mime: MimeType::Woff2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_MEDIUM: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/FiraCode-Medium.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Medium.woff2"),
|
||||
mime: MimeType::Woff2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_REGULAR: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/FiraCode-Regular.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Regular.woff2"),
|
||||
mime: MimeType::Woff2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_SEMIBOLD: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/FiraCode-SemiBold.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-SemiBold.woff2"),
|
||||
mime: MimeType::Woff2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FIRACODE_VF: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/FiraCode-VF.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-VF.woff2"),
|
||||
mime: MimeType::Woff2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
//
|
||||
// MARK: icons
|
||||
//
|
||||
pub static FONT_FA_BRANDS_WOFF2: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/fa/fa-brands-400.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-brands-400.woff2"),
|
||||
mime: MimeType::Woff2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FA_REGULAR_WOFF2: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/fa/fa-regular-400.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-regular-400.woff2"),
|
||||
mime: MimeType::Woff2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FA_SOLID_WOFF2: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/fa/fa-solid-900.woff2".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-solid-900.woff2"),
|
||||
mime: MimeType::Woff2,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FA_BRANDS_TTF: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/fa/fa-brands-400.ttf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-brands-400.ttf"),
|
||||
mime: MimeType::Ttf,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FA_REGULAR_TTF: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/fa/fa-regular-400.ttf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-regular-400.ttf"),
|
||||
mime: MimeType::Ttf,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static FONT_FA_SOLID_TTF: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/fonts/fa/fa-solid-900.ttf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/fonts/fa/fa-solid-900.ttf"),
|
||||
mime: MimeType::Ttf,
|
||||
mime: mime::IMAGE_PNG,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
@@ -182,55 +46,55 @@ pub static FONT_FA_SOLID_TTF: ServableWithRoute<StaticAsset> = ServableWithRoute
|
||||
// MARK: htwah
|
||||
//
|
||||
pub static HTWAH_DEFINITIONS: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/htwah/definitions.pdf".into(),
|
||||
|| "/static/htwah/definitions.pdf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/htwah/definitions.pdf"),
|
||||
mime: MimeType::Pdf,
|
||||
mime: mime::APPLICATION_PDF,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static HTWAH_NUMBERING: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/htwah/numbering.pdf".into(),
|
||||
|| "/static/htwah/numbering.pdf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/htwah/numbering.pdf"),
|
||||
mime: MimeType::Pdf,
|
||||
mime: mime::APPLICATION_PDF,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static HTWAH_SOLS_A: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/htwah/sols-a.pdf".into(),
|
||||
|| "/static/htwah/sols-a.pdf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/htwah/sols-a.pdf"),
|
||||
mime: MimeType::Pdf,
|
||||
mime: mime::APPLICATION_PDF,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static HTWAH_SOLS_B: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/htwah/sols-b.pdf".into(),
|
||||
|| "/static/htwah/sols-b.pdf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/htwah/sols-b.pdf"),
|
||||
mime: MimeType::Pdf,
|
||||
mime: mime::APPLICATION_PDF,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static HTWAH_SPACING_A: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/htwah/spacing-a.pdf".into(),
|
||||
|| "/static/htwah/spacing-a.pdf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/htwah/spacing-a.pdf"),
|
||||
mime: MimeType::Pdf,
|
||||
mime: mime::APPLICATION_PDF,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
|
||||
pub static HTWAH_SPACING_B: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||
|| "/assets/htwah/spacing-b.pdf".into(),
|
||||
|| "/static/htwah/spacing-b.pdf".into(),
|
||||
StaticAsset {
|
||||
bytes: include_bytes!("../../assets/htwah/spacing-b.pdf"),
|
||||
mime: MimeType::Pdf,
|
||||
mime: mime::APPLICATION_PDF,
|
||||
ttl: StaticAsset::DEFAULT_TTL,
|
||||
},
|
||||
);
|
||||
@@ -239,39 +103,18 @@ fn build_server() -> ServableRouter {
|
||||
ServableRouter::new()
|
||||
.with_404(&pages::NOT_FOUND)
|
||||
.add_page("/", &pages::INDEX)
|
||||
.add_page("/links", pages::LINKS)
|
||||
.add_page("/whats-a-betalupi", pages::BETALUPI)
|
||||
.add_page("/links", &pages::LINKS)
|
||||
.add_page("/whats-a-betalupi", &pages::BETALUPI)
|
||||
.add_page("/handouts", &pages::HANDOUTS)
|
||||
.add_page("/htwah", {
|
||||
#[expect(clippy::unwrap_used)]
|
||||
Redirect::new("/handouts").unwrap()
|
||||
})
|
||||
.add_page("/htwah/typesetting", pages::HTWAH_TYPESETTING)
|
||||
.add_page_with_route(&HTMX)
|
||||
.add_page_with_route(&HTMX_JSON)
|
||||
.add_page("/htwah/typesetting", &pages::HTWAH_TYPESETTING)
|
||||
//
|
||||
.add_page_with_route(&MAIN_CSS)
|
||||
.add_page_with_route(&IMG_COVER_SMALL)
|
||||
.add_page_with_route(&IMG_BETALUPI)
|
||||
.add_page_with_route(&IMG_ICON)
|
||||
//
|
||||
// MARK: fonts
|
||||
//
|
||||
.add_page_with_route(&FONT_FIRACODE_BOLD)
|
||||
.add_page_with_route(&FONT_FIRACODE_LIGHT)
|
||||
.add_page_with_route(&FONT_FIRACODE_MEDIUM)
|
||||
.add_page_with_route(&FONT_FIRACODE_REGULAR)
|
||||
.add_page_with_route(&FONT_FIRACODE_SEMIBOLD)
|
||||
.add_page_with_route(&FONT_FIRACODE_VF)
|
||||
//
|
||||
// MARK: icons
|
||||
//
|
||||
.add_page_with_route(&FONT_FA_BRANDS_WOFF2)
|
||||
.add_page_with_route(&FONT_FA_REGULAR_WOFF2)
|
||||
.add_page_with_route(&FONT_FA_SOLID_WOFF2)
|
||||
.add_page_with_route(&FONT_FA_BRANDS_TTF)
|
||||
.add_page_with_route(&FONT_FA_REGULAR_TTF)
|
||||
.add_page_with_route(&FONT_FA_SOLID_TTF)
|
||||
//
|
||||
// MARK: htwah
|
||||
//
|
||||
@@ -284,6 +127,7 @@ fn build_server() -> ServableRouter {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn server_builds_without_panic() {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
|
||||
Reference in New Issue
Block a user