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