From bd35347fa270b57016a8f0ee9c28fdae3a495854 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 25 May 2026 21:45:25 +0200 Subject: [PATCH] refactor(kanban): render board cards with hemplate Move Kanban board/column/card rendering from Rust raw HTML assembly into hemplate partial views. Keep handler effects returning generated slot payloads and cover rendered structure with scraper. req: html_safety/002 req: view/001 req: test/005 --- examples/kanban/src/main.rs | 118 ++++++++++++------ .../kanban/templates/partials/board_card.heml | 10 ++ .../templates/partials/board_column.heml | 6 + .../templates/partials/board_columns.heml | 5 + 4 files changed, 99 insertions(+), 40 deletions(-) create mode 100644 examples/kanban/templates/partials/board_card.heml create mode 100644 examples/kanban/templates/partials/board_column.heml create mode 100644 examples/kanban/templates/partials/board_columns.heml diff --git a/examples/kanban/src/main.rs b/examples/kanban/src/main.rs index 40571f6..6b6c746 100644 --- a/examples/kanban/src/main.rs +++ b/examples/kanban/src/main.rs @@ -33,6 +33,31 @@ struct Card { column: usize, } +#[derive(Hemplate)] +#[hemplate = "partials"] +struct BoardColumns { + columns: Vec, +} + +#[derive(Hemplate)] +#[hemplate = "partials"] +struct BoardColumn { + title: &'static str, + cards: Vec, +} + +#[derive(Hemplate)] +#[hemplate = "partials"] +struct BoardCard { + id: u64, + title: String, + left_handle: u32, + right_handle: u32, + delete_handle: u32, + left_disabled: bool, + right_disabled: bool, +} + #[derive(Hemplate)] struct Board { options: SafeHtml, @@ -174,7 +199,7 @@ fn registry(state: Arc) -> HandlerRegistry { fn board_effects(board: &BoardState, notice: &'static str) -> impl IntoEffect { ( - ui::board::slots::board.html(SafeHtml::trusted(render_board(board))), + ui::board::slots::board.html(render_board(board)), ui::board::slots::notice.text(notice), ui::board::forms::create_card.clear("title"), ) @@ -203,7 +228,7 @@ 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)), + board: render_board(board), })) } @@ -244,37 +269,35 @@ fn render_options() -> SafeHtml { }) } -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))); - for card in board.cards.iter().filter(|card| card.column == idx) { - out.push_str(&render_card(card)); - } - out.push_str("
"); - } - out.push_str("
"); - out +fn render_board(board: &BoardState) -> SafeHtml { + // req: html_safety/002 req: view/001 + render_html(&BoardColumns { + columns: COLUMNS + .iter() + .enumerate() + .map(|(idx, (_, title))| BoardColumn { + title, + cards: board + .cards + .iter() + .filter(|card| card.column == idx) + .map(render_card) + .collect(), + }) + .collect(), + }) } -fn render_card(card: &Card) -> String { - format!( - r#"
- {title} - - - - - -
"#, - id = card.id, - title = escape_html(&card.title), - left = ui::board::handles::move_left.id().id, - right = ui::board::handles::move_right.id().id, - delete = ui::board::handles::delete_card.id().id, - left_disabled = if card.column == 0 { "disabled" } else { "" }, - right_disabled = if card.column + 1 == COLUMNS.len() { "disabled" } else { "" }, - ) +fn render_card(card: &Card) -> BoardCard { + BoardCard { + id: card.id, + title: card.title.clone(), + left_handle: ui::board::handles::move_left.id().id, + right_handle: ui::board::handles::move_right.id().id, + delete_handle: ui::board::handles::delete_card.id().id, + left_disabled: card.column == 0, + right_disabled: card.column + 1 == COLUMNS.len(), + } } fn render_presence(count: u64) -> SafeHtml { @@ -290,15 +313,6 @@ fn render_template(template: &impl Hemplate) -> String { template.render().expect("kanban hemplate view renders") } -fn escape_html(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} - #[cfg(test)] mod tests { use super::*; @@ -322,6 +336,30 @@ mod tests { assert_eq!(document.select(&selector("[data-hid]")).count(), 4); } + // req: html_safety/002 req: view/001 req: test/005 + #[test] + fn board_payload_is_rendered_by_a_hemplate_view() { + let board = BoardState { + next_id: 2, + cards: vec![Card { + id: 1, + title: "Compile checked".to_owned(), + column: 0, + }], + }; + + let html = render_board(&board); + let document = Html::parse_fragment(html.as_str()); + assert_eq!(document.select(&selector(".columns > section.column")).count(), 3); + let card = document + .select(&selector("article.card[data-key=\"1\"]")) + .next() + .expect("card renders"); + assert!(card.text().collect::().contains("Compile checked")); + assert!(card.select(&selector("b")).next().is_none()); + assert_eq!(card.select(&selector("button[disabled]")).count(), 1); + } + // 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/partials/board_card.heml b/examples/kanban/templates/partials/board_card.heml new file mode 100644 index 0000000..7f2f983 --- /dev/null +++ b/examples/kanban/templates/partials/board_card.heml @@ -0,0 +1,10 @@ +
+ {+ self.title +} + + + + + + + +
diff --git a/examples/kanban/templates/partials/board_column.heml b/examples/kanban/templates/partials/board_column.heml new file mode 100644 index 0000000..d0134e4 --- /dev/null +++ b/examples/kanban/templates/partials/board_column.heml @@ -0,0 +1,6 @@ +
+

{+ self.title +}

+ +
diff --git a/examples/kanban/templates/partials/board_columns.heml b/examples/kanban/templates/partials/board_columns.heml new file mode 100644 index 0000000..8ee9740 --- /dev/null +++ b/examples/kanban/templates/partials/board_columns.heml @@ -0,0 +1,5 @@ +
+ +