1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use data_encoding::{Specification, Encoding};
use lazy_static::lazy_static;
use crate::properties::css_modules::{Composes, ComposesFrom};
use parcel_selectors::SelectorList;
use crate::selector::Selectors;
use serde::Serialize;
use crate::error::PrinterErrorKind;

#[derive(PartialEq, Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum CssModuleReference {
  Local {
    name: String,
  },
  Global {
    name: String
  },
  Dependency {
    name: String,
    specifier: String
  }
}

#[derive(PartialEq, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CssModuleExport {
  pub name: String,
  pub composes: Vec<CssModuleReference>,
  pub is_referenced: bool
}

pub type CssModuleExports = HashMap<String, CssModuleExport>;

lazy_static! {
  static ref ENCODER: Encoding = {
    let mut spec = Specification::new();
    spec.symbols.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-");
    spec.encoding().unwrap()
  };
}

pub(crate) struct CssModule<'a> {
  pub hash: &'a str,
  pub exports: &'a mut CssModuleExports
}

impl<'a> CssModule<'a> {
  pub fn add_local(&mut self, exported: &str, local: &str) {
    self.exports.entry(exported.into())
      .or_insert_with(|| CssModuleExport {
        name: get_hashed_name(self.hash, local),
        composes: vec![],
        is_referenced: false
      });
  }

  pub fn reference(&mut self, name: &str) {
    match self.exports.entry(name.into()) {
      std::collections::hash_map::Entry::Occupied(mut entry) => {
        entry.get_mut().is_referenced = true;
      }
      std::collections::hash_map::Entry::Vacant(entry) => {
        entry.insert(CssModuleExport {
          name: get_hashed_name(self.hash, name),
          composes: vec![],
          is_referenced: true
        });
      }
    }
  }

  pub fn handle_composes(&mut self, selectors: &SelectorList<Selectors>, composes: &Composes) -> Result<(), PrinterErrorKind> {
    for sel in &selectors.0 {
      if sel.len() == 1 {
        match sel.iter_raw_match_order().next().unwrap() {
          parcel_selectors::parser::Component::Class(ref id) => {
            for name in &composes.names {
              let reference = match &composes.from {
                None => CssModuleReference::Local { name: get_hashed_name(self.hash, name.0.as_ref()) },
                Some(ComposesFrom::Global) => CssModuleReference::Global { name: name.0.as_ref().into() },
                Some(ComposesFrom::File(file)) => CssModuleReference::Dependency {
                  name: name.0.to_string(),
                  specifier: file.to_string()
                }
              };

              let export = self.exports.get_mut(&id.0.as_ref().to_owned()).unwrap();
              if !export.composes.contains(&reference) {
                export.composes.push(reference);
              }
            }
            continue;
          }
          _ => {}
        }
      }

      // The composes property can only be used within a simple class selector.
      return Err(PrinterErrorKind::InvalidComposesSelector)
    }

    Ok(())
  }
}

fn get_hashed_name(hash: &str, name: &str) -> String {
  // Hash must come first so that CSS grid identifiers work.
  // This is because grid lines may have an implicit -start or -end appended.
  format!("{}_{}", hash, name)
}

pub(crate) fn hash(s: &str) -> String {
  let mut hasher = DefaultHasher::new();
  s.hash(&mut hasher);
  let hash = hasher.finish() as u32;
  
  let hash = ENCODER.encode(&hash.to_le_bytes());
  if matches!(hash.as_bytes()[0], b'0'..=b'9') {
    format!("_{}", hash)
  } else {
    hash
  }
}