From d76682e98fe7d8898fc9a8e7f64223e5829ded72 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 25 May 2026 21:34:53 +0200 Subject: [PATCH] refactor(kanban): render presence with hemplate Move the kanban SSE presence fragment from Rust raw HTML formatting into a hemplate partial/view and cover the rendered structure with scraper. req: html_safety/002 req: view/001 req: test/005 --- Cargo.lock | 2 + examples/kanban/Cargo.toml | 2 + examples/kanban/src/main.rs | 47 ++++++++++++++++--- .../kanban/templates/partials/presence.heml | 1 + 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 examples/kanban/templates/partials/presence.heml diff --git a/Cargo.lock b/Cargo.lock index 9834544..d9a9697 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1409,6 +1409,8 @@ version = "0.1.0" dependencies = [ "axum", "futures-util", + "hemplate", + "scraper", "slhx", "slhx-axum", "slhx-build", diff --git a/examples/kanban/Cargo.toml b/examples/kanban/Cargo.toml index eb123b2..88b3884 100644 --- a/examples/kanban/Cargo.toml +++ b/examples/kanban/Cargo.toml @@ -10,11 +10,13 @@ path = "src/lib.rs" [dependencies] axum = "0.7" futures-util = "0.3" +hemplate = { path = "../../../hemplate/hemplate" } slhx = { path = "../../slhx" } slhx-axum = { path = "../../slhx-axum" } tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] } [dev-dependencies] +scraper = "0.23" slhx-test = { path = "../../slhx-test" } [build-dependencies] diff --git a/examples/kanban/src/main.rs b/examples/kanban/src/main.rs index da16db3..9bc3305 100644 --- a/examples/kanban/src/main.rs +++ b/examples/kanban/src/main.rs @@ -3,6 +3,7 @@ use axum::response::IntoResponse; use axum::routing::get; use axum::Router; use futures_util::{stream, StreamExt}; +use hemplate::Hemplate; use slhx::{IntoEffect, SafeHtml}; use slhx_axum::{runtime_js, sse, DispatchRejection, EffectResponse, HandlerRegistry, InteractionForm, PageRequest}; use slhx_kanban_example::ui; @@ -32,6 +33,12 @@ struct Card { column: usize, } +#[derive(Hemplate)] +#[hemplate = "partials"] +struct Presence { + count: u64, +} + #[tokio::main] async fn main() { let state = Arc::new(AppState { @@ -80,13 +87,13 @@ async fn interact( // req: push/001 req: push/003 req: examples/001 async fn events(Query(params): Query>) -> impl IntoResponse { if params.contains_key("once") { - let effect = ui::board::slots::presence.html(SafeHtml::trusted(render_presence(1))); + let effect = ui::board::slots::presence.html(render_presence(1)); return sse(stream::iter([Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT))]).boxed()); } let batches = stream::unfold(1_u64, |count| async move { tokio::time::sleep(Duration::from_secs(4)).await; - let effect = ui::board::slots::presence.html(SafeHtml::trusted(render_presence(count))); + let effect = ui::board::slots::presence.html(render_presence(count)); Some((Ok::<_, Infallible>(effect.into_batch(ui::BUILD_FINGERPRINT)), count + 1)) }) .boxed(); @@ -247,10 +254,13 @@ fn render_card(card: &Card) -> String { ) } -fn render_presence(count: u64) -> String { - format!( - r#"Ada online Grace online tick #{count}"# - ) +fn render_presence(count: u64) -> SafeHtml { + // req: html_safety/002 req: view/001 + render_html(&Presence { count }) +} + +fn render_html(template: &impl Hemplate) -> SafeHtml { + SafeHtml::trusted(template.render().expect("kanban hemplate partial renders")) } fn escape_html(value: &str) -> String { @@ -261,3 +271,28 @@ fn escape_html(value: &str) -> String { .replace('"', """) .replace('\'', "'") } + +#[cfg(test)] +mod tests { + use super::*; + use scraper::{Html, Selector}; + + fn selector(value: &str) -> Selector { + Selector::parse(value).expect("test selector parses") + } + + // req: html_safety/002 req: view/001 req: test/005 + #[test] + fn presence_payload_is_rendered_by_a_hemplate_view() { + let html = render_presence(7); + let document = Html::parse_fragment(html.as_str()); + assert_eq!(document.select(&selector("span.presence")).count(), 2); + assert_eq!( + document + .select(&selector("small")) + .next() + .map(|small| small.text().collect::()), + Some("tick #7".to_owned()) + ); + } +} diff --git a/examples/kanban/templates/partials/presence.heml b/examples/kanban/templates/partials/presence.heml new file mode 100644 index 0000000..3650d9c --- /dev/null +++ b/examples/kanban/templates/partials/presence.heml @@ -0,0 +1 @@ +Ada online Grace online tick #{+ self.count +}