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