diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 02d3a9e..bc60680 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -76,7 +76,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps 005 Error messages must explain fixes in author language, not internal language. Say “add `h-key="todo.id"` to this `h-for`”, not “missing ScopeKey for ResourceRef”. ### req: dx/006 -006 Generated resource methods are the preferred authoring API: `slots::todo_list.render(view)`, `slots::card.replace(key, view)`, `slots::count.text(42)`, `atoms::user.set(user)`. These return `impl IntoEffect`. Raw `Effect` constructors, opcodes, and `EffectWriter` remain low-level. [north_star] +006 Generated resource methods are the preferred authoring API: `slots::todo_list.render(view)`, `slots::card.replace(key, view)`, `slots::count.text(42)`, `atoms::user.set(user)`. Generated view modules also expose `render(view)` for trusted hemplate-to-`SafeHtml` page and fragment composition; `render_html(view)` remains as an explicit compatibility alias. These return `impl IntoEffect` or `SafeHtml` at the boundary. Raw `Effect` constructors, opcodes, and `EffectWriter` remain low-level. [north_star] ### req: dx/007 007 Tuple composition of `IntoEffect` is the canonical batch syntax: `(a, b, c)` implements `IntoEffect` up to arity 12. `Effect::batch((...))` is available but not required for the happy path. diff --git a/examples/kanban/src/lib.rs b/examples/kanban/src/lib.rs index 2e9e494..5e86bfa 100644 --- a/examples/kanban/src/lib.rs +++ b/examples/kanban/src/lib.rs @@ -38,7 +38,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_html(&empty_board()); + let html = super::ui::render(&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 92756ea..39d3c90 100644 --- a/examples/kanban/src/main.rs +++ b/examples/kanban/src/main.rs @@ -226,7 +226,7 @@ fn parse_column(value: Option<&str>) -> usize { fn page_html(board: &BoardState) -> String { // Explicit full-page composition boundary for already-rendered hemplate fragments. // req: html_safety/002 req: view/001 - board_ui::render_html(&Board { + board_ui::render(&Board { options: render_options(), board: render_board(board), }) @@ -262,7 +262,7 @@ fn shell(body: String) -> String { fn render_options() -> SafeHtml { // req: html_safety/002 req: view/001 - ui::render_html(&ColumnOptions { + ui::render(&ColumnOptions { options: COLUMNS .iter() .map(|(id, title)| ColumnOption { id, title }) @@ -291,7 +291,7 @@ fn board_view(board: &BoardState) -> BoardColumns { fn render_board(board: &BoardState) -> SafeHtml { // req: html_safety/002 req: view/001 - ui::render_html(&board_view(board)) + ui::render(&board_view(board)) } fn render_card(card: &Card) -> BoardCard { @@ -354,7 +354,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_html(&Presence { count: 7 }); + let html = ui::render(&Presence { count: 7 }); let document = Html::parse_fragment(html.as_str()); assert_eq!(document.select(&selector("span.presence")).count(), 2); assert_eq!( diff --git a/examples/techdemo/src/main.rs b/examples/techdemo/src/main.rs index 9c3b2a6..f1c046b 100644 --- a/examples/techdemo/src/main.rs +++ b/examples/techdemo/src/main.rs @@ -461,7 +461,7 @@ fn page_html(demo: &DemoState) -> String { fn shell(body: String) -> String { // Explicit full-page shell composition boundary for already-rendered hemplate fragments. // req: html_safety/002 req: view/001 req: component/003 - ui::app_shell::render_html(&AppShell { body: SafeHtml::trusted(body) }).into_string() + ui::app_shell::render(&AppShell { body: SafeHtml::trusted(body) }).into_string() } fn hero_view(demo: &DemoState) -> HeroMetrics { @@ -479,7 +479,7 @@ fn hero_view(demo: &DemoState) -> HeroMetrics { fn render_hero(demo: &DemoState) -> SafeHtml { // req: html_safety/002 req: view/001 req: component/003 - ui::render_html(&hero_view(demo)) + ui::render(&hero_view(demo)) } fn board_view(demo: &DemoState) -> BoardLanes { @@ -506,7 +506,7 @@ fn board_view(demo: &DemoState) -> BoardLanes { fn render_board(demo: &DemoState) -> SafeHtml { // req: html_safety/002 req: view/001 req: component/003 - ui::render_html(&board_view(demo)) + ui::render(&board_view(demo)) } fn issue_card(item: &WorkItem, selected: bool) -> IssueCard { @@ -535,7 +535,7 @@ fn activity_view(demo: &DemoState) -> ActivityFeed { fn render_activity(demo: &DemoState) -> SafeHtml { // req: html_safety/002 req: view/001 req: component/003 - ui::render_html(&activity_view(demo)) + ui::render(&activity_view(demo)) } fn inspector_view(demo: &DemoState) -> InspectorPanel { @@ -544,9 +544,9 @@ fn inspector_view(demo: &DemoState) -> InspectorPanel { .selected_id .and_then(|id| demo.work.iter().find(|item| item.id == id)); let selected = selected.map_or_else( - || ui::render_html(&InspectorEmpty), + || ui::render(&InspectorEmpty), |item| { - ui::render_html(&InspectorSelected { + ui::render(&InspectorSelected { title: item.title.clone(), lane: LANES[item.lane].1, stage: item.stage.label(), @@ -562,32 +562,32 @@ fn inspector_view(demo: &DemoState) -> InspectorPanel { fn render_inspector(demo: &DemoState) -> SafeHtml { // req: html_safety/002 req: view/001 req: component/003 - ui::render_html(&inspector_view(demo)) + ui::render(&inspector_view(demo)) } fn architecture_hero() -> SafeHtml { // req: html_safety/002 req: view/001 - ui::render_html(&ArchitectureHero) + ui::render(&ArchitectureHero) } fn architecture_board() -> SafeHtml { // req: html_safety/002 req: view/001 - ui::render_html(&ArchitectureBoard) + ui::render(&ArchitectureBoard) } fn architecture_inspector() -> SafeHtml { // req: html_safety/002 req: view/001 - ui::render_html(&ArchitectureInspector) + ui::render(&ArchitectureInspector) } fn architecture_activity() -> SafeHtml { // req: html_safety/002 req: view/001 - ui::render_html(&ArchitectureActivity) + ui::render(&ArchitectureActivity) } fn render_control_center(page: ControlCenter) -> String { // req: html_safety/002 req: view/001 req: component/003 - ui::render_html(&page).into_string() + ui::render(&page).into_string() } @@ -705,7 +705,7 @@ mod tests { // req: html_safety/002 req: view/001 req: test/005 #[test] fn live_feed_payload_is_rendered_by_a_hemplate_view() { - let html = ui::render_html(&LiveFeed { tick: 7 }); + let html = ui::render(&LiveFeed { tick: 7 }); let document = Html::parse_fragment(html.as_str()); let row = document .select(&selector(".live-row")) diff --git a/examples/v0/src/main.rs b/examples/v0/src/main.rs index 825a102..43cea4c 100644 --- a/examples/v0/src/main.rs +++ b/examples/v0/src/main.rs @@ -194,7 +194,7 @@ fn all_examples() -> String { fn shell(body: String) -> String { // Explicit full-page shell composition boundary for already-rendered hemplate fragments. // req: html_safety/002 req: view/001 req: component/003 - ui::render_html(&AppShell { + ui::render(&AppShell { body: SafeHtml::trusted(body), }) .into_string() @@ -215,7 +215,7 @@ fn todos_view(todos: &[Todo]) -> TodoItems { fn render_page_swap(title: &'static str, message: &'static str) -> String { // req: html_safety/002 req: view/001 req: component/003 - page_swap::render_html(&PageSwap { + page_swap::render(&PageSwap { content: render_docs_content(message), title, }) @@ -224,7 +224,7 @@ fn render_page_swap(title: &'static str, message: &'static str) -> String { fn render_docs_content(message: &'static str) -> SafeHtml { // req: html_safety/002 req: view/001 req: component/003 - ui::render_html(&DocsContent { message }) + ui::render(&DocsContent { message }) } #[cfg(test)] @@ -298,7 +298,7 @@ mod tests { fn todos_payload_is_rendered_by_a_hemplate_view() { let todos = vec![Todo { id: 7, title: "Ship v0".to_owned() }]; - let html = ui::render_html(&todos_view(&todos)); + let html = ui::render(&todos_view(&todos)); let document = Html::parse_fragment(html.as_str()); let rows = document.select(&selector("li[data-key]")).collect::>(); assert_eq!(rows.len(), 1); @@ -310,7 +310,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 = ui::render_html(&todos_view(&[])); + let html = ui::render(&todos_view(&[])); let document = Html::parse_fragment(html.as_str()); let rows = document.select(&selector("li")).collect::>(); assert_eq!(rows.len(), 1); diff --git a/slhx-build/src/lib.rs b/slhx-build/src/lib.rs index 6795469..0e5bbb6 100644 --- a/slhx-build/src/lib.rs +++ b/slhx-build/src/lib.rs @@ -524,11 +524,12 @@ impl Resources { } out.push_str(&format!("__slhx_lower_html(html.as_ref(), &{table_name})\n")); out.push_str(&format!("{pad}}}\n\n")); - out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml {{\n")); + out.push_str(&format!("{pad}pub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml {{\n")); out.push_str(&format!("{pad} let mut html = ::std::string::String::new();\n")); out.push_str(&format!("{pad} ::hemplate::Hemplate::render_into(view, &mut html).expect(\"slhx hemplate view renders\");\n")); out.push_str(&format!("{pad} ::slhx::SafeHtml::trusted(lower_html(html))\n")); out.push_str(&format!("{pad}}}\n\n")); + out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml {{ render(view) }}\n\n")); self.push_lowering_table(out, component, indent, &table_name); } @@ -1298,6 +1299,7 @@ mod tests { assert!(generated.contains("pub mod todo")); assert!(generated.contains("pub const ALL_IDS")); assert!(generated.contains("pub fn lower_html")); + assert!(generated.contains("pub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml")); assert!(generated.contains("pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml")); assert!(generated.contains("data-slhx-slot")); assert!(generated.contains("data-slhx-form")); @@ -1364,6 +1366,7 @@ mod tests { assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod components")); assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod slots")); assert!(generated.contains("\npub fn lower_html")); + assert!(generated.contains("\npub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml")); assert!(generated.contains("\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml")); assert!(generated.contains("pub mod todo")); assert!(generated.contains(" pub const COMPONENT: ::slhx::ComponentRef = ::slhx::ComponentRef::new(\"todo\")"));