#[derive(Clone, Debug)]
pub struct ConditionScope {
pub positive: Option<String>,
pub sibling_negations: Vec<String>,
}
pub fn generate_condition_text(scopes: &[ConditionScope]) -> Option<String> {
let negations: Vec<String> = scopes
.iter()
.flat_map(|s| s.sibling_negations.iter())
.map(|cond| format!("!({})", wrap_if_needed(cond)))
.collect();
let positives: Vec<String> = scopes
.iter()
.filter_map(|s| s.positive.as_ref())
.map(|cond| wrap_if_needed(cond).into_owned())
.collect();
let parts: Vec<&str> = negations
.iter()
.chain(positives.iter())
.map(|s| s.as_str())
.collect();
if parts.is_empty() {
None
} else {
Some(parts.join(" && "))
}
}
pub fn build_block_guard(condition_text: &str) -> String {
format!("if(!({})) return;", condition_text)
}
pub fn build_ternary_guard(condition_text: &str) -> String {
format!("!({})?undefined:", condition_text)
}
fn wrap_if_needed(expr: &str) -> std::borrow::Cow<'_, str> {
if expr.starts_with('(') && expr.ends_with(')') {
let mut depth = 0i32;
let bytes = expr.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b'(' {
depth += 1;
}
if b == b')' {
depth -= 1;
}
if depth == 0 && i < bytes.len() - 1 {
return std::borrow::Cow::Owned(format!("({})", expr));
}
}
return std::borrow::Cow::Borrowed(expr);
}
std::borrow::Cow::Owned(format!("({})", expr))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_simple_ident_gets_wrapped() {
assert_eq!(wrap_if_needed("x").as_ref(), "(x)");
}
#[test]
fn wrap_expr_with_and_gets_wrapped() {
assert_eq!(wrap_if_needed("a && b").as_ref(), "(a && b)");
}
#[test]
fn wrap_expr_with_or_gets_wrapped() {
assert_eq!(wrap_if_needed("a || b").as_ref(), "(a || b)");
}
#[test]
fn wrap_already_wrapped_stays() {
assert_eq!(
wrap_if_needed("(typeof test === 'string')").as_ref(),
"(typeof test === 'string')"
);
}
#[test]
fn wrap_split_parens_gets_wrapped() {
assert_eq!(wrap_if_needed("(a) && (b)").as_ref(), "((a) && (b))");
}
#[test]
fn wrap_typeof_gets_wrapped() {
assert_eq!(
wrap_if_needed("typeof test === 'string'").as_ref(),
"(typeof test === 'string')"
);
}
#[test]
fn condition_text_simple_v_if() {
let scopes = vec![ConditionScope {
positive: Some("show".into()),
sibling_negations: vec![],
}];
assert_eq!(generate_condition_text(&scopes).unwrap(), "(show)");
}
#[test]
fn condition_text_v_else_if_with_negation() {
let scopes = vec![ConditionScope {
positive: Some("typeof test === 'string'".into()),
sibling_negations: vec!["typeof test === 'object'".into()],
}];
assert_eq!(
generate_condition_text(&scopes).unwrap(),
"!((typeof test === 'object')) && (typeof test === 'string')"
);
}
#[test]
fn condition_text_v_else_negates_all() {
let scopes = vec![ConditionScope {
positive: None,
sibling_negations: vec![
"typeof test === 'string'".into(),
"typeof test === 'number'".into(),
],
}];
assert_eq!(
generate_condition_text(&scopes).unwrap(),
"!((typeof test === 'string')) && !((typeof test === 'number'))"
);
}
#[test]
fn condition_text_nested_v_if_combines_parent_and_own() {
let scopes = vec![
ConditionScope {
positive: Some("typeof test === 'string'".into()),
sibling_negations: vec![],
},
ConditionScope {
positive: Some("test === 'app'".into()),
sibling_negations: vec![],
},
];
assert_eq!(
generate_condition_text(&scopes).unwrap(),
"(typeof test === 'string') && (test === 'app')"
);
}
#[test]
fn condition_text_nested_with_sibling_negations() {
let scopes = vec![
ConditionScope {
positive: Some("P".into()),
sibling_negations: vec![],
},
ConditionScope {
positive: Some("C".into()),
sibling_negations: vec!["A".into()],
},
];
assert_eq!(
generate_condition_text(&scopes).unwrap(),
"!((A)) && (P) && (C)"
);
}
#[test]
fn condition_text_complex_chain() {
let scopes = vec![ConditionScope {
positive: None,
sibling_negations: vec!["A".into(), "B".into(), "C".into()],
}];
assert_eq!(
generate_condition_text(&scopes).unwrap(),
"!((A)) && !((B)) && !((C))"
);
}
#[test]
fn condition_text_empty_scopes_returns_none() {
assert!(generate_condition_text(&[]).is_none());
}
#[test]
fn condition_text_and_or_get_wrapped() {
let scopes = vec![ConditionScope {
positive: Some("a && b".into()),
sibling_negations: vec!["c || d".into()],
}];
assert_eq!(
generate_condition_text(&scopes).unwrap(),
"!((c || d)) && (a && b)"
);
}
#[test]
fn block_guard_simple() {
assert_eq!(build_block_guard("show"), "if(!(show)) return;");
}
#[test]
fn block_guard_complex() {
assert_eq!(build_block_guard("!(A) && B"), "if(!(!(A) && B)) return;");
}
#[test]
fn ternary_guard_simple() {
assert_eq!(
build_ternary_guard("typeof test === 'string'"),
"!(typeof test === 'string')?undefined:"
);
}
#[test]
fn ternary_guard_complex() {
assert_eq!(
build_ternary_guard("!((typeof test === 'object')) && (typeof test === 'string')"),
"!(!((typeof test === 'object')) && (typeof test === 'string'))?undefined:"
);
}
}