From 3b614518585d70207fbb03c299582fa01fd666a3 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 25 May 2026 21:38:36 +0200 Subject: [PATCH] refactor(kanban): compose page with hemplate Replace board page placeholder string replacement for options/board content with a hemplate page view and partial-rendered options. Cover page structure with DOM-aware scraper assertions. req: html_safety/002 req: view/001 req: test/005 --- examples/kanban/src/main.rs | 75 ++++++++++++++----- examples/kanban/templates/board.heml | 4 +- .../templates/partials/column_option.heml | 1 + .../templates/partials/column_options.heml | 3 + 4 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 examples/kanban/templates/partials/column_option.heml create mode 100644 examples/kanban/templates/partials/column_options.heml diff --git a/examples/kanban/src/main.rs b/examples/kanban/src/main.rs index 9bc3305..40571f6 100644 --- a/examples/kanban/src/main.rs +++ b/examples/kanban/src/main.rs @@ -17,11 +17,11 @@ const COLUMNS: [(&str, &str); 3] = [("backlog", "Backlog"), ("doing", "Doing"), #[derive(Default)] struct AppState { - board: Mutex, + board: Mutex, } #[derive(Default, Clone)] -struct Board { +struct BoardState { next_id: u64, cards: Vec, } @@ -33,6 +33,25 @@ struct Card { column: usize, } +#[derive(Hemplate)] +struct Board { + options: SafeHtml, + board: SafeHtml, +} + +#[derive(Hemplate)] +#[hemplate = "partials"] +struct ColumnOptions { + options: Vec, +} + +#[derive(Hemplate)] +#[hemplate = "partials"] +struct ColumnOption { + id: &'static str, + title: &'static str, +} + #[derive(Hemplate)] #[hemplate = "partials"] struct Presence { @@ -42,7 +61,7 @@ struct Presence { #[tokio::main] async fn main() { let state = Arc::new(AppState { - board: Mutex::new(Board { + board: Mutex::new(BoardState { next_id: 4, cards: vec![ Card { id: 1, title: "Write requirements".into(), column: 0 }, @@ -153,7 +172,7 @@ fn registry(state: Arc) -> HandlerRegistry { }) } -fn board_effects(board: &Board, notice: &'static str) -> impl IntoEffect { +fn board_effects(board: &BoardState, notice: &'static str) -> impl IntoEffect { ( ui::board::slots::board.html(SafeHtml::trusted(render_board(board))), ui::board::slots::notice.text(notice), @@ -161,7 +180,7 @@ fn board_effects(board: &Board, notice: &'static str) -> impl IntoEffect { ) } -fn update_card(board: &mut Board, card_id: Option<&str>, update: impl FnOnce(&mut Card)) -> bool { +fn update_card(board: &mut BoardState, card_id: Option<&str>, update: impl FnOnce(&mut Card)) -> bool { let Some(id) = card_id.and_then(|id| id.parse::().ok()) else { return false; }; @@ -180,10 +199,12 @@ fn parse_column(value: Option<&str>) -> usize { .unwrap_or(0) } -fn page_html(board: &Board) -> String { - ui::board::lower_html(include_str!("../templates/board.heml")) - .replace("__OPTIONS__", &render_options()) - .replace("__BOARD__", &render_board(board)) +fn page_html(board: &BoardState) -> String { + // req: html_safety/002 req: view/001 + ui::board::lower_html(render_template(&Board { + options: render_options(), + board: SafeHtml::trusted(render_board(board)), + })) } fn shell(body: String) -> String { @@ -213,15 +234,17 @@ fn shell(body: String) -> String { ) } -fn render_options() -> String { - COLUMNS - .iter() - .map(|(id, title)| format!(r#""#, escape_html(id), escape_html(title))) - .collect::>() - .join("") +fn render_options() -> SafeHtml { + // req: html_safety/002 req: view/001 + render_html(&ColumnOptions { + options: COLUMNS + .iter() + .map(|(id, title)| ColumnOption { id, title }) + .collect(), + }) } -fn render_board(board: &Board) -> String { +fn render_board(board: &BoardState) -> String { let mut out = String::from("
"); for (idx, (_, title)) in COLUMNS.iter().enumerate() { out.push_str(&format!(r#"

{}

"#, escape_html(title))); @@ -260,7 +283,11 @@ fn render_presence(count: u64) -> SafeHtml { } fn render_html(template: &impl Hemplate) -> SafeHtml { - SafeHtml::trusted(template.render().expect("kanban hemplate partial renders")) + SafeHtml::trusted(render_template(template)) +} + +fn render_template(template: &impl Hemplate) -> String { + template.render().expect("kanban hemplate view renders") } fn escape_html(value: &str) -> String { @@ -281,6 +308,20 @@ mod tests { Selector::parse(value).expect("test selector parses") } + // req: html_safety/002 req: view/001 req: test/005 + #[test] + fn kanban_page_is_composed_by_a_hemplate_view() { + let html = page_html(&BoardState::default()); + assert!(!html.contains("__OPTIONS__")); + assert!(!html.contains("__BOARD__")); + + let document = Html::parse_fragment(&html); + assert_eq!(document.select(&selector("section[data-slhx-root=\"kanban\"]")).count(), 1); + assert_eq!(document.select(&selector("select[name=\"column\"] > option")).count(), 3); + assert_eq!(document.select(&selector("[data-sid]")).count(), 3); + assert_eq!(document.select(&selector("[data-hid]")).count(), 4); + } + // req: html_safety/002 req: view/001 req: test/005 #[test] fn presence_payload_is_rendered_by_a_hemplate_view() { diff --git a/examples/kanban/templates/board.heml b/examples/kanban/templates/board.heml index f46ee36..47fc196 100644 --- a/examples/kanban/templates/board.heml +++ b/examples/kanban/templates/board.heml @@ -3,13 +3,13 @@

slhx Kanban

- +

Ready

-
__BOARD__
+
{+= self.board =+}
diff --git a/examples/kanban/templates/partials/column_option.heml b/examples/kanban/templates/partials/column_option.heml new file mode 100644 index 0000000..c5f60d5 --- /dev/null +++ b/examples/kanban/templates/partials/column_option.heml @@ -0,0 +1 @@ + diff --git a/examples/kanban/templates/partials/column_options.heml b/examples/kanban/templates/partials/column_options.heml new file mode 100644 index 0000000..64560dc --- /dev/null +++ b/examples/kanban/templates/partials/column_options.heml @@ -0,0 +1,3 @@ +