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
This commit is contained in:
slhx agent
2026-06-12 07:31:39 +02:00
parent 4a8442c5f9
commit 7be1c9c649
7 changed files with 49 additions and 43 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ use ui::board::{forms, targets};
targets::card.replace(card.id, &CardView::from(card)); targets::card.replace(card.id, &CardView::from(card));
forms::create_card.clear("title"); 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. No string desync. No manual ids. The generated module owns the names.
+1 -1
View File
@@ -37,7 +37,7 @@ mod tests {
// req: html_safety/002 req: view/001 req: test/005 // req: html_safety/002 req: view/001 req: test/005
#[test] #[test]
fn kanban_board_test_payload_is_rendered_by_a_hemplate_view() { 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()); let document = Html::parse_fragment(html.as_str());
assert_eq!(document.select(&selector(".columns")).count(), 1); assert_eq!(document.select(&selector(".columns")).count(), 1);
} }
+6 -6
View File
@@ -281,15 +281,15 @@ fn parse_column(value: Option<&str>) -> usize {
fn page_html(board: &BoardState) -> Html { fn page_html(board: &BoardState) -> Html {
// req: html_safety/002 req: view/001 // req: html_safety/002 req: view/001
board_ui::render(&Board { board_ui::page(&Board {
options: render_options(), options: render_options(),
board: ui::render(&board_view(board)), board: ui::page(&board_view(board)),
}) })
} }
fn shell(body: Html) -> Html { fn shell(body: Html) -> Html {
// req: html_safety/001 req: html_safety/002 req: axum_integration/001 // req: html_safety/001 req: html_safety/002 req: axum_integration/001
hemx::page(&AppShell { ui::page(&AppShell {
runtime_src: runtime_js_path(), runtime_src: runtime_js_path(),
body, body,
}) })
@@ -297,7 +297,7 @@ fn shell(body: Html) -> Html {
fn render_options() -> Html { fn render_options() -> Html {
// req: html_safety/002 req: view/001 // req: html_safety/002 req: view/001
ui::render(&ColumnOptions { ui::page(&ColumnOptions {
options: COLUMNS options: COLUMNS
.iter() .iter()
.map(|(id, title)| ColumnOption { id, title }) .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()); let document = Html::parse_fragment(html.as_str());
assert_eq!( assert_eq!(
document document
@@ -417,7 +417,7 @@ mod tests {
// req: html_safety/002 req: view/001 req: test/005 // req: html_safety/002 req: view/001 req: test/005
#[test] #[test]
fn presence_payload_is_rendered_by_a_hemplate_view() { 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()); let document = Html::parse_fragment(html.as_str());
assert_eq!( assert_eq!(
document document
+15 -19
View File
@@ -254,18 +254,18 @@ pub struct AppShell {
} }
pub fn home_page(ctx: &AppContext) -> Html { pub fn home_page(ctx: &AppContext) -> Html {
hemx::page(&AppShell { ui::page(&AppShell {
title: "hemx SaaS tutorial", title: "hemx SaaS tutorial",
runtime_src: runtime_js_path(), 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 { pub fn settings_page(ctx: &AppContext) -> Html {
hemx::page(&AppShell { ui::page(&AppShell {
title: "hemx SaaS tutorial settings", title: "hemx SaaS tutorial settings",
runtime_src: runtime_js_path(), 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 { mod tests {
use super::*; use super::*;
use hemx_axum::{InteractionForm, InteractionRequest}; 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}; use scraper::{Html as ParsedHtml, Selector};
fn form<I>(handle: hemx::Handle<I>, fields: &[(&str, &str)]) -> InteractionForm { fn form<I>(handle: hemx::Handle<I>, fields: &[(&str, &str)]) -> InteractionForm {
@@ -353,15 +353,13 @@ mod tests {
let html = home_page(&ctx); let html = home_page(&ctx);
let document = ParsedHtml::parse_document(html.as_str()); let document = ParsedHtml::parse_document(html.as_str());
assert_eq!(document.select(&selector(any_root_selector())).count(), 1);
assert_eq!( assert_eq!(
document document
.select(&selector("[data-hemx-root='dashboard']")) .select(&selector(&format!(
.count(), "form{}",
1 target_selector(dashboard::new_project)
); )))
assert_eq!(
document
.select(&selector("form[data-hemx-form='new_project']"))
.count(), .count(),
1 1
); );
@@ -389,15 +387,13 @@ mod tests {
let html = settings_page(&ctx); let html = settings_page(&ctx);
let document = ParsedHtml::parse_document(html.as_str()); let document = ParsedHtml::parse_document(html.as_str());
assert_eq!(document.select(&selector(any_root_selector())).count(), 1);
assert_eq!( assert_eq!(
document document
.select(&selector("[data-hemx-root='dashboard']")) .select(&selector(&format!(
.count(), "{} .settings-page",
1 target_selector(dashboard::page_panel)
); )))
assert_eq!(
document
.select(&selector("[data-hemx-slot='page_panel'] .settings-page"))
.count(), .count(),
1 1
); );
+10 -10
View File
@@ -268,18 +268,18 @@ fn registry(state: Arc<ExampleState>) -> Registry {
fn all_examples() -> Html { fn all_examples() -> Html {
// req: html_safety/001 req: html_safety/002 req: component/003 // req: html_safety/001 req: html_safety/002 req: component/003
Html::join([ Html::join([
counter::render(&Counter), counter::page(&Counter),
hemx::page(&todos_view(&[])), ui::page(&todos_view(&[])),
wizard::render(&Wizard), wizard::page(&Wizard),
auth::render(&Auth), auth::page(&Auth),
render_page_swap("Welcome", "Welcome"), render_page_swap("Welcome", "Welcome"),
notifications::render(&Notifications), notifications::page(&Notifications),
]) ])
} }
fn shell(body: Html) -> Html { fn shell(body: Html) -> Html {
// req: html_safety/001 req: html_safety/002 req: axum_integration/001 req: component/003 // 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(), runtime_src: runtime_js_path(),
body, body,
}) })
@@ -447,7 +447,7 @@ fn todo_summary(todos: &[TodoRecord]) -> String {
fn render_page_swap(title: &'static str, message: &'static str) -> Html { fn render_page_swap(title: &'static str, message: &'static str) -> Html {
// req: html_safety/002 req: view/001 req: component/003 // req: html_safety/002 req: view/001 req: component/003
page_swap::render(&PageSwap { page_swap::page(&PageSwap {
content: render_docs_content(message), content: render_docs_content(message),
title, title,
}) })
@@ -455,7 +455,7 @@ fn render_page_swap(title: &'static str, message: &'static str) -> Html {
fn render_docs_content(message: &'static str) -> Html { fn render_docs_content(message: &'static str) -> Html {
// req: html_safety/002 req: view/001 req: component/003 // req: html_safety/002 req: view/001 req: component/003
hemx::page(&DocsContent { message }) ui::page(&DocsContent { message })
} }
#[cfg(test)] #[cfg(test)]
@@ -570,7 +570,7 @@ mod tests {
title: "<b>Ship v0</b>".to_owned(), title: "<b>Ship v0</b>".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 document = Html::parse_fragment(html.as_str());
let rows = document let rows = document
.select(&selector(&keyed_items_selector("li"))) .select(&selector(&keyed_items_selector("li")))
@@ -590,7 +590,7 @@ mod tests {
// req: html_safety/002 req: view/001 req: test/005 // req: html_safety/002 req: view/001 req: test/005
#[test] #[test]
fn empty_todos_payload_is_rendered_by_a_hemplate_view() { 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 document = Html::parse_fragment(html.as_str());
let rows = document let rows = document
.select(&selector(&list_item_selector(""))) .select(&selector(&list_item_selector("")))
+10 -6
View File
@@ -732,6 +732,7 @@ impl Resources {
"{pad} ::hemx::__private::html_trusted(lower(html))\n" "{pad} ::hemx::__private::html_trusted(lower(html))\n"
)); ));
out.push_str(&format!("{pad}}}\n\n")); out.push_str(&format!("{pad}}}\n\n"));
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
out.push_str(&format!( out.push_str(&format!(
"{pad}pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{\n" "{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("#[doc(hidden)]\npub fn lower_html"));
assert!(generated.contains("pub fn static_fragment(html: &'static str) -> ::hemx::Html")); assert!(generated.contains("pub fn static_fragment(html: &'static str) -> ::hemx::Html"));
assert!( assert!(generated.contains(
generated.contains("pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") "#[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("pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html"));
assert!(generated.contains( assert!(generated.contains(
"#[doc(hidden)]\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html" "#[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("\n#[doc(hidden)]\npub fn lower_html"));
assert!(generated.contains("\npub fn static_fragment(html: &'static str) -> ::hemx::Html")); assert!(generated.contains("\npub fn static_fragment(html: &'static str) -> ::hemx::Html"));
assert!( assert!(generated.contains(
generated.contains("\npub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") "\n#[doc(hidden)]\npub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html"
); ));
assert!( assert!(
generated.contains("\npub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") 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 const todos: SlotTarget<::std::string::String> = SlotTarget::new(super::advanced::slots::todos);"));
assert!(generated.contains(" pub use self::targets::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 lower_html"));
assert!(generated.contains(
" #[doc(hidden)]\n pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html"
));
assert!( assert!(
generated.contains(" pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") generated.contains(" pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html")
); );
+6
View File
@@ -1165,6 +1165,12 @@ impl<T> core::fmt::Display for Form<T> {
} }
} }
impl<T> GeneratedTarget for Form<T> {
fn __hemx_resource_id(self) -> ResourceId {
self.id
}
}
impl<T> Form<T> { impl<T> Form<T> {
pub const fn new(id: u32) -> Self { pub const fn new(id: u32) -> Self {
Self { Self {