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:
+12
-1
@@ -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]
|
||||
|
||||
@@ -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>")
|
||||
|
||||
Reference in New Issue
Block a user