diff --git a/Cargo.lock b/Cargo.lock index f901873..4a71c1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,19 @@ checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" name = "assetserver" version = "0.0.1" +[[package]] +name = "async-compression" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -349,6 +362,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + [[package]] name = "const_format" version = "0.2.35" @@ -375,6 +405,35 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna 1.1.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -474,6 +533,15 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -550,12 +618,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -572,6 +634,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -580,6 +643,29 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -593,9 +679,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -660,6 +751,25 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -675,11 +785,6 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] [[package]] name = "heck" @@ -752,6 +857,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1063,6 +1169,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1088,15 +1200,6 @@ dependencies = [ "prost-types", ] -[[package]] -name = "lru" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" -dependencies = [ - "hashbrown 0.16.0", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -1553,6 +1656,12 @@ dependencies = [ "prost", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "psm" version = "0.1.28" @@ -1563,6 +1672,16 @@ dependencies = [ "cc", ] +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna 1.1.0", + "psl-types", +] + [[package]] name = "quick-xml" version = "0.38.3" @@ -1756,9 +1875,16 @@ version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ + "async-compression", "base64", "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-channel", "futures-core", + "futures-util", + "h2", "http", "http-body", "http-body-util", @@ -1767,6 +1893,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", "quinn", @@ -1778,12 +1905,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", ] @@ -2008,15 +2137,16 @@ dependencies = [ "emojis", "lazy_static", "libservice", - "lru", "macro-assets", "macro-sass", "markdown-it", "maud", "parking_lot", + "reqwest", "serde", "serde_yaml", "strum", + "tokio", "tracing", ] @@ -2350,6 +2480,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toolbox" version = "0.0.1" @@ -2755,6 +2898,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.82" diff --git a/Cargo.toml b/Cargo.toml index 02a261e..f0f13db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ service-webpage = { path = "crates/service/service-webpage" } # -# MARK: Servers +# MARK: Server # axum = { version = "0.8.6", features = ["macros", "multipart"] } tower-http = { version = "0.6.6", features = ["trace"] } @@ -88,6 +88,17 @@ maud = { version = "0.27.0", features = ["axum"] } grass = "0.13.4" markdown-it = "0.6.1" emojis = "0.8.0" +reqwest = { version = "0.12.24", default-features = false, features = [ + "http2", + "rustls-tls", + "cookies", + "gzip", + "stream", + "json", + "charset", + "blocking", +] } + # # MARK: Async & Parallelism diff --git a/crates/lib/toolbox/src/logging.rs b/crates/lib/toolbox/src/logging.rs index a600ecd..3429441 100644 --- a/crates/lib/toolbox/src/logging.rs +++ b/crates/lib/toolbox/src/logging.rs @@ -68,6 +68,8 @@ impl From for EnvFilter { format!("h2={}", conf.silence), format!("rustls={}", conf.silence), format!("tower={}", conf.silence), + format!("reqwest={}", conf.silence), + format!("axum={}", conf.silence), // // Libs // diff --git a/crates/service/service-webpage/Cargo.toml b/crates/service/service-webpage/Cargo.toml index aec794e..7a0bd17 100644 --- a/crates/service/service-webpage/Cargo.toml +++ b/crates/service/service-webpage/Cargo.toml @@ -21,7 +21,8 @@ emojis = { workspace = true } strum = { workspace = true } chrono = { workspace = true } parking_lot = { workspace = true } -lru = { workspace = true } lazy_static = { workspace = true } serde_yaml = { workspace = true } serde = { workspace = true } +reqwest = { workspace = true } +tokio = { workspace = true } diff --git a/crates/service/service-webpage/css/special.scss b/crates/service/service-webpage/css/special.scss index 4e99c5b..b45d0a7 100644 --- a/crates/service/service-webpage/css/special.scss +++ b/crates/service/service-webpage/css/special.scss @@ -1,6 +1,3 @@ -// Handout list pages -// works with "{{ handout() }}" shortcode. - .handout-li-links { color: var(--grey); } @@ -39,10 +36,6 @@ display: none; } -.handout-star { - color: var(--yellow); -} - // Email obfuscation // Works with "{{ email_*() }}" shortcodes. .eobf { diff --git a/crates/service/service-webpage/src/page/mod.rs b/crates/service/service-webpage/src/page/mod.rs index 0a42498..4a1dc6d 100644 --- a/crates/service/service-webpage/src/page/mod.rs +++ b/crates/service/service-webpage/src/page/mod.rs @@ -9,14 +9,19 @@ use axum::{ response::{IntoResponse, Response}, routing::get, }; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, TimeDelta, Utc}; use libservice::ServiceConnectInfo; -use lru::LruCache; +use markdown_it::Node; use maud::{Markup, PreEscaped, Render, html}; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; use serde::Deserialize; -use std::{collections::HashMap, num::NonZero, sync::Arc, time::Duration}; -use tracing::{debug, trace}; +use std::{ + collections::HashMap, + pin::Pin, + sync::Arc, + time::{Duration, Instant}, +}; +use tracing::{trace, warn}; use crate::components::{ md::{FrontMatter, Markdown}, @@ -70,6 +75,23 @@ impl Render for PageMetadata { } } +impl PageMetadata { + /// Try to read page metadata from a markdown file's frontmatter. + /// - returns `none` if there is no frontmatter + /// - returns an error if we fail to parse frontmatter + pub fn from_markdown_frontmatter( + root_node: &Node, + ) -> Result, serde_yaml::Error> { + root_node + .children + .get(0) + .map(|x| x.cast::()) + .flatten() + .map(|x| serde_yaml::from_str::(&x.content)) + .map_or(Ok(None), |v| v.map(Some)) + } +} + // // MARK: page // @@ -79,9 +101,10 @@ pub struct Page { pub meta: PageMetadata, /// How long this page's html may be cached. + /// This controls the maximum age of a page shown to the user. /// /// If `None`, this page is always rendered from scratch. - pub html_ttl: Option, + pub html_ttl: Option, /// A function that generates this page's html. /// @@ -89,41 +112,40 @@ pub struct Page { /// or the contents of a wrapper element (defined in the page server struct). /// /// This closure must never return `` or ``. - pub generate_html: Box Markup>, + pub generate_html: Box< + dyn Send + + Sync + + for<'a> Fn(&'a Page) -> Pin + 'a + Send + Sync>>, + >, } impl Default for Page { fn default() -> Self { Page { meta: Default::default(), - html_ttl: Some(Duration::from_secs(60 * 24 * 30)), + html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)), //css_ttl: Duration::from_secs(60 * 24 * 30), //generate_css: None, - generate_html: Box::new(|_| html!()), + generate_html: Box::new(|_| Box::pin(async { html!() })), } } } impl Page { - pub fn generate_html(&self) -> Markup { - (self.generate_html)(self) + pub async fn generate_html(&self) -> Markup { + (self.generate_html)(self).await } pub fn from_markdown(md: impl Into, default_image: Option) -> Self { let md: String = md.into(); let md = Markdown::parse(&md); - let mut meta = md - .children - .get(0) - .map(|x| x.cast::()) - .flatten() - .map(|x| serde_yaml::from_str::(&x.content)) - .unwrap_or(Ok(Default::default())) - .unwrap_or(PageMetadata { + let mut meta = PageMetadata::from_markdown_frontmatter(&md) + .unwrap_or(Some(PageMetadata { title: "Invalid frontmatter!".into(), ..Default::default() - }); + })) + .unwrap_or(Default::default()); if meta.image.is_none() { meta.image = default_image @@ -134,13 +156,16 @@ impl Page { Page { meta, generate_html: Box::new(move |page| { - html! { - @if let Some(slug) = &page.meta.slug { - (Backlinks(&[("/", "home")], slug)) - } + let html = html.clone(); + Box::pin(async move { + html! { + @if let Some(slug) = &page.meta.slug { + (Backlinks(&[("/", "home")], slug)) + } - (html) - } + (html) + } + }) }), ..Default::default() @@ -153,35 +178,50 @@ impl Page { // pub struct PageServer { + /// If true, expired pages will be rerendered before being sent to the user. + /// If false, requests never trigger rerenders. We rely on the rerender task. + /// + /// If true, we deliver fresher pages but delay responses. + /// TODO: replace this with a smarter rendering strategy? + never_rerender_on_request: bool, + /// Map of `{ route: page }` - pages: HashMap, + pages: Arc>>>, /// Map of `{ route: (page data, expire time) }` /// /// We use an LruCache for bounded memory usage. - html_cache: Mutex)>>, + html_cache: RwLock)>>, /// Called whenever we need to render a page. /// - this method should call `page.generate_html()`, /// - wrap the result in ``, /// - and add `` /// ``` - render_page: Box Markup>, + render_page: Box< + dyn Send + + Sync + + for<'a> Fn(&'a Page) -> Pin + 'a + Send + Sync>>, + >, } impl PageServer { - pub fn new(page_wrapper: Box Markup>) -> Self { - #[expect(clippy::unwrap_used)] - let cache_size = LruCache::new(NonZero::new(128).unwrap()); - - Self { - pages: HashMap::new(), - html_cache: Mutex::new(cache_size), - render_page: Box::new(page_wrapper), - } + pub fn new( + render_page: Box< + dyn Send + + Sync + + for<'a> Fn(&'a Page) -> Pin + 'a + Send + Sync>>, + >, + ) -> Arc { + Arc::new(Self { + pages: Arc::new(Mutex::new(HashMap::new())), + html_cache: RwLock::new(HashMap::new()), + render_page, + never_rerender_on_request: true, + }) } - pub fn add_page(mut self, route: impl Into, page: Page) -> Self { + pub fn add_page(&self, route: impl Into, page: Page) -> &Self { #[expect(clippy::expect_used)] let route = route .into() @@ -189,24 +229,79 @@ impl PageServer { .expect("page route must start with /") .to_owned(); - self.pages.insert(route, page); - + self.pages.lock().insert(route, Arc::new(page)); self } + /// Re-render the page at `route`, regardless of cache state. + /// Does nothing if there is no page at `route`. + /// + /// Returns the rendered page's content. + async fn render_page(&self, reason: &'static str, route: &str) -> Option { + let now = Utc::now(); + let start = Instant::now(); + trace!(message = "Rendering page", route, reason); + + let page = match self.pages.lock().get(route) { + Some(x) => x.clone(), + None => { + warn!(message = "Not rerendering, no such route", route, reason); + return None; + } + }; + + let html = (self.render_page)(&*page).await.0; + + if let Some(ttl) = page.html_ttl { + self.html_cache + .write() + .insert(route.to_owned(), (html.clone(), now + ttl)); + } + + let elapsed = start.elapsed().as_millis(); + trace!(message = "Rendered page", route, reason, time_ms = elapsed); + return Some(html); + } + + // Rerender considerations: + // - rerendering often in the background is wasteful. Maybe we should fall asleep? + // - rerendering on request is slow + // - rerendering in the background after a request could be a good idea. Maybe implement? + // + // - cached pages only make sense for static assets. + // - user pages can't be pre-rendered! + pub async fn start_rerender_task(self: Arc, interval: Duration) { + loop { + tokio::time::sleep(interval).await; + + let now = Utc::now(); + let pages = self + .pages + .lock() + .iter() + .filter(|(_, v)| v.html_ttl.is_some()) + .map(|(k, _)| k.clone()) + .collect::>(); + + for route in pages { + let needs_render = match self.html_cache.read().get(&route) { + Some(x) => x.1 < now, // Expired + None => true, // Never rendered + }; + + if needs_render { + self.render_page("rerender_task", &route).await; + } + } + } + } + async fn handler( - Path(path): Path, + Path(route): Path, State(state): State>, ConnectInfo(addr): ConnectInfo, ) -> Response { - trace!("Serving {path} to {}", addr.addr); - - let page = match state.pages.get(&path) { - Some(x) => x, - - // TODO: 404 page - None => return (StatusCode::NOT_FOUND, "page doesn't exist").into_response(), - }; + trace!("Serving {route} to {}", addr.addr); let now = Utc::now(); let headers = [( @@ -214,30 +309,28 @@ impl PageServer { HeaderValue::from_static("text/html; charset=utf-8"), )]; - if let Some((html, expires)) = state.html_cache.lock().get(&path) - && *expires > now + if let Some((html, expires)) = state.html_cache.read().get(&route) + && (*expires > now || state.never_rerender_on_request) { // TODO: no clone? return (headers, html.clone()).into_response(); }; - debug!("Rendering {path}"); - let html = (state.render_page)(page).0; + let html = match state.render_page("request", &route).await { + Some(x) => x.clone(), + None => return (StatusCode::NOT_FOUND, "page doesn't exist").into_response(), + }; - if let Some(ttl) = page.html_ttl { - state.html_cache.lock().put(path, (html.clone(), now + ttl)); - } - - return (headers, html.clone()).into_response(); + return (headers, html).into_response(); } - pub fn into_router(self) -> Router<()> { + pub fn into_router(self: Arc) -> Router<()> { Router::new() .route( "/", get(|state, conn| async { Self::handler(Path(String::new()), state, conn).await }), ) .route("/{*path}", get(Self::handler)) - .with_state(Arc::new(self)) + .with_state(self) } } diff --git a/crates/service/service-webpage/src/pages/handouts.md b/crates/service/service-webpage/src/pages/handouts.md index ea77a65..a6387ea 100644 --- a/crates/service/service-webpage/src/pages/handouts.md +++ b/crates/service/service-webpage/src/pages/handouts.md @@ -36,197 +36,3 @@ If the class finishes early, the lesson is either too short or too easy.




- -## Warm-Ups - -Students never show up on time. Some come early, some come late. Warm-ups -are my solution to this problem: we hand these out as students walk in, -giving them something to do until we can start the lesson. - - -
    - - - - -

    - - -## Advanced - -The highest level of the ORMC, and the group I spend most of my time with. -Students in ORMC Advanced are in high school, which means -they're ~14-18 years old. - - -
      - - - -

      diff --git a/crates/service/service-webpage/src/pages/handouts.rs b/crates/service/service-webpage/src/pages/handouts.rs new file mode 100644 index 0000000..da2fbdb --- /dev/null +++ b/crates/service/service-webpage/src/pages/handouts.rs @@ -0,0 +1,129 @@ +use std::time::Instant; + +use assetserver::Asset; +use chrono::TimeDelta; +use maud::{Markup, PreEscaped, html}; +use serde::Deserialize; +use tracing::{debug, warn}; + +use crate::{ + components::md::Markdown, + page::{Page, PageMetadata}, + routes::assets::Image_Icon, +}; + +#[derive(Debug, Deserialize)] +struct HandoutEntry { + title: String, + group: String, + handout: String, + solutions: Option, +} + +async fn get_index() -> Result, reqwest::Error> { + let start = Instant::now(); + let res = reqwest::get( + "https://git.betalupi.com/api/packages/Mark/generic/ormc-handouts/latest/index.json", + ) + .await; + + let res = match res { + Ok(x) => x, + Err(err) => { + warn!("Error while getting index: {err:?}"); + return Err(err); + } + }; + + let mut res: Vec = res.json().await?; + res.sort_by_key(|x| x.title.clone()); + + debug!( + message = "Fetched handout index", + n_handouts = res.len(), + time_ms = start.elapsed().as_millis() + ); + return Ok(res); +} + +fn build_list_for_group(handouts: &[HandoutEntry], group: &str) -> Markup { + html! { + ul class="handout-ul" { + + @for h in handouts { + @if h.group ==group { + li { + span class="handdout-li-title" { + strong { (h.title) } + } + span class="handout-li-links" { + "[ " + + @if let Some(solutions) = &h.solutions { + a href=(h.handout) {"handout"} + " | " + a href=(solutions) {"solutions"} + } @else { + a href=(h.handout) {"handout"} + } + "] " + } + } + } + } + } + } +} + +// +// MARK: page +// + +pub fn handouts() -> Page { + let md = Markdown::parse(include_str!("handouts.md")); + let mut meta = PageMetadata::from_markdown_frontmatter(&md) + .unwrap() + .unwrap(); + + if meta.image.is_none() { + meta.image = Some(Image_Icon::URL.to_string()); + } + + let html = PreEscaped(md.render()); + + Page { + meta, + html_ttl: Some(TimeDelta::seconds(300)), + + generate_html: Box::new(move |_page| { + let html = html.clone(); + Box::pin(async move { + let handouts = get_index().await.unwrap(); + + html! { + (html) + + (Markdown(concat!( + "## Warm-Ups", + "\n\n", + "Students never show up on time. Some come early, some come late. Warm-ups ", + "are my solution to this problem: we hand these out as students walk in, ", + "giving them something to do until we can start the lesson.", + ))) + (build_list_for_group(&handouts, "Warm-Ups")) + br {} + + (Markdown(concat!( + "## Advanced", + "\n\n", + "The highest level of the ORMC, and the group I spend most of my time with. ", + "Students in ORMC Advanced are in high school, which means ", + "they're ~14-18 years old.", + ))) + (build_list_for_group(&handouts, "Advanced")) + br {} + } + }) + }), + } +} diff --git a/crates/service/service-webpage/src/pages/index.rs b/crates/service/service-webpage/src/pages/index.rs index 0397e9f..438b6fd 100644 --- a/crates/service/service-webpage/src/pages/index.rs +++ b/crates/service/service-webpage/src/pages/index.rs @@ -23,53 +23,55 @@ pub fn index() -> Page { }, generate_html: Box::new(move |_page| { - html! { - h2 id="about" { "About" } + Box::pin(async { + html! { + h2 id="about" { "About" } - div { - img - src=(Image_Cover::URL) - style="float:left;margin:10px 10px 10px 10px;display:block;width:25%;" - {} + div { + img + src=(Image_Cover::URL) + style="float:left;margin:10px 10px 10px 10px;display:block;width:25%;" + {} - div style="margin:2ex 1ex 2ex 1ex;display:inline-block;overflow:hidden;width:60%;" { - "Welcome, you've reached Mark's main page. Here you'll find" - " links to various projects I've worked on." + div style="margin:2ex 1ex 2ex 1ex;display:inline-block;overflow:hidden;width:60%;" { + "Welcome, you've reached Mark's main page. Here you'll find" + " links to various projects I've worked on." - ul { - li { (MangledBetaEmail {}) } - li { (MangledGoogleEmail {}) } + ul { + li { (MangledBetaEmail {}) } + li { (MangledGoogleEmail {}) } - li { - ( - FarLink( - "https://github.com/rm-dr", - html!( - (FAIcon::Github) - "rm-dr" + li { + ( + FarLink( + "https://github.com/rm-dr", + html!( + (FAIcon::Github) + "rm-dr" + ) ) ) - ) - } + } - li { - ( - FarLink( - "https://git.betalupi.com", - html!( - (FAIcon::Git) - "git.betalupi.com" + li { + ( + FarLink( + "https://git.betalupi.com", + html!( + (FAIcon::Git) + "git.betalupi.com" + ) ) ) - ) + } } } + br style="clear:both;" {} } - br style="clear:both;" {} - } - (Markdown(include_str!("index.md"))) - } + (Markdown(include_str!("index.md"))) + } + }) }), ..Default::default() } diff --git a/crates/service/service-webpage/src/pages/mod.rs b/crates/service/service-webpage/src/pages/mod.rs index 857cbcd..9299603 100644 --- a/crates/service/service-webpage/src/pages/mod.rs +++ b/crates/service/service-webpage/src/pages/mod.rs @@ -1,9 +1,13 @@ -mod index; use assetserver::Asset; -pub use index::index; use crate::{page::Page, routes::assets::Image_Icon}; +mod handouts; +mod index; + +pub use handouts::handouts; +pub use index::index; + pub fn links() -> Page { /* Dead links: @@ -21,10 +25,3 @@ pub fn betalupi() -> Page { Some(Image_Icon::URL.to_string()), ) } - -pub fn handouts() -> Page { - Page::from_markdown( - include_str!("handouts.md"), - Some(Image_Icon::URL.to_string()), - ) -} diff --git a/crates/service/service-webpage/src/routes/mod.rs b/crates/service/service-webpage/src/routes/mod.rs index cc36818..0acdee2 100644 --- a/crates/service/service-webpage/src/routes/mod.rs +++ b/crates/service/service-webpage/src/routes/mod.rs @@ -1,3 +1,5 @@ +use std::{pin::Pin, sync::Arc, time::Duration}; + use assetserver::Asset; use axum::Router; use maud::{DOCTYPE, Markup, PreEscaped, html}; @@ -16,59 +18,65 @@ pub(super) fn router() -> Router<()> { let (asset_prefix, asset_router) = assets::asset_router(); info!("Serving assets at {asset_prefix}"); - let server = build_server().into_router(); + let server = build_server(); + tokio::task::spawn(server.clone().start_rerender_task(Duration::from_secs(3))); + let router = server.into_router(); - Router::new().merge(server).nest(asset_prefix, asset_router) + Router::new().merge(router).nest(asset_prefix, asset_router) } -fn build_server() -> PageServer { - PageServer::new(Box::new(page_wrapper)) +fn build_server() -> Arc { + let server = PageServer::new(Box::new(page_wrapper)); + server .add_page("/", pages::index()) .add_page("/links", pages::links()) .add_page("/whats-a-betalupi", pages::betalupi()) - .add_page("/handouts", pages::handouts()) + .add_page("/handouts", pages::handouts()); + server } -fn page_wrapper(page: &Page) -> Markup { - html! { - (DOCTYPE) - html { - head { - meta charset="UTF" {} - meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {} - meta content="text/html; charset=UTF-8" http-equiv="content-type" {} - meta property="og:type" content="website" {} +fn page_wrapper<'a>(page: &'a Page) -> Pin + 'a + Send + Sync>> { + Box::pin(async move { + html! { + (DOCTYPE) + html { + head { + meta charset="UTF" {} + meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {} + meta content="text/html; charset=UTF-8" http-equiv="content-type" {} + meta property="og:type" content="website" {} - link rel="stylesheet" href=(Styles_Main::URL) {} + link rel="stylesheet" href=(Styles_Main::URL) {} - (&page.meta) - title { (PreEscaped(page.meta.title.clone())) } - } + (&page.meta) + title { (PreEscaped(page.meta.title.clone())) } + } - body { - div class="wrapper" { - main { ( page.generate_html() ) } + body { + div class="wrapper" { + main { ( page.generate_html().await ) } - footer { - hr class = "footline" {} - div class = "footContainer" { - p { - "This site was built by hand using " - (FarLink("https://rust-lang.org", "Rust")) - ", " - (FarLink("https://maud.lambda.xyz", "Maud")) - ", " - (FarLink("https://github.com/connorskees/grass", "Grass")) - ", and " - (FarLink("https://docs.rs/axum/latest/axum", "Axum")) - "." + footer { + hr class = "footline" {} + div class = "footContainer" { + p { + "This site was built by hand using " + (FarLink("https://rust-lang.org", "Rust")) + ", " + (FarLink("https://maud.lambda.xyz", "Maud")) + ", " + (FarLink("https://github.com/connorskees/grass", "Grass")) + ", and " + (FarLink("https://docs.rs/axum/latest/axum", "Axum")) + "." + } } } } } } } - } + }) } #[test]