use super::{check, CallError, FunctionMap};
use crate::css::{CssString, Value};
use crate::value::ListSeparator;
use crate::Scope;
use lazy_static::lazy_static;
use std::cmp::min;
use std::sync::Mutex;
pub fn create_module() -> Scope {
let mut f = Scope::builtin_module("sass:string");
def!(f, quote(string), |s| {
let arg = s.get_opt(name!(string))?;
Ok(arg.unwrap_or_else(|| CssString::from("")).quote().into())
});
def!(f, index(string, substring), |s| {
let string: String = s.get(name!(string))?;
Ok(string
.find(&s.get::<String>(name!(substring))?)
.map_or(Value::Null, |i| {
Value::scalar(1 + string[0..i].chars().count())
}))
});
def!(f, insert(string, insert, index), |s| {
let string: CssString = s.get(name!(string))?;
let insert: String = s.get(name!(insert))?;
let index = s.get_map(name!(index), check::unitless_int)?;
let index = if index.is_negative() {
let len = string.value().chars().count();
len.saturating_sub(index.unsigned_abs() as usize - 1)
} else {
(index as usize).saturating_sub(1)
};
let mut s = string.value().chars();
let mut join = s.by_ref().take(index).collect::<String>();
join.push_str(&insert);
join.extend(s);
Ok(CssString::new(join, string.quotes()).into())
});
def!(f, length(string), |s| {
let string: String = s.get(name!(string))?;
Ok(Value::scalar(string.chars().count()))
});
def!(f, slice(string, start_at, end_at = b"-1"), |s| {
let string: CssString = s.get(name!(string))?;
let st = string.value();
let len = st.chars().count();
let start_at = s.get_map(name!(start_at), check::unitless_int)?;
let start_at = if start_at.is_negative() {
len.saturating_sub(start_at.unsigned_abs() as usize)
} else if start_at.is_positive() {
min(start_at as usize - 1, len)
} else {
0
};
let end_at = s.get_map(name!(end_at), check::unitless_int)?;
let end_at = if end_at.is_negative() {
len.saturating_sub(end_at.unsigned_abs() as usize - 1)
} else {
end_at as usize
};
if start_at <= end_at {
let part =
st.chars().skip(start_at).take(end_at - start_at).collect();
Ok(CssString::new(part, string.quotes()).into())
} else {
Err(CallError::msg(format!("Bad indexes: {start_at}..{end_at}")))
}
});
def!(f, split(string, separator, limit = b"null"), |s| {
let string: CssString = s.get(name!(string))?;
let q = string.quotes();
let string = string.take_value();
let separator: CssString = s.get(name!(separator))?;
let separator = separator.value();
let limit = s.get_opt_map(name!(limit), |v| {
let v = check::unitless_int(v)?;
if v > 0 {
Ok(v as usize)
} else {
Err(format!("Must be 1 or greater, was {v}."))
}
})?;
let v = if string.is_empty() {
vec![]
} else {
let mut v = string.splitn(
limit.unwrap_or(usize::MAX).saturating_add(1),
separator,
);
if separator.is_empty() {
v.next();
}
let mut v = v
.map(|s| CssString::new(s.into(), q).into())
.collect::<Vec<_>>();
if separator.is_empty() {
v.pop();
}
v
};
Ok(Value::List(v, Some(ListSeparator::Comma), true))
});
def!(f, to_upper_case(string), |s| {
let v: CssString = s.get(name!(string))?;
Ok(CssString::new(v.value().to_ascii_uppercase(), v.quotes()).into())
});
def!(f, to_upper_case_unicode(string), |s| {
let v: CssString = s.get(name!(string))?;
Ok(CssString::new(v.value().to_uppercase(), v.quotes()).into())
});
def!(f, to_lower_case(string), |s| {
let v: CssString = s.get(name!(string))?;
Ok(CssString::new(v.value().to_ascii_lowercase(), v.quotes()).into())
});
def!(f, to_lower_case_unicode(string), |s| {
let v: CssString = s.get(name!(string))?;
Ok(CssString::new(v.value().to_lowercase(), v.quotes()).into())
});
def!(f, unique_id(), |_s| {
lazy_static! {
static ref CALL_ID: Mutex<u64> =
Mutex::new(u64::from(std::process::id()) * 0xa01);
};
let v = {
let mut v = CALL_ID.lock().unwrap();
*v += 1;
*v
};
Ok(format!("x{v:x}").into())
});
def!(f, unquote(string), |s| {
Ok(s.get::<CssString>(name!(string))?.unquote().into())
});
f
}
pub fn expose(m: &Scope, global: &mut FunctionMap) {
for (gname, lname) in &[
(name!(quote), name!(quote)),
(name!(str_index), name!(index)),
(name!(str_insert), name!(insert)),
(name!(str_length), name!(length)),
(name!(str_slice), name!(slice)),
(name!(to_upper_case), name!(to_upper_case)),
(name!(to_lower_case), name!(to_lower_case)),
(name!(unique_id), name!(unique_id)),
(name!(unquote), name!(unquote)),
] {
global.insert(gname.clone(), m.get_lfunction(lname));
}
def!(global, url(string = b"null"), |s| {
let string: Value = s.get(name!(string))?;
Ok(format!("url({})", string.format(Default::default())).into())
});
}