feat(axum): add safe html page composition

Add a SafeHtml PageRequest path for shell/partial composition and migrate examples so rendered hemplate fragments stay typed through the transport boundary instead of round-tripping through unchecked strings.

req: axum_integration/001

req: html_safety/001

req: html_safety/002
This commit is contained in:
slhx agent
2026-06-01 23:45:52 +02:00
parent a287d87e38
commit 4e47b470a7
6 changed files with 96 additions and 49 deletions
+12 -1
View File
@@ -5,7 +5,7 @@ use axum::http::{header, request::Parts, HeaderMap, HeaderValue, Request, Respon
use axum::response::sse::{Event, Sse};
use axum::response::IntoResponse;
use futures_util::{Stream, StreamExt};
use slhx_core::{BuildFingerprint, EffectBatch, Handle, IntoEffect};
use slhx_core::{BuildFingerprint, EffectBatch, Handle, IntoEffect, SafeHtml};
use std::collections::BTreeMap;
use std::convert::Infallible;
@@ -61,6 +61,17 @@ impl PageRequest {
PageMode::Partial => PageResponse::partial(partial_html),
}
}
pub fn page_html(
self,
partial_html: SafeHtml,
shell: impl FnOnce(SafeHtml) -> SafeHtml,
) -> PageResponse {
match self.mode {
PageMode::Full => PageResponse::full(shell(partial_html).into_string()),
PageMode::Partial => PageResponse::partial(partial_html.into_string()),
}
}
}
#[async_trait]
+41 -1
View File
@@ -6,7 +6,7 @@ use slhx_axum::{
InteractionFormRejection, PageMode, PageRequest, PageResponse, SLHX_CONTENT_TYPE,
SLHX_FINGERPRINT_HEADER, SLHX_PARTIAL_HEADER, SLHX_RUNTIME_CONTENT_TYPE, SLHX_TITLE_HEADER,
};
use slhx_core::{push, BuildFingerprint, Handle, Slot};
use slhx_core::{push, BuildFingerprint, Handle, SafeHtml, Slot};
fn selector(value: &str) -> Selector {
Selector::parse(value).expect("test selector parses")
@@ -47,6 +47,46 @@ fn page_request_wraps_full_pages_and_leaves_partials_unwrapped() {
assert_eq!(partial_fragment.select(&selector("body[data-shell=\"docs\"]")).count(), 0);
}
#[test]
fn page_request_wraps_safe_html_full_pages_and_leaves_partials_unwrapped() {
// req: axum_integration/001 req: html_safety/001 req: html_safety/002 req: test/005
let full = PageRequest {
mode: PageMode::Full,
}
.page_html(
SafeHtml::trusted("<main data-page=\"docs\">Docs</main>"),
|content| {
SafeHtml::trusted(format!(
"<html><body data-shell=\"docs\">{content}</body></html>"
))
},
);
assert_eq!(full.mode, PageMode::Full);
let full_document = Html::parse_document(&full.html);
assert_eq!(
full_document
.select(&selector("body[data-shell=\"docs\"] main[data-page=\"docs\"]"))
.count(),
1
);
let partial = PageRequest {
mode: PageMode::Partial,
}
.page_html(
SafeHtml::trusted("<main data-page=\"docs\">Docs</main>"),
|content| {
SafeHtml::trusted(format!(
"<html><body data-shell=\"docs\">{content}</body></html>"
))
},
);
assert_eq!(partial.mode, PageMode::Partial);
let partial_fragment = Html::parse_fragment(&partial.html);
assert_eq!(partial_fragment.select(&selector("main[data-page=\"docs\"]")).count(), 1);
assert_eq!(partial_fragment.select(&selector("body[data-shell=\"docs\"]")).count(), 0);
}
#[test]
fn partial_page_response_sets_partial_and_title_headers() {
let response = PageResponse::partial("<main>Docs</main>")