1use crate::error::PrinterErrorKind;
12use crate::properties::css_modules::{Composes, Specifier};
13use crate::selector::SelectorList;
14use data_encoding::{Encoding, Specification};
15use lazy_static::lazy_static;
16use pathdiff::diff_paths;
17#[cfg(any(feature = "serde", feature = "nodejs"))]
18use serde::Serialize;
19use smallvec::{smallvec, SmallVec};
20use std::borrow::Cow;
21use std::collections::hash_map::DefaultHasher;
22use std::collections::HashMap;
23use std::fmt::Write;
24use std::hash::{Hash, Hasher};
25use std::path::Path;
26
27#[derive(Clone, Debug, PartialEq)]
29#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
30pub struct Config {
31 pub pattern: Pattern,
34 pub dashed_idents: bool,
36 pub animation: bool,
39 pub grid: bool,
42 pub custom_idents: bool,
45 pub container: bool,
48 pub pure: bool,
50}
51
52impl Default for Config {
53 fn default() -> Self {
54 Config {
55 pattern: Default::default(),
56 dashed_idents: Default::default(),
57 animation: true,
58 grid: true,
59 container: true,
60 custom_idents: true,
61 pure: false,
62 }
63 }
64}
65
66#[derive(Clone, Debug, PartialEq)]
68pub struct Pattern {
69 pub segments: SmallVec<[Segment; 2]>,
71}
72
73impl Default for Pattern {
74 fn default() -> Self {
75 Pattern {
76 segments: smallvec![Segment::Hash, Segment::Literal(Cow::Borrowed("_")), Segment::Local],
77 }
78 }
79}
80
81#[derive(Debug)]
83pub enum PatternParseError {
84 UnknownPlaceholder(String, usize),
86 UnclosedBrackets(usize),
88}
89
90impl std::fmt::Display for PatternParseError {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 use PatternParseError::*;
93 match self {
94 UnknownPlaceholder(p, i) => write!(
95 f,
96 "Error parsing CSS modules pattern: unknown placeholder \"{}\" at index {}",
97 p, i
98 ),
99 UnclosedBrackets(i) => write!(f, "Error parsing CSS modules pattern: unclosed brackets at index {}", i),
100 }
101 }
102}
103
104impl std::error::Error for PatternParseError {}
105
106impl Pattern {
107 pub fn parse(mut input: &str) -> Result<Self, PatternParseError> {
109 let mut segments = SmallVec::new();
110 let mut start_idx: usize = 0;
111 while !input.is_empty() {
112 if input.starts_with('[') {
113 if let Some(end_idx) = input.find(']') {
114 let segment = match &input[0..=end_idx] {
115 "[name]" => Segment::Name,
116 "[local]" => Segment::Local,
117 "[hash]" => Segment::Hash,
118 "[content-hash]" => Segment::ContentHash,
119 s => return Err(PatternParseError::UnknownPlaceholder(s.into(), start_idx)),
120 };
121 segments.push(segment);
122 start_idx += end_idx + 1;
123 input = &input[end_idx + 1..];
124 } else {
125 return Err(PatternParseError::UnclosedBrackets(start_idx));
126 }
127 } else {
128 let end_idx = input.find('[').unwrap_or_else(|| input.len());
129 segments.push(Segment::Literal(Cow::Owned(input[0..end_idx].to_owned())));
130 start_idx += end_idx;
131 input = &input[end_idx..];
132 }
133 }
134
135 Ok(Pattern { segments })
136 }
137
138 pub fn has_content_hash(&self) -> bool {
140 self.segments.iter().any(|s| matches!(s, Segment::ContentHash))
141 }
142
143 pub fn write<W, E>(
145 &self,
146 hash: &str,
147 path: &Path,
148 local: &str,
149 content_hash: &str,
150 mut write: W,
151 ) -> Result<(), E>
152 where
153 W: FnMut(&str) -> Result<(), E>,
154 {
155 for segment in &self.segments {
156 match segment {
157 Segment::Literal(s) => {
158 write(s)?;
159 }
160 Segment::Name => {
161 let stem = path.file_stem().unwrap().to_str().unwrap();
162 if stem.contains('.') {
163 write(&stem.replace('.', "-"))?;
164 } else {
165 write(stem)?;
166 }
167 }
168 Segment::Local => {
169 write(local)?;
170 }
171 Segment::Hash => {
172 write(hash)?;
173 }
174 Segment::ContentHash => {
175 write(content_hash)?;
176 }
177 }
178 }
179 Ok(())
180 }
181
182 #[inline]
183 fn write_to_string(
184 &self,
185 mut res: String,
186 hash: &str,
187 path: &Path,
188 local: &str,
189 content_hash: &str,
190 ) -> Result<String, std::fmt::Error> {
191 self.write(hash, path, local, content_hash, |s| res.write_str(s))?;
192 Ok(res)
193 }
194}
195
196#[derive(Clone, Debug, PartialEq)]
200pub enum Segment {
201 Literal(Cow<'static, str>),
203 Name,
205 Local,
207 Hash,
209 ContentHash,
211}
212
213#[derive(PartialEq, Debug, Clone)]
217#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
218#[cfg_attr(
219 any(feature = "serde", feature = "nodejs"),
220 serde(tag = "type", rename_all = "lowercase")
221)]
222pub enum CssModuleReference {
223 Local {
225 name: String,
227 },
228 Global {
230 name: String,
232 },
233 Dependency {
235 name: String,
237 specifier: String,
239 },
240}
241
242#[derive(PartialEq, Debug, Clone)]
244#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
245#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(rename_all = "camelCase"))]
246pub struct CssModuleExport {
247 pub name: String,
249 pub composes: Vec<CssModuleReference>,
251 pub is_referenced: bool,
253}
254
255pub type CssModuleExports = HashMap<String, CssModuleExport>;
257
258pub type CssModuleReferences = HashMap<String, CssModuleReference>;
260
261lazy_static! {
262 static ref ENCODER: Encoding = {
263 let mut spec = Specification::new();
264 spec
265 .symbols
266 .push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-");
267 spec.encoding().unwrap()
268 };
269}
270
271pub(crate) struct CssModule<'a, 'c> {
272 pub config: &'a Config,
273 pub sources: Vec<&'c Path>,
274 pub hashes: Vec<String>,
275 pub content_hashes: &'a Option<Vec<String>>,
276 pub exports_by_source_index: Vec<CssModuleExports>,
277 pub references: &'a mut HashMap<String, CssModuleReference>,
278}
279
280impl<'a, 'c> CssModule<'a, 'c> {
281 pub fn new(
282 config: &'a Config,
283 sources: &'c Vec<String>,
284 project_root: Option<&'c str>,
285 references: &'a mut HashMap<String, CssModuleReference>,
286 content_hashes: &'a Option<Vec<String>>,
287 ) -> Self {
288 let project_root = project_root.map(|p| Path::new(p));
289 let sources: Vec<&Path> = sources.iter().map(|filename| Path::new(filename)).collect();
290 let hashes = sources
291 .iter()
292 .map(|path| {
293 let source = match project_root {
295 Some(project_root) if path.is_absolute() => {
296 diff_paths(path, project_root).map_or(Cow::Borrowed(*path), Cow::Owned)
297 }
298 _ => Cow::Borrowed(*path),
299 };
300 hash(
301 &source.to_string_lossy(),
302 matches!(config.pattern.segments[0], Segment::Hash),
303 )
304 })
305 .collect();
306 Self {
307 config,
308 exports_by_source_index: sources.iter().map(|_| HashMap::new()).collect(),
309 sources,
310 hashes,
311 content_hashes,
312 references,
313 }
314 }
315
316 pub fn add_local(&mut self, exported: &str, local: &str, source_index: u32) {
317 self.exports_by_source_index[source_index as usize]
318 .entry(exported.into())
319 .or_insert_with(|| CssModuleExport {
320 name: self
321 .config
322 .pattern
323 .write_to_string(
324 String::new(),
325 &self.hashes[source_index as usize],
326 &self.sources[source_index as usize],
327 local,
328 if let Some(content_hashes) = &self.content_hashes {
329 &content_hashes[source_index as usize]
330 } else {
331 ""
332 },
333 )
334 .unwrap(),
335 composes: vec![],
336 is_referenced: false,
337 });
338 }
339
340 pub fn add_dashed(&mut self, local: &str, source_index: u32) {
341 self.exports_by_source_index[source_index as usize]
342 .entry(local.into())
343 .or_insert_with(|| CssModuleExport {
344 name: self
345 .config
346 .pattern
347 .write_to_string(
348 "--".into(),
349 &self.hashes[source_index as usize],
350 &self.sources[source_index as usize],
351 &local[2..],
352 if let Some(content_hashes) = &self.content_hashes {
353 &content_hashes[source_index as usize]
354 } else {
355 ""
356 },
357 )
358 .unwrap(),
359 composes: vec![],
360 is_referenced: false,
361 });
362 }
363
364 pub fn reference(&mut self, name: &str, source_index: u32) {
365 match self.exports_by_source_index[source_index as usize].entry(name.into()) {
366 std::collections::hash_map::Entry::Occupied(mut entry) => {
367 entry.get_mut().is_referenced = true;
368 }
369 std::collections::hash_map::Entry::Vacant(entry) => {
370 entry.insert(CssModuleExport {
371 name: self
372 .config
373 .pattern
374 .write_to_string(
375 String::new(),
376 &self.hashes[source_index as usize],
377 &self.sources[source_index as usize],
378 name,
379 if let Some(content_hashes) = &self.content_hashes {
380 &content_hashes[source_index as usize]
381 } else {
382 ""
383 },
384 )
385 .unwrap(),
386 composes: vec![],
387 is_referenced: true,
388 });
389 }
390 }
391 }
392
393 pub fn reference_dashed(&mut self, name: &str, from: &Option<Specifier>, source_index: u32) -> Option<String> {
394 let (reference, key) = match from {
395 Some(Specifier::Global) => return Some(name[2..].into()),
396 Some(Specifier::File(file)) => (
397 CssModuleReference::Dependency {
398 name: name.to_string(),
399 specifier: file.to_string(),
400 },
401 file.as_ref(),
402 ),
403 Some(Specifier::SourceIndex(source_index)) => {
404 return Some(
405 self
406 .config
407 .pattern
408 .write_to_string(
409 String::new(),
410 &self.hashes[*source_index as usize],
411 &self.sources[*source_index as usize],
412 &name[2..],
413 if let Some(content_hashes) = &self.content_hashes {
414 &content_hashes[*source_index as usize]
415 } else {
416 ""
417 },
418 )
419 .unwrap(),
420 )
421 }
422 None => {
423 match self.exports_by_source_index[source_index as usize].entry(name.into()) {
425 std::collections::hash_map::Entry::Occupied(mut entry) => {
426 entry.get_mut().is_referenced = true;
427 }
428 std::collections::hash_map::Entry::Vacant(entry) => {
429 entry.insert(CssModuleExport {
430 name: self
431 .config
432 .pattern
433 .write_to_string(
434 "--".into(),
435 &self.hashes[source_index as usize],
436 &self.sources[source_index as usize],
437 &name[2..],
438 if let Some(content_hashes) = &self.content_hashes {
439 &content_hashes[source_index as usize]
440 } else {
441 ""
442 },
443 )
444 .unwrap(),
445 composes: vec![],
446 is_referenced: true,
447 });
448 }
449 }
450 return None;
451 }
452 };
453
454 let hash = hash(
455 &format!("{}_{}_{}", self.hashes[source_index as usize], name, key),
456 false,
457 );
458 let name = format!("--{}", hash);
459
460 self.references.insert(name.clone(), reference);
461 Some(hash)
462 }
463
464 pub fn handle_composes(
465 &mut self,
466 selectors: &SelectorList,
467 composes: &Composes,
468 source_index: u32,
469 ) -> Result<(), PrinterErrorKind> {
470 for sel in &selectors.0 {
471 if sel.len() == 1 {
472 match sel.iter_raw_match_order().next().unwrap() {
473 parcel_selectors::parser::Component::Class(ref id) => {
474 for name in &composes.names {
475 let reference = match &composes.from {
476 None => CssModuleReference::Local {
477 name: self
478 .config
479 .pattern
480 .write_to_string(
481 String::new(),
482 &self.hashes[source_index as usize],
483 &self.sources[source_index as usize],
484 name.0.as_ref(),
485 if let Some(content_hashes) = &self.content_hashes {
486 &content_hashes[source_index as usize]
487 } else {
488 ""
489 },
490 )
491 .unwrap(),
492 },
493 Some(Specifier::SourceIndex(dep_source_index)) => {
494 if let Some(entry) =
495 self.exports_by_source_index[*dep_source_index as usize].get(&name.0.as_ref().to_owned())
496 {
497 let name = entry.name.clone();
498 let composes = entry.composes.clone();
499 let export = self.exports_by_source_index[source_index as usize]
500 .get_mut(&id.0.as_ref().to_owned())
501 .unwrap();
502
503 export.composes.push(CssModuleReference::Local { name });
504 export.composes.extend(composes);
505 }
506 continue;
507 }
508 Some(Specifier::Global) => CssModuleReference::Global {
509 name: name.0.as_ref().into(),
510 },
511 Some(Specifier::File(file)) => CssModuleReference::Dependency {
512 name: name.0.to_string(),
513 specifier: file.to_string(),
514 },
515 };
516
517 let export = self.exports_by_source_index[source_index as usize]
518 .get_mut(&id.0.as_ref().to_owned())
519 .unwrap();
520 if !export.composes.contains(&reference) {
521 export.composes.push(reference);
522 }
523 }
524 continue;
525 }
526 _ => {}
527 }
528 }
529
530 return Err(PrinterErrorKind::InvalidComposesSelector);
532 }
533
534 Ok(())
535 }
536}
537
538pub(crate) fn hash(s: &str, at_start: bool) -> String {
539 let mut hasher = DefaultHasher::new();
540 s.hash(&mut hasher);
541 let hash = hasher.finish() as u32;
542
543 let hash = ENCODER.encode(&hash.to_le_bytes());
544 if at_start && matches!(hash.as_bytes()[0], b'0'..=b'9') {
545 format!("_{}", hash)
546 } else {
547 hash
548 }
549}