diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 4c313ce..33b2b53 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -561,7 +561,7 @@ what a valid business email is. 001 Raw HTML insertion requires an explicit safe HTML type (`SafeHtml` or equivalent). Plain `String` renders as escaped text unless explicitly wrapped. ### req: html/002 -002 Hemplate-rendered output may be converted to `SafeHtml` by trusted render APIs. User input is never `SafeHtml` by default. Full-page shell composition may pass already-rendered hemplate fragments through explicit `SafeHtml` fields; handlers should prefer slot/resource render helpers for effect payloads. +002 Hemplate-rendered output may be converted to `SafeHtml` by trusted render APIs. User input is never `SafeHtml` by default. Full-page shell composition may pass already-rendered hemplate fragments through explicit `SafeHtml` fields, and already-safe fragments may be joined without downgrading to `String`; handlers should prefer slot/resource render helpers for effect payloads. ### req: html/003 003 Slot render commands distinguish text payloads from HTML payloads at the type level. diff --git a/examples/v0/src/main.rs b/examples/v0/src/main.rs index 040ba0d..9200641 100644 --- a/examples/v0/src/main.rs +++ b/examples/v0/src/main.rs @@ -182,17 +182,14 @@ fn registry(state: Arc) -> HandlerRegistry { fn all_examples() -> SafeHtml { // Static `.heml` fragments are lowered by generated code before they join rendered views. // req: html_safety/001 req: html_safety/002 req: component/003 - SafeHtml::trusted( - [ - counter::lower(include_str!("../templates/counter.heml")), - todos::lower(include_str!("../templates/todos.heml")), - wizard::lower(include_str!("../templates/wizard.heml")), - auth::lower(include_str!("../templates/auth.heml")), - render_page_swap("Welcome", "Welcome").into_string(), - notifications::lower(include_str!("../templates/notifications.heml")), - ] - .join("\n"), - ) + SafeHtml::join([ + SafeHtml::trusted(counter::lower(include_str!("../templates/counter.heml"))), + SafeHtml::trusted(todos::lower(include_str!("../templates/todos.heml"))), + SafeHtml::trusted(wizard::lower(include_str!("../templates/wizard.heml"))), + SafeHtml::trusted(auth::lower(include_str!("../templates/auth.heml"))), + render_page_swap("Welcome", "Welcome"), + SafeHtml::trusted(notifications::lower(include_str!("../templates/notifications.heml"))), + ]) } fn shell(body: SafeHtml) -> SafeHtml { diff --git a/slhx-core/src/lib.rs b/slhx-core/src/lib.rs index 37650c0..5cdb496 100644 --- a/slhx-core/src/lib.rs +++ b/slhx-core/src/lib.rs @@ -141,6 +141,20 @@ impl SafeHtml { pub fn as_str(&self) -> &str { &self.0 } + + pub fn join(fragments: impl IntoIterator) -> Self { + fragments.into_iter().collect() + } +} + +impl FromIterator for SafeHtml { + fn from_iter>(iter: T) -> Self { + let mut html = String::new(); + for fragment in iter { + html.push_str(fragment.as_str()); + } + Self(html) + } } impl AsRef for SafeHtml { diff --git a/slhx-core/tests/effect_batch.rs b/slhx-core/tests/effect_batch.rs index d5030d1..3e386b9 100644 --- a/slhx-core/tests/effect_batch.rs +++ b/slhx-core/tests/effect_batch.rs @@ -93,6 +93,18 @@ fn slot_html_requires_explicit_safe_html() { assert_eq!(payload, Payload::Html(String::from("ok"))); } +#[test] +fn safe_html_joins_only_explicit_safe_fragments() { + // req: html_safety/001 req: html_safety/002 + let html = SafeHtml::join([ + SafeHtml::trusted("
"), + SafeHtml::trusted("ok"), + SafeHtml::trusted("
"), + ]); + + assert_eq!(html.as_str(), "
ok
"); +} + #[test] fn param_names_format_generated_param_names() { // req: codegen/003