rumtk_web/utils/render.rs
1/*
2 * rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 * This toolkit aims to be reliable, simple, performant, and standards compliant.
4 * Copyright (C) 2025 Luis M. Santos, M.D. <lsantos@medicalmasses.com>
5 * Copyright (C) 2025 Ethan Dixon
6 * Copyright (C) 2025 MedicalMasses L.L.C. <contact@medicalmasses.com>
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21use crate::types::HTMLResult;
22use crate::{RUMWebData, RUMWebRedirect, RUMWebTemplate};
23use pulldown_cmark::{Options, Parser};
24use rumtk_core::strings::{
25 rumtk_format, AsStr, GraphemePattern, GraphemePatternPair, RUMString, RUMStringConversions,
26};
27use std::sync::OnceLock;
28
29pub static MARKDOWN_OPTIONS: OnceLock<Options> = OnceLock::new();
30
31pub static MARKDOWN_OPTIONS_INIT: fn() -> Options = || -> Options {
32 let mut options = Options::empty();
33
34 options.insert(Options::ENABLE_STRIKETHROUGH);
35 options.insert(Options::ENABLE_TASKLISTS);
36 options.insert(Options::ENABLE_MATH);
37 options.insert(Options::ENABLE_TABLES);
38 options.insert(Options::ENABLE_WIKILINKS);
39
40 options
41};
42
43const TEMPLATE_NEWLINE_COMPONENT_PATTERN: GraphemePatternPair<'static> = (&["<"], &[">"]);
44const TEMPLATE_NEWLINE_COMPONENT_INNER_PATTERN: GraphemePatternPair<'static> =
45 (&[">", "\n"], &["<"]);
46
47#[derive(RUMWebTemplate)]
48#[template(
49 source = "
50 {% for element in elements %}
51 {{ element|safe }}
52 {% endfor %}
53 ",
54 ext = "html"
55)]
56struct ContentBlock<'a> {
57 elements: &'a [RUMString],
58}
59
60///
61/// This function trims excess newlines and whitespacing outside tag block (e.g. `<div></div>`). The
62/// idea is to cleanup the rendered template which picks up extra characters due to the way string
63/// literals work in proc macros.
64///
65/// This is not meant to be used as a sanitization function!
66///
67/// This function consumes the input string!!!!!
68///
69/// ## Example
70/// ```
71/// use rumtk_web::rumtk_web_trim_rendered_html;
72/// use rumtk_web::testdata::{TRIMMED_HTML_RENDER, UNTRIMMED_HTML_RENDER};
73///
74/// let expected = String::from(TRIMMED_HTML_RENDER);
75/// let input = String::from(UNTRIMMED_HTML_RENDER);
76/// let filtered = rumtk_web_trim_rendered_html(input);
77///
78/// assert_eq!(filtered, expected, "Template render trim failed!");
79/// ```
80///
81pub fn rumtk_web_trim_rendered_html(html: String) -> String {
82 html.as_grapheme_str()
83 .trim(&TEMPLATE_NEWLINE_COMPONENT_PATTERN)
84 .splice(&TEMPLATE_NEWLINE_COMPONENT_INNER_PATTERN)
85 .trim(&TEMPLATE_NEWLINE_COMPONENT_PATTERN)
86 .to_string()
87}
88
89///
90/// Render the given component template into an `HTML Body response` or a `URL Redirect response`.
91/// If you provide the [RUMWebRedirect] in the `url` parameter configured for redirection, then we
92/// return the redirection as the response. Otherwise, we render the HTML and save it in the response.
93///
94/// ## Example
95/// ```
96/// use rumtk_web::{HTMLBody, RUMString, RUMWebRedirect, RUMWebResponse};
97/// use rumtk_web::RUMWebTemplate;
98/// use rumtk_web::rumtk_web_render_html;
99///
100/// #[derive(RUMWebTemplate)]
101/// #[template(
102/// source = "<div></div>",
103/// ext = "html"
104/// )]
105/// struct Div { }
106///
107/// let result = rumtk_web_render_html(Div{}, RUMWebRedirect::None).unwrap();
108/// let expected = RUMWebResponse::into_get_response("<div></div>");
109///
110/// assert_eq!(result, expected, "Test Div template rendered improperly!");
111/// ```
112///
113pub fn rumtk_web_render_html<T: RUMWebTemplate>(template: T, url: RUMWebRedirect) -> HTMLResult {
114 let result = template.render();
115 match result {
116 Ok(html) => {
117 let filtered = rumtk_web_trim_rendered_html(html);
118 Ok(url.into_web_response(Some(filtered)))
119 }
120 Err(e) => {
121 let tn = std::any::type_name::<T>();
122 Err(rumtk_format!("Template {tn} render failed: {e:?}"))
123 }
124 }
125}
126
127pub fn rumtk_web_render_contents(elements: &[RUMString]) -> HTMLResult {
128 rumtk_web_render_html(ContentBlock { elements }, RUMWebRedirect::None)
129}
130
131pub fn rumtk_web_redirect(url: RUMWebRedirect) -> HTMLResult {
132 Ok(url.into_web_response(Some(String::default())))
133}
134
135///
136/// Render component into an HTML Response Body of type [HTMLResult]. This macro is a bit more complex.
137/// Depending on the arguments passed to it, it can
138///
139/// 1. Call a component function that receives exactly 0 parameters.
140/// 2. Call a component function that only receives the [SharedAppState](crate::utils::SharedAppState) handle as its only parameter.
141/// 3. Call a component function that can accept the standard set of parameters (`path`, `params`, and `app_state`). However, the Path is set to empty.
142/// 4. Call a component function that can accept the standard set of parameters (`path`, `params`, and `app_state`). All of these parameters are passed through to the function.
143///
144/// The reason for this set of behaviors is that we have standard component functions which are found in [components](crate::components) modules.
145/// These functions are of type [ComponentFunction](crate::utils::ComponentFunction) and the expected parameters are as follows:
146///
147/// 1. `path` => [URLPath](crate::utils::URLPath)
148/// 2. `params` => [URLParams](crate::utils::URLParams)
149/// 3. `app_state` => [SharedAppState](crate::utils::SharedAppState)
150///
151/// The component functions are the bread and butter of the framework and are what are expected from consumers of
152/// this library. They get registered to an internal `Map` that we use as a sort of `vTable` to dispatch the correct user function.
153/// **In this case, the component function parameter for this macro is a stringview type since we perform the lookup automatically!**
154///
155/// The reason for the other usages is that we also have static components whose only purpose are to define
156/// pre-selected items to help make web apps come together in an easy to use package. These include the
157/// `htmx` and `fontawesome` imports. Perhaps, we will open up this facility to the user in later iterations of the framework
158/// to make it easy to override and include other static assets and maybe for prefetch and optimization purposes.
159///
160/// ## Examples
161///
162/// ### Simple Component Render
163/// ```
164/// use rumtk_web::static_components::css::css;
165/// use rumtk_web::rumtk_web_render_component;
166///
167/// let rendered = rumtk_web_render_component!(css);
168/// let expected = "<link rel='stylesheet' href='/static/css/bundle.min.css' onerror='this.onerror=null;this.href='/static/css/bundle.css';' />";
169///
170/// assert_eq!(rendered, expected, "Commponent rendered improperly!");
171/// ```
172///
173/// ### Component Render with Shared State
174/// ```
175/// use rumtk_web::SharedAppState;
176/// use rumtk_web::static_components::meta::meta;
177/// use rumtk_web::utils::testdata::TRIMMED_HTML_RENDER_META;
178/// use rumtk_web::rumtk_web_render_component;
179///
180/// let state = SharedAppState::default();
181/// let rendered = rumtk_web_render_component!(meta, state);
182///
183/// assert_eq!(rendered, TRIMMED_HTML_RENDER_META, "Commponent rendered improperly!");
184/// ```
185///
186/// ### Component Render with Standard Parameters
187/// ```
188/// use rumtk_web::SharedAppState;
189/// use rumtk_web::defaults::PARAMS_TITLE;
190/// use rumtk_web::utils::testdata::TRIMMED_HTML_TITLE_RENDER;
191/// use rumtk_web::{rumtk_web_render_component, rumtk_web_init_components};
192///
193/// rumtk_web_init_components!(None);
194/// let params = [
195/// (PARAMS_TITLE, "Hello World!")
196/// ];
197/// let state = SharedAppState::default();
198/// let rendered = rumtk_web_render_component!("title", params, state).unwrap().to_rumstring();
199///
200/// assert_eq!(rendered, TRIMMED_HTML_TITLE_RENDER, "Commponent rendered improperly!");
201/// ```
202///
203#[macro_export]
204macro_rules! rumtk_web_render_component {
205 ( $component_fxn:expr ) => {{
206 use rumtk_core::strings::{RUMString, RUMStringConversions};
207 match $component_fxn() {
208 Ok(x) => x.to_rumstring(),
209 _ => RUMString::default(),
210 }
211 }};
212 ( $component_fxn:expr, $app_state:expr ) => {{
213 use rumtk_core::strings::{RUMString, RUMStringConversions};
214 match $component_fxn($app_state.clone()) {
215 Ok(x) => x.to_rumstring(),
216 _ => RUMString::default(),
217 }
218 }};
219 ( $component:expr, $params:expr, $app_state:expr ) => {{
220 rumtk_web_render_component!($component, &[""], $params, $app_state)
221 }};
222 ( $component:expr, $path:expr, $params:expr, $app_state:expr ) => {{
223 use $crate::components::div::div;
224 use $crate::{rumtk_web_get_component, rumtk_web_params_map};
225
226 let params = rumtk_web_params_map!(&$params);
227
228 match rumtk_web_get_component!($component) {
229 Some(component) => component($path, params.get_inner(), $app_state.clone()),
230 // This is tricky, but I could not decide if the correct option here was to pass an
231 // message or default to a blank div. I chose the div, but if something changes, feel
232 // free to reconsider.
233 None => div($path, params.get_inner(), $app_state.clone())
234 }
235 }};
236}
237
238#[macro_export]
239macro_rules! rumtk_web_render_html {
240 ( $page:expr ) => {{
241 use $crate::utils::{rumtk_web_render_html, RUMWebRedirect};
242
243 rumtk_web_render_html($page, RUMWebRedirect::None)
244 }};
245 ( $page:expr, $redirect_url:expr ) => {{
246 use $crate::utils::rumtk_web_render_html;
247
248 rumtk_web_render_html($page, $redirect_url)
249 }};
250}
251
252///
253/// Generates the HTML page as prescribed by the input `page` function of type [HTMLResult].
254///
255/// ## Example
256/// ```
257/// use rumtk_core::strings::RUMString;
258/// use rumtk_web::pages::index::index;
259/// use rumtk_web::{rumtk_web_render_component, rumtk_web_render_page_contents, SharedAppState};
260///
261/// let app_state = SharedAppState::default();
262/// let mydiv = rumtk_web_render_component!("div", [("type", "story")], app_state).unwrap().to_rumstring();
263///
264/// let expected_page = RUMString::new("<div class='div-default'>default</div>");
265/// let page_response = rumtk_web_render_page_contents!(
266/// &vec![
267/// mydiv
268/// ]
269/// ).expect("Page rendered!");
270/// let rendered_page = page_response.to_rumstring();
271///
272/// assert_eq!(rendered_page, expected_page, "Page was not rendered properly!")
273/// ```
274///
275#[macro_export]
276macro_rules! rumtk_web_render_page_contents {
277 ( $page_elements:expr ) => {{
278 use $crate::utils::rumtk_web_render_contents;
279
280 rumtk_web_render_contents($page_elements)
281 }};
282}
283
284///
285/// Generate redirect response automatically instead of actually rendering an HTML page.
286///
287/// ## Examples
288///
289/// ### Temporary Redirect
290/// ```
291/// use rumtk_web::RUMStringConversions;
292/// use rumtk_web::utils::response::RUMWebRedirect;
293/// use rumtk_web::rumtk_web_render_redirect;
294///
295/// let url = "http://localhost/redirected";
296/// let redirect = rumtk_web_render_redirect!(RUMWebRedirect::RedirectTemporary(url.to_rumstring()));
297///
298/// let result = redirect.expect("Failed to create the redirect response!").get_url();
299///
300/// assert_eq!(result, url, "Url in Response object does not match the expected!");
301///
302/// ```
303///
304#[macro_export]
305macro_rules! rumtk_web_render_redirect {
306 ( $url:expr ) => {{
307 use $crate::utils::rumtk_web_redirect;
308
309 rumtk_web_redirect($url)
310 }};
311}
312
313///
314///
315/// If using raw strings, do not leave an extra line. The first input must have characters, or you
316/// will get <pre><code> blocks regardless of what you do.
317///
318/// ## Example
319/// ```
320/// use rumtk_web::rumtk_web_render_markdown;
321///
322/// let md = r###"
323///**Hello World**
324/// "###;
325/// let expected_html = "<p><strong>Hello World</strong></p>\n";
326///
327/// let result = rumtk_web_render_markdown!(md);
328///
329/// assert_eq!(result, expected_html, "The rendered markdown does not match the expected HTML!");
330/// ```
331///
332#[macro_export]
333macro_rules! rumtk_web_render_markdown {
334 ( $md:expr ) => {{
335 use pulldown_cmark::{Options, Parser};
336 use rumtk_core::strings::RUMStringConversions;
337 use $crate::utils::render::{MARKDOWN_OPTIONS, MARKDOWN_OPTIONS_INIT};
338
339 let mut options = MARKDOWN_OPTIONS.get_or_init(MARKDOWN_OPTIONS_INIT);
340
341 let input = String::from($md);
342 let parser = Parser::new_ext(&input, *options);
343 let mut html_output = String::new();
344 pulldown_cmark::html::push_html(&mut html_output, parser);
345
346 html_output.to_rumstring()
347 }};
348}