Compare commits

5 Commits
main ... pile

Author SHA1 Message Date
8bcd6ada5d TMP pile 2026-03-23 22:35:15 -07:00
7cb1dfa2d1 merge 2026-03-23 10:18:55 -07:00
67e63019c5 Refactor, create service-assets 2026-03-23 10:00:16 -07:00
368034a177 Update deps 2026-03-21 10:54:22 -07:00
cdc2e40803 Add link 2026-03-21 10:54:03 -07:00
69 changed files with 1883 additions and 997 deletions

1472
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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 }

View File

@@ -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())
}

View File

@@ -160,6 +160,7 @@ impl BlockRule for FootnoteDefinitionScanner {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;

View File

@@ -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,

View 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 }

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View 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;
}

View 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;
}

View 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,
},
);

View 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,
},
)
});

View 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,
},
);

View 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)';
};
})
}";

View 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())
});
}

View 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 }

View 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;
}
}

View 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;
}

View 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())
}
}

View 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 = '';
}
")) }
}
})
}

View 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)
});

View 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();
});
}

View File

@@ -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 }

View File

@@ -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;

View File

@@ -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);

View File

@@ -1,4 +1,2 @@
pub mod fa;
pub mod mangle;
pub mod md;
pub mod misc;

View File

@@ -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>

View File

@@ -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| {

View File

@@ -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%;"
{}

View File

@@ -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>

View File

@@ -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| {

View File

@@ -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()