rcss_core/
interpolate.rs

1use std::borrow::Cow;
2
3/// Interpolation token.
4/// Different tokens are handled differently:
5/// - UnwrapLiteral: just place original string as is, into source css file.
6/// Later can be extended with compile time or runtime expressions.
7
8pub enum Token {
9    UnwrapLiteral(String),
10}
11
12/// Token with information about its position in source.
13pub struct TokenWithId {
14    pub token: Token,
15    id: String,
16}
17
18// Store each token in array, and save
19pub struct Interpolation {
20    pub tokens: Vec<TokenWithId>,
21}
22impl Interpolation {
23    pub fn unwrap_literals(&self, source: &str) -> String {
24        let mut result = String::from(source);
25        for token in &self.tokens {
26            match &token.token {
27                Token::UnwrapLiteral(s) => result = result.replace(&token.id, s),
28            }
29        }
30        result
31    }
32}
33
34/// Find any occurrences of ${} in source string and replace it with TOKEN_ID.
35/// Returns: Interpolation object with info about all tokens that was replaced
36/// And source string with replaced tokens.
37pub fn handle_interpolate(mut source: &str) -> (Interpolation, Cow<'_, str>) {
38    let mut state = Interpolation { tokens: vec![] };
39
40    let mut result = String::new();
41    let mut last_id = 0;
42    while let Some(start) = source[..].find("${") {
43        let end = source[start..].find("}").unwrap_or_else(|| {
44            panic!(
45                "No closing bracket found for interpolation token at position {}",
46                start
47            )
48        }) + start;
49
50        let token = &source[start + 2..end].trim_start();
51        let token_id = format!("__RCSS__TOKEN_{}", last_id);
52        last_id += 1;
53
54        if token.starts_with("\"") {
55            let token = token.trim_matches(|c: char| c.is_whitespace() || c == '"');
56            state.tokens.push(TokenWithId {
57                token: Token::UnwrapLiteral(token.to_string()),
58                id: token_id.clone(),
59            });
60        } else {
61            panic!(
62                "Only string literals are supported in interpolation \"#{{..}}\", got=\"{}\"",
63                token
64            );
65        }
66
67        result.push_str(&source[..start]);
68        result.push_str(&token_id);
69        source = &source[end + 1..];
70    }
71    if state.tokens.is_empty() {
72        return (state, Cow::Borrowed(source));
73    }
74    result.push_str(&source);
75    (state, result.into())
76}
77
78#[cfg(test)]
79mod test {
80    use super::*;
81
82    #[test]
83    fn test_handle_interpolate() {
84        let source = "background-color: red; left: ${\"0em\"};";
85        let (interpolation, result) = handle_interpolate(source);
86        assert_eq!(interpolation.tokens.len(), 1);
87        assert_eq!(result, "background-color: red; left: __RCSS__TOKEN_0;");
88        match &interpolation.tokens[0].token {
89            Token::UnwrapLiteral(s) => assert_eq!(s, "0em"),
90        }
91    }
92
93    #[test]
94    fn test_handle_unwrap() {
95        let source = "background-color: red; left: ${\"0em\"}; color: #${\"ff0000\"};";
96        let (interpolation, result) = handle_interpolate(source);
97        assert_eq!(
98            result,
99            "background-color: red; left: __RCSS__TOKEN_0; color: #__RCSS__TOKEN_1;"
100        );
101        assert_eq!(interpolation.tokens.len(), 2);
102        let result = interpolation.unwrap_literals(&result);
103        assert_eq!(result, "background-color: red; left: 0em; color: #ff0000;");
104    }
105}