perseus/template/core/
renderers.rs

1use super::utils::PreloadInfo;
2use crate::errors::*;
3#[cfg(engine)]
4use crate::i18n::Translator;
5use crate::path::PathMaybeWithLocale;
6#[cfg(engine)]
7use crate::reactor::Reactor;
8#[cfg(engine)]
9use crate::reactor::RenderMode;
10use crate::state::TemplateState;
11#[cfg(engine)]
12use crate::state::{BuildPaths, StateGeneratorInfo, UnknownStateType};
13#[cfg(engine)]
14use crate::template::default_headers;
15use crate::template::TemplateInner;
16#[cfg(engine)]
17use crate::Request;
18#[cfg(engine)]
19use http::HeaderMap;
20#[cfg(any(client, doc))]
21use sycamore::prelude::ScopeDisposer;
22use sycamore::web::Html;
23#[cfg(engine)]
24use sycamore::web::SsrNode;
25use sycamore::{prelude::Scope, view::View};
26
27impl<G: Html> TemplateInner<G> {
28    /// Executes the user-given function that renders the template on the
29    /// client-side ONLY. This takes in an existing global state.
30    ///
31    /// This should NOT be used to render widgets!
32    #[cfg(any(client, doc))]
33    #[allow(clippy::too_many_arguments)]
34    pub(crate) fn render_for_template_client<'a>(
35        &self,
36        path: PathMaybeWithLocale,
37        state: TemplateState,
38        cx: Scope<'a>,
39    ) -> Result<(View<G>, ScopeDisposer<'a>), ClientError> {
40        assert!(
41            !self.is_capsule,
42            "tried to render capsule with template logic"
43        );
44
45        // Only widgets use the preload info
46        (self.view)(
47            cx,
48            PreloadInfo {
49                locale: String::new(),
50                was_incremental_match: false,
51            },
52            state,
53            path,
54        )
55    }
56    /// Executes the user-given function that renders the template on the
57    /// server-side ONLY. This automatically initializes an isolated global
58    /// state.
59    #[cfg(engine)]
60    pub(crate) fn render_for_template_server(
61        &self,
62        path: PathMaybeWithLocale,
63        state: TemplateState,
64        global_state: TemplateState,
65        mode: RenderMode<SsrNode>,
66        cx: Scope,
67        translator: &Translator,
68    ) -> Result<View<G>, ClientError> {
69        assert!(
70            !self.is_capsule,
71            "tried to render capsule with template logic"
72        );
73
74        // The context we have here has no context elements set on it, so we set all the
75        // defaults (job of the router component on the client-side)
76        // We don't need the value, we just want the context instantiations
77        Reactor::engine(global_state, mode, Some(translator)).add_self_to_cx(cx);
78        // This is used for widget preloading, which doesn't occur on the engine-side
79        let preload_info = PreloadInfo {};
80        // We don't care about the scope disposer, since this scope is unique anyway
81        let (view, _) = (self.view)(cx, preload_info, state, path)?;
82        Ok(view)
83    }
84    /// Executes the user-given function that renders the document `<head>`,
85    /// returning a string to be interpolated manually. Reactivity in this
86    /// function will not take effect due to this string rendering. Note that
87    /// this function will provide a translator context.
88    #[cfg(engine)]
89    pub(crate) fn render_head_str(
90        &self,
91        state: TemplateState,
92        global_state: TemplateState,
93        translator: &Translator,
94    ) -> Result<String, ServerError> {
95        use sycamore::{
96            prelude::create_scope_immediate, utils::hydrate::with_no_hydration_context,
97        };
98
99        // This is a bit roundabout for error handling
100        let mut prerender_view = Ok(View::empty());
101        create_scope_immediate(|cx| {
102            // The context we have here has no context elements set on it, so we set all the
103            // defaults (job of the router component on the client-side)
104            // We don't need the value, we just want the context instantiations
105            // We don't need any page state store here
106            Reactor::<G>::engine(global_state, RenderMode::Head, Some(translator))
107                .add_self_to_cx(cx);
108
109            prerender_view = with_no_hydration_context(|| {
110                if let Some(head_fn) = &self.head {
111                    (head_fn)(cx, state)
112                } else {
113                    Ok(View::empty())
114                }
115            });
116        });
117        let prerender_view = prerender_view?;
118        let prerendered = sycamore::render_to_string(|_| prerender_view);
119
120        Ok(prerendered)
121    }
122    /// Gets the list of templates that should be prerendered for at build-time.
123    #[cfg(engine)]
124    pub(crate) async fn get_build_paths(&self) -> Result<BuildPaths, ServerError> {
125        if let Some(get_build_paths) = &self.get_build_paths {
126            get_build_paths.call().await
127        } else {
128            Err(BuildError::TemplateFeatureNotEnabled {
129                template_name: self.path.clone(),
130                feature_name: "build_paths".to_string(),
131            }
132            .into())
133        }
134    }
135    /// Gets the initial state for a template. This needs to be passed the full
136    /// path of the template, which may be one of those generated by
137    /// `.get_build_paths()`. This also needs the locale being rendered to so
138    /// that more complex applications like custom documentation systems can
139    /// be enabled.
140    #[cfg(engine)]
141    pub(crate) async fn get_build_state(
142        &self,
143        info: StateGeneratorInfo<UnknownStateType>,
144    ) -> Result<TemplateState, ServerError> {
145        if let Some(get_build_state) = &self.get_build_state {
146            get_build_state.call(info).await
147        } else {
148            Err(BuildError::TemplateFeatureNotEnabled {
149                template_name: self.path.clone(),
150                feature_name: "build_state".to_string(),
151            }
152            .into())
153        }
154    }
155    /// Gets the request-time state for a template. This is equivalent to SSR,
156    /// and will not be performed at build-time. Unlike `.get_build_paths()`
157    /// though, this will be passed information about the request that triggered
158    /// the render. Errors here can be caused by either the server or the
159    /// client, so the user must specify an [`ErrorBlame`]. This is also passed
160    /// the locale being rendered to.
161    #[cfg(engine)]
162    pub(crate) async fn get_request_state(
163        &self,
164        info: StateGeneratorInfo<UnknownStateType>,
165        req: Request,
166    ) -> Result<TemplateState, ServerError> {
167        if let Some(get_request_state) = &self.get_request_state {
168            get_request_state.call(info, req).await
169        } else {
170            Err(BuildError::TemplateFeatureNotEnabled {
171                template_name: self.path.clone(),
172                feature_name: "request_state".to_string(),
173            }
174            .into())
175        }
176    }
177    /// Amalgamates given request and build states. Errors here can be caused by
178    /// either the server or the client, so the user must specify
179    /// an [`ErrorBlame`].
180    ///
181    /// This takes a separate build state and request state to ensure there are
182    /// no `None`s for either of the states. This will only be called if both
183    /// states are generated.
184    #[cfg(engine)]
185    pub(crate) async fn amalgamate_states(
186        &self,
187        info: StateGeneratorInfo<UnknownStateType>,
188        build_state: TemplateState,
189        request_state: TemplateState,
190    ) -> Result<TemplateState, ServerError> {
191        if let Some(amalgamate_states) = &self.amalgamate_states {
192            amalgamate_states
193                .call(info, build_state, request_state)
194                .await
195        } else {
196            Err(BuildError::TemplateFeatureNotEnabled {
197                template_name: self.path.clone(),
198                feature_name: "amalgamate_states".to_string(),
199            }
200            .into())
201        }
202    }
203    /// Checks, by the user's custom logic, if this template should revalidate.
204    /// This function isn't presently parsed anything, but has
205    /// network access etc., and can really do whatever it likes. Errors here
206    /// can be caused by either the server or the client, so the
207    /// user must specify an [`ErrorBlame`].
208    #[cfg(engine)]
209    pub(crate) async fn should_revalidate(
210        &self,
211        info: StateGeneratorInfo<UnknownStateType>,
212        req: Request,
213    ) -> Result<bool, ServerError> {
214        if let Some(should_revalidate) = &self.should_revalidate {
215            should_revalidate.call(info, req).await
216        } else {
217            Err(BuildError::TemplateFeatureNotEnabled {
218                template_name: self.path.clone(),
219                feature_name: "should_revalidate".to_string(),
220            }
221            .into())
222        }
223    }
224    /// Gets the template's headers for the given state. These will be inserted
225    /// into any successful HTTP responses for this template, and they have
226    /// the power to override existing headers, including `Content-Type`.
227    ///
228    /// This will automatically instantiate a scope and set up an engine-side
229    /// reactor so that the user's function can access global state and
230    /// translations, as localized headers are very much real. Locale
231    /// detection pages are considered internal to Perseus, and therefore do
232    /// not have support for user headers (at this time).
233    #[cfg(engine)]
234    pub(crate) fn get_headers(
235        &self,
236        state: TemplateState,
237        global_state: TemplateState,
238        translator: Option<&Translator>,
239    ) -> Result<HeaderMap, ServerError> {
240        use sycamore::prelude::create_scope_immediate;
241
242        let mut res = Ok(HeaderMap::new());
243        create_scope_immediate(|cx| {
244            let reactor = Reactor::<G>::engine(global_state, RenderMode::Headers, translator);
245            reactor.add_self_to_cx(cx);
246
247            if let Some(header_fn) = &self.set_headers {
248                res = (header_fn)(cx, state);
249            } else {
250                res = Ok(default_headers());
251            }
252        });
253
254        res
255    }
256}