use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::{any::Any, sync::Arc};
use thiserror::Error;
use crate::expression::{TemplateObject, TemplateObjectMethodError, TemplateValue};
use crate::util::{NumberValue, Utf16String};
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TemplateMode {
HTML,
XML,
TEXT,
JAVASCRIPT,
CSS,
RAW,
}
impl TemplateMode {
#[must_use]
pub const fn is_markup(self) -> bool {
matches!(self, Self::HTML | Self::XML)
}
#[must_use]
pub const fn is_text(self) -> bool {
matches!(self, Self::TEXT | Self::JAVASCRIPT | Self::CSS)
}
#[must_use]
pub const fn is_case_sensitive(self) -> bool {
!matches!(self, Self::HTML)
}
pub fn parse(mode: Option<&str>) -> Result<Self, TemplateModeParseError> {
let Some(mode) = mode else {
return Err(TemplateModeParseError);
};
if trim(mode).is_empty() {
return Err(TemplateModeParseError);
}
let parsed = if mode.eq_ignore_ascii_case("HTML") {
Some(Self::HTML)
} else if mode.eq_ignore_ascii_case("XML") {
Some(Self::XML)
} else if mode.eq_ignore_ascii_case("TEXT") {
Some(Self::TEXT)
} else if mode.eq_ignore_ascii_case("JAVASCRIPT") {
Some(Self::JAVASCRIPT)
} else if mode.eq_ignore_ascii_case("CSS") {
Some(Self::CSS)
} else if mode.eq_ignore_ascii_case("RAW") {
Some(Self::RAW)
} else {
None
};
if let Some(parsed) = parsed {
return Ok(parsed);
}
let thread_index = current_thread_index();
tracing::warn!(
thread = %thread_index,
mode,
default_mode = %Self::HTML,
"Unknown Template Mode. Must be one of: HTML, XML, TEXT, JAVASCRIPT, CSS, RAW. Using default Template Mode."
);
Ok(Self::HTML)
}
}
impl Display for TemplateMode {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::HTML => "HTML",
Self::XML => "XML",
Self::TEXT => "TEXT",
Self::JAVASCRIPT => "JAVASCRIPT",
Self::CSS => "CSS",
Self::RAW => "RAW",
})
}
}
impl FromStr for TemplateMode {
type Err = TemplateModeParseError;
fn from_str(mode: &str) -> Result<Self, Self::Err> {
Self::parse(Some(mode))
}
}
impl TemplateObject for TemplateMode {
fn class_name(&self) -> &str {
"org.thymeleaf.templatemode.TemplateMode"
}
fn to_utf16_string(&self) -> Utf16String {
Utf16String::from_rust_str(&self.to_string())
}
fn as_any(&self) -> &dyn Any {
self
}
fn invoke_method(
&self,
method_name: &Utf16String,
arguments: &[Option<Arc<TemplateValue>>],
) -> Option<Result<Option<Arc<TemplateValue>>, TemplateObjectMethodError>> {
if !arguments.is_empty() {
return None;
}
let value = match method_name.to_string_lossy().as_str() {
"isMarkup" => TemplateValue::Boolean(self.is_markup()),
"isText" => TemplateValue::Boolean(self.is_text()),
"isCaseSensitive" => TemplateValue::Boolean(self.is_case_sensitive()),
"name" | "toString" => TemplateValue::string(self.to_utf16_string()),
"ordinal" => TemplateValue::Number(NumberValue::Integer(match self {
Self::HTML => 0,
Self::XML => 1,
Self::TEXT => 2,
Self::JAVASCRIPT => 3,
Self::CSS => 4,
Self::RAW => 5,
})),
_ => return None,
};
Some(Ok(Some(Arc::new(value))))
}
}
fn current_thread_index() -> String {
std::thread::current()
.name()
.unwrap_or("unnamed")
.to_owned()
}
fn trim(value: &str) -> &str {
value.trim_matches(|character| character <= '\u{0020}')
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[error("Template mode cannot be null or empty")]
pub struct TemplateModeParseError;
#[cfg(test)]
mod tests {
use super::{TemplateMode, TemplateModeParseError, current_thread_index};
#[test]
fn exposes_exact_mode_flags() {
assert!(TemplateMode::HTML.is_markup());
assert!(!TemplateMode::HTML.is_text());
assert!(!TemplateMode::HTML.is_case_sensitive());
assert!(TemplateMode::XML.is_markup());
assert!(!TemplateMode::XML.is_text());
assert!(TemplateMode::XML.is_case_sensitive());
for mode in [
TemplateMode::TEXT,
TemplateMode::JAVASCRIPT,
TemplateMode::CSS,
] {
assert!(!mode.is_markup());
assert!(mode.is_text());
assert!(mode.is_case_sensitive());
}
assert!(!TemplateMode::RAW.is_markup());
assert!(!TemplateMode::RAW.is_text());
assert!(TemplateMode::RAW.is_case_sensitive());
}
#[test]
fn parses_all_names_case_insensitively() {
let cases = [
("html", TemplateMode::HTML),
("XML", TemplateMode::XML),
("Text", TemplateMode::TEXT),
("javascript", TemplateMode::JAVASCRIPT),
("Css", TemplateMode::CSS),
("raw", TemplateMode::RAW),
];
for (input, expected) in cases {
assert_eq!(TemplateMode::parse(Some(input)), Ok(expected));
assert_eq!(input.parse::<TemplateMode>(), Ok(expected));
assert_eq!(expected.to_string(), input.to_ascii_uppercase());
}
}
#[test]
fn rejects_null_empty_and_whitespace() {
for input in [None, Some(""), Some(" "), Some("\n\t"), Some("\0")] {
assert_eq!(
TemplateMode::parse(input),
Err(TemplateModeParseError),
"input: {input:?}"
);
}
assert_eq!(
TemplateModeParseError.to_string(),
"Template mode cannot be null or empty"
);
}
#[test]
fn unknown_and_untrimmed_modes_fall_back_to_html() {
assert_eq!(
TemplateMode::parse(Some("MARKDOWN")),
Ok(TemplateMode::HTML)
);
assert_eq!(TemplateMode::parse(Some(" XML ")), Ok(TemplateMode::HTML));
assert_eq!(
TemplateMode::parse(Some("\u{00A0}")),
Ok(TemplateMode::HTML)
);
assert_eq!(
current_thread_index(),
"templatemode::template_mode::tests::unknown_and_untrimmed_modes_fall_back_to_html"
);
let unnamed = std::thread::spawn(current_thread_index).join().unwrap();
assert_eq!(unnamed, "unnamed");
}
}