feat(build): lower static fragments as safe html

Add a generated static_fragment helper for include_str!-style prototype fragments and use it in the v0 page assembly path so static lowered .heml no longer needs repeated SafeHtml::trusted plumbing.

req: dx/006

req: html_safety/001

req: html_safety/002

req: component/003
This commit is contained in:
slhx agent
2026-06-01 23:57:19 +02:00
parent eaa5479b42
commit bc7e7881b4
3 changed files with 12 additions and 6 deletions
+1 -1
View File
@@ -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”. 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 ### 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)`. The public facade and generated view modules expose `render(view)` for trusted hemplate-to-`SafeHtml` page and fragment composition, plus `lower(html)` for prototype/static `.heml` fragments that need generated resource lowering; `render_html(view)` and `lower_html(html)` remain explicit compatibility aliases. These return `impl IntoEffect`, `SafeHtml`, or lowered HTML at the boundary. 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)`. The public facade and generated view modules expose `render(view)` for trusted hemplate-to-`SafeHtml` page and fragment composition, `static_fragment(include_str!(...))` for prototype/static `.heml` fragments that need generated resource lowering as `SafeHtml`, plus `lower(html)` for callers that need the lowered string; `render_html(view)` and `lower_html(html)` remain explicit compatibility aliases. These return `impl IntoEffect`, `SafeHtml`, or lowered HTML at the boundary. Raw `Effect` constructors, opcodes, and `EffectWriter` remain low-level. [north_star]
### req: dx/007 ### 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. 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.
+5 -5
View File
@@ -183,12 +183,12 @@ fn all_examples() -> SafeHtml {
// Static `.heml` fragments are lowered by generated code before they join rendered views. // Static `.heml` fragments are lowered by generated code before they join rendered views.
// req: html_safety/001 req: html_safety/002 req: component/003 // req: html_safety/001 req: html_safety/002 req: component/003
SafeHtml::join([ SafeHtml::join([
SafeHtml::trusted(counter::lower(include_str!("../templates/counter.heml"))), counter::static_fragment(include_str!("../templates/counter.heml")),
SafeHtml::trusted(todos::lower(include_str!("../templates/todos.heml"))), todos::static_fragment(include_str!("../templates/todos.heml")),
SafeHtml::trusted(wizard::lower(include_str!("../templates/wizard.heml"))), wizard::static_fragment(include_str!("../templates/wizard.heml")),
SafeHtml::trusted(auth::lower(include_str!("../templates/auth.heml"))), auth::static_fragment(include_str!("../templates/auth.heml")),
render_page_swap("Welcome", "Welcome"), render_page_swap("Welcome", "Welcome"),
SafeHtml::trusted(notifications::lower(include_str!("../templates/notifications.heml"))), notifications::static_fragment(include_str!("../templates/notifications.heml")),
]) ])
} }
+6
View File
@@ -525,6 +525,9 @@ impl Resources {
out.push_str(&format!("__slhx_lower_html(html.as_ref(), &{table_name})\n")); 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}}}\n\n"));
out.push_str(&format!("{pad}pub fn lower_html(html: impl ::std::convert::AsRef<str>) -> ::std::string::String {{ lower(html) }}\n\n")); out.push_str(&format!("{pad}pub fn lower_html(html: impl ::std::convert::AsRef<str>) -> ::std::string::String {{ lower(html) }}\n\n"));
out.push_str(&format!("{pad}pub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml {{\n"));
out.push_str(&format!("{pad} ::slhx::SafeHtml::trusted(lower(html))\n"));
out.push_str(&format!("{pad}}}\n\n"));
out.push_str(&format!("{pad}pub fn render(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} 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} ::hemplate::Hemplate::render_into(view, &mut html).expect(\"slhx hemplate view renders\");\n"));
@@ -1301,6 +1304,7 @@ mod tests {
assert!(generated.contains("pub const ALL_IDS")); assert!(generated.contains("pub const ALL_IDS"));
assert!(generated.contains("pub fn lower(html: impl ::std::convert::AsRef<str>) -> ::std::string::String")); assert!(generated.contains("pub fn lower(html: impl ::std::convert::AsRef<str>) -> ::std::string::String"));
assert!(generated.contains("pub fn lower_html")); assert!(generated.contains("pub fn lower_html"));
assert!(generated.contains("pub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml"));
assert!(generated.contains("pub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml")); 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("pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
assert!(generated.contains("data-slhx-slot")); assert!(generated.contains("data-slhx-slot"));
@@ -1369,12 +1373,14 @@ mod tests {
assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod slots")); assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod slots"));
assert!(generated.contains("\npub fn lower(html: impl ::std::convert::AsRef<str>) -> ::std::string::String")); assert!(generated.contains("\npub fn lower(html: impl ::std::convert::AsRef<str>) -> ::std::string::String"));
assert!(generated.contains("\npub fn lower_html")); assert!(generated.contains("\npub fn lower_html"));
assert!(generated.contains("\npub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml"));
assert!(generated.contains("\npub fn render(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml")); 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("\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::slhx::SafeHtml"));
assert!(generated.contains("pub mod todo")); assert!(generated.contains("pub mod todo"));
assert!(generated.contains(" pub const COMPONENT: ::slhx::ComponentRef = ::slhx::ComponentRef::new(\"todo\")")); assert!(generated.contains(" pub const COMPONENT: ::slhx::ComponentRef = ::slhx::ComponentRef::new(\"todo\")"));
assert!(generated.contains(" pub mod slots")); assert!(generated.contains(" pub mod slots"));
assert!(generated.contains(" pub fn lower_html")); assert!(generated.contains(" pub fn lower_html"));
assert!(generated.contains(" pub fn static_fragment(html: &'static str) -> ::slhx::SafeHtml"));
let _ = std::fs::remove_dir_all(&base); let _ = std::fs::remove_dir_all(&base);
} }