use super::{Builtin, GlobalFunctionMap}; use num_bigint::BigInt; use num_traits::{Signed, ToPrimitive, Zero}; #[cfg(feature = "random")] use rand::{distributions::Alphanumeric, thread_rng, Rng}; use crate::{ args::CallArgs, common::QuoteKind, error::SassResult, parse::Parser, unit::Unit, value::{Number, Value}, }; pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { Value::String(mut i, q) => { i.make_ascii_uppercase(); Ok(Value::String(i, q)) } v => Err(( format!("$string: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()), } } pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { Value::String(mut i, q) => { i.make_ascii_lowercase(); Ok(Value::String(i, q)) } v => Err(( format!("$string: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()), } } pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { Value::String(i, _) => Ok(Value::Dimension( Some(Number::from(i.chars().count())), Unit::None, true, )), v => Err(( format!("$string: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()), } } pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)), v => Err(( format!("$string: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()), } } pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { i @ Value::String(..) => Ok(i.unquote()), v => Err(( format!("$string: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()), } } pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let (string, quotes) = match args.get_err(0, "string")? { Value::String(s, q) => (s, q), v => { return Err(( format!("$string: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; let str_len = string.chars().count(); let start = match args.get_err(1, "start-at")? { Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { return Err((format!("{} is not an int.", n), args.span()).into()) } Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => { n.to_integer().to_usize().unwrap_or(str_len + 1) } Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 1_usize, Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 1_usize, Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1)) .to_usize() .unwrap(), Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( "$start: Expected {} to have no units.", v.inspect(args.span())? ), args.span(), ) .into()) } v => { return Err(( format!("$start-at: {} is not a number.", v.inspect(args.span())?), args.span(), ) .into()) } }; let mut end = match args.default_arg(2, "end-at", Value::Null)? { Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { return Err((format!("{} is not an int.", n), args.span()).into()) } Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => { n.to_integer().to_usize().unwrap_or(str_len + 1) } Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 0_usize, Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 0_usize, Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1)) .to_usize() .unwrap_or(str_len + 1), Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( "$end: Expected {} to have no units.", v.inspect(args.span())? ), args.span(), ) .into()) } Value::Null => str_len, v => { return Err(( format!("$end-at: {} is not a number.", v.inspect(args.span())?), args.span(), ) .into()) } }; if end > str_len { end = str_len; } if start > end || start > str_len { Ok(Value::String(String::new(), quotes)) } else { Ok(Value::String( string .chars() .skip(start - 1) .take(end - start + 1) .collect(), quotes, )) } } pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let s1 = match args.get_err(0, "string")? { Value::String(i, _) => i, v => { return Err(( format!("$string: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; let substr = match args.get_err(1, "substring")? { Value::String(i, _) => i, v => { return Err(( format!("$substring: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(match s1.find(&substr) { Some(v) => Value::Dimension(Some(Number::from(v + 1)), Unit::None, true), None => Value::Null, }) } pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let (s1, quotes) = match args.get_err(0, "string")? { Value::String(i, q) => (i, q), v => { return Err(( format!("$string: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; let substr = match args.get_err(1, "insert")? { Value::String(i, _) => i, v => { return Err(( format!("$insert: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; let index = match args.get_err(2, "index")? { Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { return Err((format!("$index: {} is not an int.", n), args.span()).into()) } Value::Dimension(Some(n), Unit::None, _) => n, Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( "$index: Expected {} to have no units.", v.inspect(args.span())? ), args.span(), ) .into()) } v => { return Err(( format!("$index: {} is not a number.", v.inspect(args.span())?), args.span(), ) .into()) } }; if s1.is_empty() { return Ok(Value::String(substr, quotes)); } let len = s1.chars().count(); // Insert substring at char position, rather than byte position let insert = |idx, s1: String, s2| { s1.chars() .enumerate() .map(|(i, c)| { if i + 1 == idx { c.to_string() + s2 } else if idx == 0 && i == 0 { s2.to_string() + &c.to_string() } else { c.to_string() } }) .collect::() }; let string = if index.is_positive() { insert( index .to_integer() .to_usize() .unwrap_or(len + 1) .min(len + 1) - 1, s1, &substr, ) } else if index.is_zero() { insert(0, s1, &substr) } else { let idx = index.abs().to_integer().to_usize().unwrap_or(len + 1); if idx > len { insert(0, s1, &substr) } else { insert(len - idx + 1, s1, &substr) } }; Ok(Value::String(string, quotes)) } #[cfg(feature = "random")] #[allow(clippy::needless_pass_by_value)] pub(crate) fn unique_id(args: CallArgs, _: &mut Parser<'_>) -> SassResult { args.max_args(0)?; let mut rng = thread_rng(); let string = std::iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) .take(7) .collect(); Ok(Value::String(string, QuoteKind::None)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("to-upper-case", Builtin::new(to_upper_case)); f.insert("to-lower-case", Builtin::new(to_lower_case)); f.insert("str-length", Builtin::new(str_length)); f.insert("quote", Builtin::new(quote)); f.insert("unquote", Builtin::new(unquote)); f.insert("str-slice", Builtin::new(str_slice)); f.insert("str-index", Builtin::new(str_index)); f.insert("str-insert", Builtin::new(str_insert)); #[cfg(feature = "random")] f.insert("unique-id", Builtin::new(unique_id)); }