Skip to main content

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::{RUMWebRedirect, RUMWebTemplate};
23use pulldown_cmark::Options;
24use rumtk_core::base::RUMResult;
25use rumtk_core::search::rumtk_search::string_replace_all_matches;
26use rumtk_core::strings::{
27    rumtk_format, AsStr, GraphemePatternPair, RUMString,
28};
29use std::sync::LazyLock;
30
31pub static MARKDOWN_OPTIONS: LazyLock<Options> = LazyLock::new(|| -> 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);
43
44const TEMPLATE_NEWLINE_COMPONENT_PATTERN: GraphemePatternPair<'static> = (&["<"], &[">"]);
45const TEMPLATE_NEWLINE_COMPONENT_INNER_PATTERN: GraphemePatternPair<'static> =
46    (&[">", "\n"], &["<"]);
47const TEMPLATE_MIDDLE_REGEX: &str = ">\\s+<";
48const TEMPLATE_MIDDLE_REPLACEMENT: &str = "><";
49
50#[derive(RUMWebTemplate)]
51#[template(
52    source = "
53        {% for element in elements %}
54           {{ element|safe }}
55        {% endfor %}
56    ",
57    ext = "html"
58)]
59struct ContentBlock<'a> {
60    elements: &'a [RUMString],
61}
62
63///
64/// This function trims excess newlines and whitespacing outside tag block (e.g. `<div></div>`). The
65/// idea is to cleanup the rendered template which picks up extra characters due to the way string
66/// literals work in proc macros.
67///
68/// This is not meant to be used as a sanitization function!
69///
70/// This function consumes the input string!!!!!
71///
72/// ## Example
73/// ```
74/// use rumtk_web::rumtk_web_trim_rendered_html;
75/// use rumtk_web::testdata::data::{TRIMMED_HTML_RENDER, UNTRIMMED_HTML_RENDER};
76///
77/// let expected = String::from(TRIMMED_HTML_RENDER);
78/// let input = String::from(UNTRIMMED_HTML_RENDER);
79/// let filtered = rumtk_web_trim_rendered_html(input).unwrap();
80///
81/// assert_eq!(filtered, expected, "Template render trim failed!");
82/// ```
83///
84#[inline]
85pub fn rumtk_web_trim_rendered_html(html: String) -> RUMResult<String> {
86    let filtered = html.as_grapheme_str()
87        .trim(&TEMPLATE_NEWLINE_COMPONENT_PATTERN)
88        .trim(&TEMPLATE_NEWLINE_COMPONENT_PATTERN)
89        .to_string();
90    string_replace_all_matches(filtered.as_str(), TEMPLATE_MIDDLE_REGEX, TEMPLATE_MIDDLE_REPLACEMENT)
91}
92
93#[inline]
94pub fn rumtk_web_post_process(html: String, url: RUMWebRedirect) -> HTMLResult {
95    let filtered = rumtk_web_trim_rendered_html(html)?;
96    Ok(url.into_web_response(Some(filtered)))
97}
98
99///
100/// Render the given component template into an `HTML Body response` or a `URL Redirect response`.
101/// If you provide the [RUMWebRedirect] in the `url` parameter configured for redirection, then we
102/// return the redirection as the response. Otherwise, we render the HTML and save it in the response.
103///
104/// ## Example
105/// ```
106/// use rumtk_web::{HTMLBody, RUMString, RUMWebRedirect, RUMWebResponse};
107/// use rumtk_web::RUMWebTemplate;
108/// use rumtk_web::rumtk_web_render;
109///
110/// #[derive(RUMWebTemplate)]
111/// #[template(
112///     source = "<div></div>",
113///     ext = "html"
114/// )]
115/// struct Div { }
116///
117/// let result = rumtk_web_render(Div{}, RUMWebRedirect::None).unwrap();
118/// let expected = RUMWebResponse::into_get_response("<div></div>");
119///
120/// assert_eq!(result, expected, "Test Div template rendered improperly!");
121/// ```
122///
123#[inline]
124pub fn rumtk_web_render<T: RUMWebTemplate>(template: T, url: RUMWebRedirect) -> HTMLResult {
125    let result = template.render();
126    match result {
127        Ok(html) => {
128            rumtk_web_post_process(html, url)
129        }
130        Err(e) => {
131            let tn = std::any::type_name::<T>();
132            Err(rumtk_format!("Template {tn} render failed: {e:?}"))
133        }
134    }
135}
136
137#[inline]
138pub fn rumtk_web_render_contents(elements: &[RUMString]) -> HTMLResult {
139    rumtk_web_render(ContentBlock { elements }, RUMWebRedirect::None)
140}
141
142#[inline]
143pub fn rumtk_web_redirect(url: RUMWebRedirect) -> HTMLResult {
144    Ok(url.into_web_response(Some(String::default())))
145}
146
147///
148/// Render component into an HTML Response Body of type [HTMLResult]. This macro is a bit more complex.
149/// Depending on the arguments passed to it, it can
150///
151/// 1. Call a component function that receives exactly 0 parameters.
152/// 2. Call a component function that only receives the [SharedAppState](crate::utils::SharedAppState) handle as its only parameter.
153/// 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.
154/// 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.
155///
156/// The reason for this set of behaviors is that we have standard component functions which are found in [components](crate::components) modules.
157/// These functions are of type [ComponentFunction](crate::utils::ComponentFunction) and the expected parameters are as follows:
158///
159/// 1. `path` => [URLPath](crate::utils::URLPath)
160/// 2. `params` => [URLParams](crate::utils::URLParams)
161/// 3. `app_state` => [SharedAppState](crate::utils::SharedAppState)
162///
163/// The component functions are the bread and butter of the framework and are what are expected from consumers of
164/// this library. They get registered to an internal `Map` that we use as a sort of `vTable` to dispatch the correct user function.
165/// **In this case, the component function parameter for this macro is a stringview type since we perform the lookup automatically!**
166///
167/// The reason for the other usages is that we also have static components whose only purpose are to define
168/// pre-selected items to help make web apps come together in an easy to use package. These include the
169/// `htmx` and `fontawesome` imports. Perhaps, we will open up this facility to the user in later iterations of the framework
170/// to make it easy to override and include other static assets and maybe for prefetch and optimization purposes.
171///
172/// ## Examples
173///
174/// ### Simple Component Render
175/// ```
176/// use rumtk_web::components::app::css::css;
177/// use rumtk_web::utils::testdata::data::TRIMMED_HTML_RENDER_CSS;
178/// use rumtk_web::rumtk_web_render_component;
179///
180/// let rendered = rumtk_web_render_component!(css);
181/// let expected = TRIMMED_HTML_RENDER_CSS;
182///
183/// assert_eq!(rendered, expected, "Commponent rendered improperly!");
184/// ```
185///
186/// ### Component Render with Shared State
187/// ```
188/// use rumtk_web::SharedAppState;
189/// use rumtk_web::components::app::meta::meta;
190/// use rumtk_web::utils::testdata::data::TRIMMED_HTML_RENDER_META;
191/// use rumtk_web::rumtk_web_render_component;
192///
193/// let state = SharedAppState::default();
194/// let rendered = rumtk_web_render_component!(meta, state);
195///
196/// assert_eq!(rendered, TRIMMED_HTML_RENDER_META, "Commponent rendered improperly!");
197/// ```
198///
199/// ### Component Render with Standard Parameters
200/// ```
201/// use rumtk_web::{rumtk_web_collect_page, rumtk_web_params_map, rumtk_web_trim_rendered_html, SharedAppState};
202/// use rumtk_web::defaults::PARAMS_TITLE;
203/// use rumtk_web::utils::testdata::data::TRIMMED_HTML_TITLE_RENDER;
204/// use rumtk_web::components::title::title;
205///
206/// let state = SharedAppState::default();
207/// let params = rumtk_web_params_map!([(PARAMS_TITLE, "Hello World!")]);
208/// let rendered = title(&[], params.get_inner(), state).unwrap().to_string();
209/// let trimmed = rumtk_web_trim_rendered_html(rendered).unwrap();
210///
211/// assert_eq!(&trimmed, TRIMMED_HTML_TITLE_RENDER, "Commponent rendered improperly!");
212/// ```
213///
214#[macro_export]
215macro_rules! rumtk_web_render_component {
216    ( $component_fxn:expr ) => {{
217        use $crate::render::rumtk_web_trim_rendered_html;
218        use rumtk_core::strings::{RUMString, RUMStringConversions};
219        match $component_fxn() {
220            Ok(x) => match rumtk_web_trim_rendered_html(x.to_string()) {
221                Ok(r) => r,
222                _ => RUMString::default()
223            },
224            _ => RUMString::default(),
225        }
226    }};
227    ( $component_fxn:expr, $app_state:expr ) => {{
228        use $crate::render::rumtk_web_trim_rendered_html;
229        use rumtk_core::strings::{RUMString, RUMStringConversions};
230        match $component_fxn($app_state.clone()) {
231            Ok(x) => match rumtk_web_trim_rendered_html(x.to_string()) {
232                Ok(r) => r,
233                _ => RUMString::default()
234            },
235            _ => RUMString::default(),
236        }
237    }};
238    ( $component:expr, $params:expr, $app_state:expr ) => {{
239        rumtk_web_render_component!($component, &[""], $params, $app_state)
240    }};
241    ( $component:expr, $path:expr, $params:expr, $app_state:expr ) => {{
242        use $crate::components::html::div;
243        use $crate::render::rumtk_web_trim_rendered_html;
244        use $crate::{rumtk_web_get_component, rumtk_web_params_map};
245
246        let params = rumtk_web_params_map!($params);
247
248        match rumtk_web_get_component!($component) {
249            Some(component) => match component($path, params.get_inner(), $app_state.clone()){
250                Ok(x) => match rumtk_web_trim_rendered_html(x.to_string()) {
251                    Ok(r) => r,
252                    _ => RUMString::default()
253                },
254                _ => RUMString::default(),
255            },
256                // This is tricky, but I could not decide if the correct option here was to pass an
257                // message or default to a blank div. I chose the div, but if something changes, feel
258                // free to reconsider.
259            None => div($path, params.get_inner(), $app_state.clone()) {
260                Ok(x) => match rumtk_web_trim_rendered_html(x.to_string()) {
261                    Ok(r) => r,
262                    _ => RUMString::default()
263                },
264                _ => RUMString::default(),
265            }
266        }
267    }};
268}
269
270#[macro_export]
271macro_rules! rumtk_web_render_template {
272    ( $page:expr ) => {{
273        use $crate::utils::{rumtk_web_render, RUMWebRedirect};
274
275        rumtk_web_render($page, RUMWebRedirect::None)
276    }};
277    ( $page:expr, $redirect_url:expr ) => {{
278        use $crate::utils::rumtk_web_render;
279
280        rumtk_web_render($page, $redirect_url)
281    }};
282}
283
284#[macro_export]
285macro_rules! rumtk_web_post_process_html {
286    ( $html:expr ) => {{
287        use rumtk_core::strings::{RUMStringConversions};
288        use $crate::utils::{rumtk_web_post_process, RUMWebRedirect};
289
290        rumtk_web_post_process($html.to_string(), RUMWebRedirect::None)
291    }};
292    ( $html:expr, $redirect_url:expr ) => {{
293        use rumtk_core::strings::{RUMStringConversions};
294        use $crate::utils::rumtk_web_post_process;
295
296        rumtk_web_post_process($html.to_string(), $redirect_url)
297    }};
298}
299
300///
301/// Generates the HTML page as prescribed by the input `page` function of type [HTMLResult].
302///
303/// ## Example
304/// ```
305/// use rumtk_core::strings::RUMString;
306/// use rumtk_web::defaults::{PARAMS_TYPE};
307/// use rumtk_web::pages::index::index;
308/// use rumtk_web::components::html::div;
309/// use rumtk_web::{rumtk_web_params_map, rumtk_web_render_component, rumtk_web_render_page_contents, SharedAppState};
310///
311/// let app_state = SharedAppState::default();
312/// let params = rumtk_web_params_map!([("", "")]);
313/// let mydiv = div("story", params.get_inner(), app_state).unwrap().to_string();
314///
315/// let expected_page = RUMString::from("<div class='div-default'>story</div>");
316/// let page_response = rumtk_web_render_page_contents!(
317///     &vec![
318///         mydiv
319///     ]
320/// ).expect("Page rendered!");
321/// let rendered_page = page_response.to_string();
322///
323/// assert_eq!(rendered_page, expected_page, "Page was not rendered properly!")
324/// ```
325///
326#[macro_export]
327macro_rules! rumtk_web_render_page_contents {
328    ( $page_elements:expr ) => {{
329        use $crate::utils::rumtk_web_render_contents;
330
331        rumtk_web_render_contents($page_elements)
332    }};
333}
334
335///
336/// Generate redirect response automatically instead of actually rendering an HTML page.
337///
338/// ## Examples
339///
340/// ### Temporary Redirect
341/// ```
342/// use rumtk_web::RUMStringConversions;
343/// use rumtk_web::utils::response::RUMWebRedirect;
344/// use rumtk_web::rumtk_web_render_redirect;
345///
346/// let url = "http://localhost/redirected";
347/// let redirect = rumtk_web_render_redirect!(RUMWebRedirect::RedirectTemporary(url.to_string()));
348///
349/// let result = redirect.expect("Failed to create the redirect response!").get_url();
350///
351/// assert_eq!(result, url, "Url in Response object does not match the expected!");
352///
353/// ```
354///
355#[macro_export]
356macro_rules! rumtk_web_render_redirect {
357    ( $url:expr ) => {{
358        use $crate::utils::rumtk_web_redirect;
359
360        rumtk_web_redirect($url)
361    }};
362}
363
364///
365///
366/// If using raw strings, do not leave an extra line. The first input must have characters, or you
367/// will get <pre><code> blocks regardless of what you do.
368///
369/// ## Example
370/// ```
371/// use rumtk_web::rumtk_web_render_markdown;
372///
373/// let md = r###"
374///**Hello World**
375/// "###;
376/// let expected_html = "<p><strong>Hello World</strong></p>\n";
377///
378/// let result = rumtk_web_render_markdown!(md);
379///
380/// assert_eq!(result, expected_html, "The rendered markdown does not match the expected HTML!");
381/// ```
382///
383#[macro_export]
384macro_rules! rumtk_web_render_markdown {
385    ( $md:expr ) => {{
386        use pulldown_cmark::{Options, Parser};
387        use rumtk_core::strings::RUMStringConversions;
388        use $crate::utils::render::{MARKDOWN_OPTIONS};
389
390        let input = String::from($md);
391        let parser = Parser::new_ext(&input, unsafe {(*MARKDOWN_OPTIONS).clone()});
392        let mut html_output = String::new();
393        pulldown_cmark::html::push_html(&mut html_output, parser);
394
395        html_output.to_string()
396    }};
397}