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 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
//
// shortcode module
//
use tera::{Result, Function};
use std::collections::HashMap;
use once_cell::sync::Lazy;
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| reqwest::Client::new());
// const ROBOTS_TXT: &'static str = "Link for Robots (No JavaScript)";
/// A struct that manages shortcode functions for use in Tera templates.
///
/// # Fields
///
/// - `functions`: A `HashMap` where the key is the shortcode display name (a `String`), and the value is a function pointer that takes a reference to a `HashMap` of arguments and returns a `String` representing the generated content.
pub struct Shortcodes {
pub functions: HashMap<String, fn(&HashMap<String, tera::Value>) -> String>,
}
impl Shortcodes {
/// Creates a new `Shortcodes` instance with an empty set of registered functions.
///
/// # Returns
///
/// A `Shortcodes` struct with an empty `functions` map.
pub fn new() -> Self {
Shortcodes {
functions: HashMap::new(),
}
}
/// Registers a new shortcode function in the `Shortcodes` struct.
///
/// # Parameters
///
/// - `display`: The shortcode display name as a `&str`, which will be used as the key in the `functions` map.
/// - `shortcode_fn`: A function pointer that takes a `HashMap` of arguments and returns a `String`.
///
/// # Returns
///
/// An updated instance of `Shortcodes` with the newly registered shortcode function.
///
/// # Example
///
/// ```rust
/// use tera_shortcodes::Shortcodes;
///
/// let shortcodes = Shortcodes::new().register("example", |args| {
/// "Shortcode output".to_string()
/// });
/// ```
pub fn register(mut self,
display: &str,
shortcode_fn: fn(&HashMap<String, tera::Value>) -> String,
) -> Self {
self.functions.insert(display.to_owned(), shortcode_fn);
self
}
}
impl Function for Shortcodes {
/// Invokes a registered shortcode function by its display name.
///
/// # Parameters
///
/// - `args`: A reference to a `HashMap` containing the arguments passed to the shortcode function.
///
/// # Returns
///
/// A `Result<tera::Value>` that contains the generated content as a `String` or an error message if the display name is missing or unknown.
///
/// # Error Handling
///
/// - If the `display` attribute is missing, it returns an error message `"Missing display attribute"`.
/// - If no function is registered for the given display name, it returns an error message `"Unknown shortcode display name: <display>"`.
fn call(&self,
args: &HashMap<String, tera::Value>,
) -> Result<tera::Value> {
let display = match args.get("display") {
Some(value) => value.as_str()
.unwrap()
.trim_matches(|c| c == '"' || c == '\''),
None => return Ok(tera::Value::String("Missing display attribute".to_owned())),
};
let fragment = match self.functions.get(display) {
Some(shortcode_fn) => shortcode_fn(args),
None => {
return Ok(tera::Value::String(format!("Unknown shortcode display name: {}", display)))
},
};
Ok(tera::Value::String(fragment))
}
}
/// Generates a JavaScript snippet that asynchronously fetches data from a URL using either the GET
/// or POST HTTP method and injects the response into the DOM. The function also provides fallback
/// content for crawlers/robots that do not support JavaScript. If the response has JavaScript code
/// like <script>console.log('test');</script>, it will be executable.
///
/// # Parameters
///
/// - `url`: A string slice containing the URL to which the HTTP request will be made.
/// - `method`: An optional HTTP method, either `GET` or `POST`. Defaults to `GET` if `None` is provided.
/// - `json_body`: An optional JSON string for the request body when using the `POST` method. Defaults to
/// an empty JSON object (`{}`) if `None` is provided. Ignored if the method is `GET`.
/// - `alt`: An optional alternative content to display in a `<noscript>` block for crawlers/robots without JavaScript.
/// This is only used if the method is `GET`. Defaults to `None`.
///
/// # Returns
///
/// A `String` containing the generated JavaScript code that can be inserted into an HTML page. The script:
/// - Sends an asynchronous `fetch` request to the specified URL.
/// - If the response is successful, it injects the response content into the DOM.
/// - If the request fails, it logs an error message to the browser's console.
/// - If an invalid HTTP method is passed (anything other than `GET` or `POST`), an HTML `<output>` element
/// with an error message is returned instead of the JavaScript code.
///
/// If the `GET` method is used and `alt` is provided, the function also includes a `<noscript>` fallback
/// to display a link in case JavaScript is disabled or not supported.
///
/// # Example
///
/// ```rust
/// use tera_shortcodes::fetch_shortcode_js;
///
/// let js_code = fetch_shortcode_js(
/// "https://example.com/data",
/// Some("POST"),
/// Some("{\"key\": \"value\"}"),
/// Some("No JavaScript fallback")
/// );
///
/// println!("{}", js_code);
/// ```
///
/// This will generate JavaScript code to make a `POST` request to `https://example.com/data` with the
/// provided JSON body, and include a fallback for users without JavaScript.
///
/// # Error Handling
///
/// - If an unsupported HTTP method is provided (anything other than `GET` or `POST`), the function will
/// return an HTML `<output>` element with an error message specifying the invalid method.
pub fn fetch_shortcode_js(
url: &str,
method: Option<&str>,
json_body: Option<&str>,
alt: Option<&str>,
) -> String {
let method = method.unwrap_or("GET");
let json_body = json_body.unwrap_or("{}");
let fetch_js = match method.to_lowercase().as_str() {
"get" => format!(r#"const response = await fetch("{}");"#, url),
"post" => format!(r#"
const request = new Request("{}", {{
headers: (() => {{
const headers = new Headers();
headers.append("Content-Type", "application/json");
return headers;
}})(),
method: "POST",
body: JSON.stringify({}),
}});
const response = await fetch(request);"#, url, json_body),
_ => return format!(r#"<output style="background-color:#f44336;color:#fff;padding:6px;">
Invalid method {} for url {} (only GET and POST methods available)
</output>"#, method, url),
};
// reScript function ia a trick to make the Javascript code work when inserted.
// Replace it with another clone element script.
let js_code = format!(r#"<script>
(function () {{
async function fetchShortcodeData() {{
try {{
{}
if (!response.ok) {{
throw new Error(`HTTP error! Status: ${{response.status}}`);
}}
return await response.text();
}} catch (error) {{
console.error("Fetch failed:", error);
return "";
}}
}}
function reScript(helper) {{
for (const node of helper.childNodes) {{
if (node.hasChildNodes()) {{
reScript(node);
}}
if (node.nodeName === 'SCRIPT') {{
const script = document.createElement('script');
script.type = "text/javascript";
script.textContent = node.textContent;
node.replaceWith(script);
}}
}}
}}
(async () => {{
const currentScript = document.currentScript;
const content = await fetchShortcodeData();
// console.log(content);
const helper = document.createElement('div');
helper.id = 'helper';
helper.innerHTML = content;
reScript(helper);
currentScript.after(...helper.childNodes);
currentScript.remove();
}})();
}})();
</script>"#,
fetch_js);
if method.to_lowercase().as_str() == "get" && alt.is_some() {
let alt = alt.unwrap();
js_code.to_string() + &format!(r#"<noscript><a href="{}">{}</a></noscript>"#, url, alt)
} else {
js_code
}
}
/// Sends an HTTP request to the provided URL using either the `GET` or `POST` method and returns the response as a String.
/// This function handles asynchronous requests but executes them in a synchronous context using Tokio function `block_in_place`.
/// Note: This function is slow. For better performance, consider using the fetch_shortcode_js function instead.
///
/// # Parameters
///
/// - `url`: A string slice that holds the URL to which the HTTP request will be sent.
/// - `method`: An optional HTTP method, either `GET` or `POST`. Defaults to `GET` if `None` is provided.
/// - `json_body`: An optional JSON string to be used as the request body for `POST` requests.
/// Defaults to an empty JSON object (`{}`) if `None` is provided. This parameter is ignored for `GET` requests.
///
/// # Returns
///
/// A `String` containing either the response body from the server or an error message in case
/// of failure.
/// - If the HTTP request succeeds and returns a valid response, the body of the response is returned as a `String`.
/// - If the HTTP request fails (due to network errors, invalid URLs, or server errors), a descriptive error message is returned.
///
/// # Error Handling
///
/// - If an invalid HTTP method is provided, the function returns `"Invalid method: <method>"`.
/// - If the request fails, either due to network issues or an unsuccessful HTTP status, the function returns
/// an error message like `"Request failed with status: <status>"` or `"Request error: <error>"`.
///
/// # Blocking and Asynchronous Execution
///
/// This function uses `tokio::task::block_in_place` to run the asynchronous request synchronously.
/// This allows the function to be used in synchronous contexts while still performing asynchronous
/// operations under the hood.
///
/// # Example
///
/// ```rust
/// use tera_shortcodes::fetch_shortcode;
///
/// #[tokio::main]
/// async fn main() {
/// let response = fetch_shortcode(
/// "https://example.com/api",
/// Some("POST"),
/// Some(r#"{"key": "value"}"#)
/// );
///
/// println!("Response: {}", response);
/// }
/// ```
///
/// This will perform a `POST` request to `https://example.com/api` with the given JSON body and print the response.
pub fn fetch_shortcode(
url: &str,
method: Option<&str>,
json_body: Option<&str>,
) -> String {
let method = method.unwrap_or("GET");
let json_body = json_body.unwrap_or("{}");
let data_to_route = async {
let response = match method.to_lowercase().as_str() {
"get" => CLIENT.get(url)
.send()
.await,
"post" => CLIENT.post(url)
.header("Content-Type", "application/json")
.body(json_body.to_owned())
.send()
.await,
_ => return format!("Invalid method: {}", method),
};
match response {
Ok(res) => {
if res.status().is_success() {
res.text().await.unwrap_or_else(|_| "Failed to read response body".into())
} else {
format!("Request failed with status: {}", res.status())
}
}
Err(e) => format!("Request error: {}", e),
}
};
// Use `block_in_place` to run the async function
// within the blocking context
tokio::task::block_in_place(||
// We need to access the current runtime to
// run the async function
tokio::runtime::Handle::current()
.block_on(data_to_route)
)
}