xiss/
css_map.rs

1use std::{
2    collections::hash_map::Entry,
3    io::{self, Write},
4    rc::Rc,
5};
6
7use regex::Regex;
8use rustc_hash::FxHashMap;
9use smol_str::SmolStr;
10use xiss_map::IdKind;
11
12use crate::{global_id::IdSet, id::Id};
13
14#[derive(Debug, thiserror::Error)]
15pub enum CssMapError {
16    #[error(transparent)]
17    IOError(#[from] io::Error),
18    #[error(transparent)]
19    ParserError(#[from] xiss_map::parser::Error),
20    #[error("Invalid exclude rule: {0}")]
21    InvalidExcludeRule(regex::Error),
22    #[error("Duplicate entry: {0},{1},{2}")]
23    DuplicateEntry(IdKind, SmolStr, SmolStr),
24}
25
26pub struct CssMapModule {
27    pub id: SmolStr,
28    pub index: u32,
29    pub classes: FxHashMap<SmolStr, Rc<Id>>,
30    pub vars: FxHashMap<SmolStr, Rc<Id>>,
31    pub keyframes: FxHashMap<SmolStr, Rc<Id>>,
32}
33
34impl CssMapModule {
35    pub fn new(id: SmolStr, index: u32) -> Self {
36        Self {
37            id,
38            index,
39            classes: FxHashMap::default(),
40            vars: FxHashMap::default(),
41            keyframes: FxHashMap::default(),
42        }
43    }
44}
45
46/// [CssMap] is used to create unique identifiers for class names, variables
47/// and keyframes.
48pub struct CssMap {
49    index: FxHashMap<SmolStr, u32>,
50    pub modules: Vec<Box<CssMapModule>>,
51    classes: IdSet,
52    vars: IdSet,
53    keyframes: IdSet,
54    new_ids_buf: String,
55}
56
57impl CssMap {
58    pub fn new(
59        exclude_class: &Vec<String>,
60        exclude_var: &Vec<String>,
61        exclude_keyframes: &Vec<String>,
62    ) -> Result<Self, CssMapError> {
63        Ok(Self {
64            index: FxHashMap::default(),
65            modules: Vec::new(),
66            classes: IdSet::new(0, build_exclude(exclude_class)?),
67            vars: IdSet::new(0, build_exclude(exclude_var)?),
68            keyframes: IdSet::new(0, build_exclude(exclude_keyframes)?),
69            new_ids_buf: String::new(),
70        })
71    }
72
73    /// Imports css map from [io::BufRead].
74    pub fn import<R: io::BufRead>(&mut self, reader: &mut R) -> Result<(), CssMapError> {
75        let mut buf = String::new();
76        reader.read_to_string(&mut buf)?;
77        let mut parser = xiss_map::parser::Parser::new(&buf);
78        let mut module_index = 0;
79
80        while let Some((kind, module_id, local_id, global_id)) = parser.next_id()? {
81            if let Some(module_id) = module_id {
82                let i = match self.index.entry(module_id.into()) {
83                    Entry::Occupied(entry) => *entry.get(),
84                    Entry::Vacant(entry) => {
85                        let index = self.modules.len() as u32;
86                        self.modules
87                            .push(Box::new(CssMapModule::new(entry.key().clone(), index)));
88                        *entry.insert(index)
89                    }
90                };
91                module_index = i;
92            }
93            if let Some(module) = self.modules.get_mut(module_index as usize) {
94                let id = Rc::new(Id::new(
95                    kind,
96                    module.index,
97                    local_id.into(),
98                    global_id.into(),
99                ));
100                match kind {
101                    IdKind::Class => {
102                        insert_id(&module.id, &mut module.classes, &mut self.classes, id)?
103                    }
104                    IdKind::Var => insert_id(&module.id, &mut module.vars, &mut self.vars, id)?,
105                    IdKind::Keyframes => {
106                        insert_id(&module.id, &mut module.keyframes, &mut self.keyframes, id)?
107                    }
108                }
109            }
110        }
111
112        Ok(())
113    }
114
115    /// Returns [Id] if it exists or creates a new one.
116    pub fn get_id(&mut self, module_index: u32, id_kind: IdKind, local_id: &str) -> Rc<Id> {
117        let module = &mut self.modules[module_index as usize];
118        let (id_char, id_set, map) = match id_kind {
119            IdKind::Class => ('C', &mut self.classes, &mut module.classes),
120            IdKind::Var => ('V', &mut self.vars, &mut module.vars),
121            IdKind::Keyframes => ('K', &mut self.keyframes, &mut module.keyframes),
122        };
123        if let Some(id) = map.get(local_id) {
124            id.clone()
125        } else {
126            let global_id = id_set.next_id();
127
128            let buf = &mut self.new_ids_buf;
129            buf.reserve(5 + module.id.len() + local_id.len() + global_id.len());
130            buf.push(id_char);
131            buf.push(',');
132            buf.push_str(&module.id);
133            buf.push(',');
134            buf.push_str(local_id);
135            buf.push(',');
136            buf.push_str(&global_id);
137            buf.push('\n');
138
139            let id = Rc::new(Id::new(
140                IdKind::Class,
141                module_index,
142                local_id.into(),
143                global_id,
144            ));
145            map.insert(local_id.into(), id.clone());
146
147            id
148        }
149    }
150
151    /// Returns module index if the module exists, otherwise creates a new one
152    /// and returns its index.
153    pub fn get_module_index(&mut self, module_name: &str) -> u32 {
154        get_module_index(&mut self.index, &mut self.modules, module_name)
155    }
156
157    /// Writes new ids into the [output].
158    pub fn flush_new_ids<W: Write>(&mut self, output: &mut W) -> Result<(), io::Error> {
159        output.write(self.new_ids_buf.as_bytes())?;
160        output.flush()?;
161        self.new_ids_buf.clear();
162        Ok(())
163    }
164}
165
166/// Builds a vector of [Regex] objects from a vector of regex strings.
167///
168/// TODO: evaluate the possibility to build one [Regex] from many regex strings
169/// by joining them with `|` operator.
170fn build_exclude(exclude: &Vec<String>) -> Result<Vec<Regex>, CssMapError> {
171    let mut result = Vec::with_capacity(exclude.len());
172    for s in exclude {
173        let r = match Regex::new(s) {
174            Ok(r) => r,
175            Err(err) => return Err(CssMapError::InvalidExcludeRule(err)),
176        };
177        result.push(r);
178    }
179    Ok(result)
180}
181
182/// Returns module index if the module exists, otherwise creates a new one and
183/// returns its index,
184fn get_module_index<'a>(
185    modules_index: &mut FxHashMap<SmolStr, u32>,
186    modules: &'a mut Vec<Box<CssMapModule>>,
187    module_name: &str,
188) -> u32 {
189    if let Some(module_id) = modules_index.get(module_name) {
190        *module_id
191    } else {
192        let module_id = modules.len() as u32;
193        modules_index.insert(module_name.into(), module_id);
194        modules.push(Box::new(CssMapModule::new(module_name.into(), module_id)));
195        module_id
196    }
197}
198
199fn insert_id(
200    module_id: &str,
201    map: &mut FxHashMap<SmolStr, Rc<Id>>,
202    id_set: &mut IdSet,
203    id: Rc<Id>,
204) -> Result<(), CssMapError> {
205    if let Entry::Vacant(v) = map.entry(id.local_id.clone()) {
206        id_set.add(&id.global_id);
207        v.insert(id);
208        Ok(())
209    } else {
210        Err(CssMapError::DuplicateEntry(
211            id.kind,
212            module_id.into(),
213            id.local_id.clone(),
214        ))
215    }
216}