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