From 7be1c9c649987ae3aa882495e5f67b02697ce741 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 12 Jun 2026 07:31:39 +0200 Subject: [PATCH] feat(build): make generated page helper canonical Move canonical examples and docs snippets from raw render/page calls to generated ui::page helpers, hide generated render behind doc(hidden), and let form resources participate in generated target inspection for tests. req: canonical_authoring/005 req: dx/006 req: codegen/002 req: public_api/002 req: test/001 --- examples/kanban.md | 2 +- examples/kanban/src/lib.rs | 2 +- examples/kanban/src/main.rs | 12 ++++++------ examples/saas/src/lib.rs | 34 +++++++++++++++------------------- examples/v0/src/main.rs | 20 ++++++++++---------- hemx-build/src/lib.rs | 16 ++++++++++------ hemx-core/src/lib.rs | 6 ++++++ 7 files changed, 49 insertions(+), 43 deletions(-) diff --git a/examples/kanban.md b/examples/kanban.md index b91487a..6b44ae8 100644 --- a/examples/kanban.md +++ b/examples/kanban.md @@ -94,7 +94,7 @@ use ui::board::{forms, targets}; targets::card.replace(card.id, &CardView::from(card)); forms::create_card.clear("title"); -ui::render(&BoardView::from(board)); +ui::page(&BoardView::from(board)); ``` No string desync. No manual ids. The generated module owns the names. diff --git a/examples/kanban/src/lib.rs b/examples/kanban/src/lib.rs index e46ae9c..26b992e 100644 --- a/examples/kanban/src/lib.rs +++ b/examples/kanban/src/lib.rs @@ -37,7 +37,7 @@ mod tests { // req: html_safety/002 req: view/001 req: test/005 #[test] fn kanban_board_test_payload_is_rendered_by_a_hemplate_view() { - let html = super::ui::render(&empty_board()); + let html = super::ui::page(&empty_board()); let document = Html::parse_fragment(html.as_str()); assert_eq!(document.select(&selector(".columns")).count(), 1); } diff --git a/examples/kanban/src/main.rs b/examples/kanban/src/main.rs index b8aeccf..c8d28db 100644 --- a/examples/kanban/src/main.rs +++ b/examples/kanban/src/main.rs @@ -281,15 +281,15 @@ fn parse_column(value: Option<&str>) -> usize { fn page_html(board: &BoardState) -> Html { // req: html_safety/002 req: view/001 - board_ui::render(&Board { + board_ui::page(&Board { options: render_options(), - board: ui::render(&board_view(board)), + board: ui::page(&board_view(board)), }) } fn shell(body: Html) -> Html { // req: html_safety/001 req: html_safety/002 req: axum_integration/001 - hemx::page(&AppShell { + ui::page(&AppShell { runtime_src: runtime_js_path(), body, }) @@ -297,7 +297,7 @@ fn shell(body: Html) -> Html { fn render_options() -> Html { // req: html_safety/002 req: view/001 - ui::render(&ColumnOptions { + ui::page(&ColumnOptions { options: COLUMNS .iter() .map(|(id, title)| ColumnOption { id, title }) @@ -385,7 +385,7 @@ mod tests { }], }; - let html = ui::render(&board_view(&board)); + let html = ui::page(&board_view(&board)); let document = Html::parse_fragment(html.as_str()); assert_eq!( document @@ -417,7 +417,7 @@ mod tests { // req: html_safety/002 req: view/001 req: test/005 #[test] fn presence_payload_is_rendered_by_a_hemplate_view() { - let html = ui::render(&Presence { count: 7 }); + let html = ui::page(&Presence { count: 7 }); let document = Html::parse_fragment(html.as_str()); assert_eq!( document diff --git a/examples/saas/src/lib.rs b/examples/saas/src/lib.rs index 26e1c57..9caa6e7 100644 --- a/examples/saas/src/lib.rs +++ b/examples/saas/src/lib.rs @@ -254,18 +254,18 @@ pub struct AppShell { } pub fn home_page(ctx: &AppContext) -> Html { - hemx::page(&AppShell { + ui::page(&AppShell { title: "hemx SaaS tutorial", runtime_src: runtime_js_path(), - body: hemx::page(&Dashboard::from_context(ctx)), + body: ui::page(&Dashboard::from_context(ctx)), }) } pub fn settings_page(ctx: &AppContext) -> Html { - hemx::page(&AppShell { + ui::page(&AppShell { title: "hemx SaaS tutorial settings", runtime_src: runtime_js_path(), - body: hemx::page(&Dashboard::settings(ctx)), + body: ui::page(&Dashboard::settings(ctx)), }) } @@ -330,7 +330,7 @@ fn project_summary(total: usize) -> String { mod tests { use super::*; use hemx_axum::{InteractionForm, InteractionRequest}; - use hemx_test::{inspect, inspect_batch}; + use hemx_test::{any_root_selector, inspect, inspect_batch, target_selector}; use scraper::{Html as ParsedHtml, Selector}; fn form(handle: hemx::Handle, fields: &[(&str, &str)]) -> InteractionForm { @@ -353,15 +353,13 @@ mod tests { let html = home_page(&ctx); let document = ParsedHtml::parse_document(html.as_str()); + assert_eq!(document.select(&selector(any_root_selector())).count(), 1); assert_eq!( document - .select(&selector("[data-hemx-root='dashboard']")) - .count(), - 1 - ); - assert_eq!( - document - .select(&selector("form[data-hemx-form='new_project']")) + .select(&selector(&format!( + "form{}", + target_selector(dashboard::new_project) + ))) .count(), 1 ); @@ -389,15 +387,13 @@ mod tests { let html = settings_page(&ctx); let document = ParsedHtml::parse_document(html.as_str()); + assert_eq!(document.select(&selector(any_root_selector())).count(), 1); assert_eq!( document - .select(&selector("[data-hemx-root='dashboard']")) - .count(), - 1 - ); - assert_eq!( - document - .select(&selector("[data-hemx-slot='page_panel'] .settings-page")) + .select(&selector(&format!( + "{} .settings-page", + target_selector(dashboard::page_panel) + ))) .count(), 1 ); diff --git a/examples/v0/src/main.rs b/examples/v0/src/main.rs index d7b721e..f2e2302 100644 --- a/examples/v0/src/main.rs +++ b/examples/v0/src/main.rs @@ -268,18 +268,18 @@ fn registry(state: Arc) -> Registry { fn all_examples() -> Html { // req: html_safety/001 req: html_safety/002 req: component/003 Html::join([ - counter::render(&Counter), - hemx::page(&todos_view(&[])), - wizard::render(&Wizard), - auth::render(&Auth), + counter::page(&Counter), + ui::page(&todos_view(&[])), + wizard::page(&Wizard), + auth::page(&Auth), render_page_swap("Welcome", "Welcome"), - notifications::render(&Notifications), + notifications::page(&Notifications), ]) } fn shell(body: Html) -> Html { // req: html_safety/001 req: html_safety/002 req: axum_integration/001 req: component/003 - hemx::page(&AppShell { + ui::page(&AppShell { runtime_src: runtime_js_path(), body, }) @@ -447,7 +447,7 @@ fn todo_summary(todos: &[TodoRecord]) -> String { fn render_page_swap(title: &'static str, message: &'static str) -> Html { // req: html_safety/002 req: view/001 req: component/003 - page_swap::render(&PageSwap { + page_swap::page(&PageSwap { content: render_docs_content(message), title, }) @@ -455,7 +455,7 @@ fn render_page_swap(title: &'static str, message: &'static str) -> Html { fn render_docs_content(message: &'static str) -> Html { // req: html_safety/002 req: view/001 req: component/003 - hemx::page(&DocsContent { message }) + ui::page(&DocsContent { message }) } #[cfg(test)] @@ -570,7 +570,7 @@ mod tests { title: "Ship v0".to_owned(), }]; - let html = hemx::page(&todos_view(&todos)); + let html = ui::page(&todos_view(&todos)); let document = Html::parse_fragment(html.as_str()); let rows = document .select(&selector(&keyed_items_selector("li"))) @@ -590,7 +590,7 @@ mod tests { // req: html_safety/002 req: view/001 req: test/005 #[test] fn empty_todos_payload_is_rendered_by_a_hemplate_view() { - let html = hemx::page(&todos_view(&[])); + let html = ui::page(&todos_view(&[])); let document = Html::parse_fragment(html.as_str()); let rows = document .select(&selector(&list_item_selector(""))) diff --git a/hemx-build/src/lib.rs b/hemx-build/src/lib.rs index 626fc73..6f92df3 100644 --- a/hemx-build/src/lib.rs +++ b/hemx-build/src/lib.rs @@ -732,6 +732,7 @@ impl Resources { "{pad} ::hemx::__private::html_trusted(lower(html))\n" )); out.push_str(&format!("{pad}}}\n\n")); + out.push_str(&format!("{pad}#[doc(hidden)]\n")); out.push_str(&format!( "{pad}pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{\n" )); @@ -1733,9 +1734,9 @@ mod tests { )); assert!(generated.contains("#[doc(hidden)]\npub fn lower_html")); assert!(generated.contains("pub fn static_fragment(html: &'static str) -> ::hemx::Html")); - assert!( - generated.contains("pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") - ); + assert!(generated.contains( + "#[doc(hidden)]\npub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html" + )); assert!(generated.contains("pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html")); assert!(generated.contains( "#[doc(hidden)]\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html" @@ -1846,9 +1847,9 @@ mod tests { )); assert!(generated.contains("\n#[doc(hidden)]\npub fn lower_html")); assert!(generated.contains("\npub fn static_fragment(html: &'static str) -> ::hemx::Html")); - assert!( - generated.contains("\npub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") - ); + assert!(generated.contains( + "\n#[doc(hidden)]\npub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html" + )); assert!( generated.contains("\npub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") ); @@ -1865,6 +1866,9 @@ mod tests { assert!(generated.contains(" pub const todos: SlotTarget<::std::string::String> = SlotTarget::new(super::advanced::slots::todos);")); assert!(generated.contains(" pub use self::targets::todos;")); assert!(generated.contains(" #[doc(hidden)]\n pub fn lower_html")); + assert!(generated.contains( + " #[doc(hidden)]\n pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html" + )); assert!( generated.contains(" pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") ); diff --git a/hemx-core/src/lib.rs b/hemx-core/src/lib.rs index effc2f9..cb4f5e2 100644 --- a/hemx-core/src/lib.rs +++ b/hemx-core/src/lib.rs @@ -1165,6 +1165,12 @@ impl core::fmt::Display for Form { } } +impl GeneratedTarget for Form { + fn __hemx_resource_id(self) -> ResourceId { + self.id + } +} + impl Form { pub const fn new(id: u32) -> Self { Self {