use std::sync::Arc;
use thiserror::Error;
use unicode_general_category::{GeneralCategory, get_general_category};
use crate::context::IExpressionContext;
use crate::util::{NumberValue, Utf16String};
use super::TemplateValue;
pub struct NativeShortcutExpression {
expression_levels: Vec<Utf16String>,
}
impl NativeShortcutExpression {
pub fn new(expression_levels: Vec<Utf16String>) -> Self {
Self { expression_levels }
}
pub fn parse(expression: Option<&Utf16String>) -> Option<Vec<Utf16String>> {
let expression = expression?;
let mut levels = Vec::new();
for level in expression.as_utf16().split(|unit| *unit == b'.' as u16) {
if level.is_empty() || !is_java_identifier(level) {
return None;
}
levels.push(Utf16String::from_utf16(level.to_vec()));
}
(!levels.is_empty()).then_some(levels)
}
pub fn evaluate(
&self,
context: &dyn IExpressionContext,
use_selection_as_root: bool,
restrict_variable_access: bool,
) -> Result<Option<Arc<TemplateValue>>, NativeShortcutError> {
let mut target = if use_selection_as_root {
context.as_template_context().and_then(|template_context| {
template_context.has_selection_target().then(|| {
template_context
.get_selection_target()
.unwrap_or_else(|| Arc::new(TemplateValue::Null))
})
})
} else {
None
};
let mut context_root = target.is_none();
for property_name in &self.expression_levels {
if context_root {
if restrict_variable_access && property_name == &Utf16String::from_rust_str("param")
{
return Err(NativeShortcutError::RestrictedVariable {
name: property_name.to_string_lossy(),
});
}
target = context.get_variable(Some(property_name));
context_root = false;
continue;
}
let current = target
.as_deref()
.ok_or_else(|| NativeShortcutError::NullSource {
property_name: property_name.to_string_lossy(),
})?;
target = read_property(current, property_name)?;
}
Ok(target)
}
}
fn read_property(
target: &TemplateValue,
property_name: &Utf16String,
) -> Result<Option<Arc<TemplateValue>>, NativeShortcutError> {
let name = property_name.to_string_lossy();
match target {
TemplateValue::Map(entries) => {
if name == "size" {
return Ok(Some(integer_value(entries.len())));
}
if name == "isEmpty" {
return Ok(Some(Arc::new(TemplateValue::Boolean(entries.is_empty()))));
}
if name == "keys" || name == "keySet" {
return Ok(Some(Arc::new(TemplateValue::List(Arc::new(
entries.iter().map(|(key, _)| Arc::clone(key)).collect(),
)))));
}
if name == "values" {
return Ok(Some(Arc::new(TemplateValue::List(Arc::new(
entries.iter().map(|(_, value)| Arc::clone(value)).collect(),
)))));
}
Ok(entries.iter().find_map(|(key, value)| {
matches!(
key.as_ref(),
TemplateValue::String(key_value) | TemplateValue::SafeHtml(key_value)
if key_value.as_ref() == property_name
)
.then(|| Arc::clone(value))
}))
}
TemplateValue::List(values) => match name.as_str() {
"size" | "length" => Ok(Some(integer_value(values.len()))),
"isEmpty" | "empty" => Ok(Some(Arc::new(TemplateValue::Boolean(values.is_empty())))),
_ => Err(not_applicable(name, target)),
},
TemplateValue::Bytes(values) => match name.as_str() {
"length" => Ok(Some(integer_value(values.len()))),
_ => Err(not_applicable(name, target)),
},
TemplateValue::String(value) | TemplateValue::SafeHtml(value) => match name.as_str() {
"length" => Ok(Some(integer_value(value.len()))),
"isEmpty" | "empty" => Ok(Some(Arc::new(TemplateValue::Boolean(value.is_empty())))),
_ => Err(not_applicable(name, target)),
},
TemplateValue::Object(value) => match value.get_property(property_name) {
Some(result) => result.map_err(|error| NativeShortcutError::PropertyGetter {
property_name: name,
class_name: value.class_name().to_owned(),
message: error.to_string(),
}),
None => Err(NativeShortcutError::NotApplicable(
NativeShortcutExpressionNotApplicableError::new(
name,
value.class_name().to_owned(),
),
)),
},
TemplateValue::Null => Err(NativeShortcutError::NullSource {
property_name: name,
}),
_ => Err(not_applicable(name, target)),
}
}
fn not_applicable(property_name: String, target: &TemplateValue) -> NativeShortcutError {
NativeShortcutError::NotApplicable(NativeShortcutExpressionNotApplicableError::new(
property_name,
target.class_name().to_owned(),
))
}
fn integer_value(value: usize) -> Arc<TemplateValue> {
Arc::new(TemplateValue::Number(NumberValue::Integer(
i32::try_from(value).unwrap_or(i32::MAX),
)))
}
fn is_java_identifier(input: &[u16]) -> bool {
let mut position = 0;
let mut first = true;
while position < input.len() {
let (code_point, width) = decode_code_point(input, position);
if if first {
!is_java_identifier_start(code_point)
} else {
!is_java_identifier_part(code_point)
} {
return false;
}
first = false;
position += width;
}
!first
}
fn decode_code_point(input: &[u16], position: usize) -> (u32, usize) {
let first = input[position];
if (0xD800..=0xDBFF).contains(&first)
&& input
.get(position + 1)
.is_some_and(|second| (0xDC00..=0xDFFF).contains(second))
{
let second = input[position + 1];
(
0x10000 + ((u32::from(first) - 0xD800) << 10) + (u32::from(second) - 0xDC00),
2,
)
} else {
(u32::from(first), 1)
}
}
fn is_java_identifier_start(code_point: u32) -> bool {
let Some(character) = char::from_u32(code_point) else {
return false;
};
matches!(
get_general_category(character),
GeneralCategory::UppercaseLetter
| GeneralCategory::LowercaseLetter
| GeneralCategory::TitlecaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
| GeneralCategory::LetterNumber
| GeneralCategory::CurrencySymbol
| GeneralCategory::ConnectorPunctuation
)
}
fn is_java_identifier_part(code_point: u32) -> bool {
if is_java_identifier_start(code_point) {
return true;
}
let Some(character) = char::from_u32(code_point) else {
return false;
};
matches!(
get_general_category(character),
GeneralCategory::NonspacingMark
| GeneralCategory::SpacingMark
| GeneralCategory::DecimalNumber
| GeneralCategory::Format
) || matches!(code_point, 0x0000..=0x0008 | 0x000E..=0x001B | 0x007F..=0x009F)
}
#[derive(Debug, Error)]
pub enum NativeShortcutError {
#[error("source is null for getProperty(null, \"{property_name}\")")]
NullSource {
property_name: String,
},
#[error(transparent)]
NotApplicable(NativeShortcutExpressionNotApplicableError),
#[error("Access to variable \"{name}\" is forbidden in this context")]
RestrictedVariable {
name: String,
},
#[error("Exception reading property \"{property_name}\" on {class_name}: {message}")]
PropertyGetter {
property_name: String,
class_name: String,
message: String,
},
}
#[derive(Debug, Error)]
#[error("property \"{property_name}\" is not readable on {class_name}")]
pub struct NativeShortcutExpressionNotApplicableError {
property_name: String,
class_name: String,
}
impl NativeShortcutExpressionNotApplicableError {
fn new(property_name: String, class_name: String) -> Self {
Self {
property_name,
class_name,
}
}
}