use crate::error::PrinterErrorKind;
use crate::properties::css_modules::{Composes, Specifier};
use crate::selector::SelectorList;
use data_encoding::{Encoding, Specification};
use lazy_static::lazy_static;
use pathdiff::diff_paths;
#[cfg(any(feature = "serde", feature = "nodejs"))]
use serde::Serialize;
use smallvec::{smallvec, SmallVec};
use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::fmt::Write;
use std::hash::{Hash, Hasher};
use std::path::Path;
#[derive(Default, Clone, Debug)]
pub struct Config<'i> {
pub pattern: Pattern<'i>,
pub dashed_idents: bool,
}
#[derive(Clone, Debug)]
pub struct Pattern<'i> {
pub segments: SmallVec<[Segment<'i>; 2]>,
}
impl<'i> Default for Pattern<'i> {
fn default() -> Self {
Pattern {
segments: smallvec![Segment::Hash, Segment::Literal("_"), Segment::Local],
}
}
}
#[derive(Debug)]
pub enum PatternParseError {
UnknownPlaceholder(String, usize),
UnclosedBrackets(usize),
}
impl std::fmt::Display for PatternParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use PatternParseError::*;
match self {
UnknownPlaceholder(p, i) => write!(
f,
"Error parsing CSS modules pattern: unknown placeholder \"{}\" at index {}",
p, i
),
UnclosedBrackets(i) => write!(f, "Error parsing CSS modules pattern: unclosed brackets at index {}", i),
}
}
}
impl std::error::Error for PatternParseError {}
impl<'i> Pattern<'i> {
pub fn parse(mut input: &'i str) -> Result<Self, PatternParseError> {
let mut segments = SmallVec::new();
let mut start_idx: usize = 0;
while !input.is_empty() {
if input.starts_with('[') {
if let Some(end_idx) = input.find(']') {
let segment = match &input[0..=end_idx] {
"[name]" => Segment::Name,
"[local]" => Segment::Local,
"[hash]" => Segment::Hash,
s => return Err(PatternParseError::UnknownPlaceholder(s.into(), start_idx)),
};
segments.push(segment);
start_idx += end_idx + 1;
input = &input[end_idx + 1..];
} else {
return Err(PatternParseError::UnclosedBrackets(start_idx));
}
} else {
let end_idx = input.find('[').unwrap_or_else(|| input.len());
segments.push(Segment::Literal(&input[0..end_idx]));
start_idx += end_idx;
input = &input[end_idx..];
}
}
Ok(Pattern { segments })
}
pub fn write<W, E>(&self, hash: &str, path: &Path, local: &str, mut write: W) -> Result<(), E>
where
W: FnMut(&str) -> Result<(), E>,
{
for segment in &self.segments {
match segment {
Segment::Literal(s) => {
write(s)?;
}
Segment::Name => {
let stem = path.file_stem().unwrap().to_str().unwrap();
if stem.contains('.') {
write(&stem.replace('.', "-"))?;
} else {
write(stem)?;
}
}
Segment::Local => {
write(local)?;
}
Segment::Hash => {
write(hash)?;
}
}
}
Ok(())
}
#[inline]
fn write_to_string(
&self,
mut res: String,
hash: &str,
path: &Path,
local: &str,
) -> Result<String, std::fmt::Error> {
self.write(hash, path, local, |s| res.write_str(s))?;
Ok(res)
}
}
#[derive(Clone, Debug)]
pub enum Segment<'i> {
Literal(&'i str),
Name,
Local,
Hash,
}
#[derive(PartialEq, Debug, Clone)]
#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
#[cfg_attr(
any(feature = "serde", feature = "nodejs"),
serde(tag = "type", rename_all = "lowercase")
)]
pub enum CssModuleReference {
Local {
name: String,
},
Global {
name: String,
},
Dependency {
name: String,
specifier: String,
},
}
#[derive(PartialEq, Debug, Clone)]
#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(rename_all = "camelCase"))]
pub struct CssModuleExport {
pub name: String,
pub composes: Vec<CssModuleReference>,
pub is_referenced: bool,
}
pub type CssModuleExports = HashMap<String, CssModuleExport>;
pub type CssModuleReferences = HashMap<String, CssModuleReference>;
lazy_static! {
static ref ENCODER: Encoding = {
let mut spec = Specification::new();
spec
.symbols
.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-");
spec.encoding().unwrap()
};
}
pub(crate) struct CssModule<'a, 'b, 'c> {
pub config: &'a Config<'b>,
pub sources: Vec<&'c Path>,
pub hashes: Vec<String>,
pub exports_by_source_index: Vec<CssModuleExports>,
pub references: &'a mut HashMap<String, CssModuleReference>,
}
impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> {
pub fn new(
config: &'a Config<'b>,
sources: &'c Vec<String>,
project_root: Option<&'c str>,
references: &'a mut HashMap<String, CssModuleReference>,
) -> Self {
let project_root = project_root.map(|p| Path::new(p));
let sources: Vec<&Path> = sources.iter().map(|filename| Path::new(filename)).collect();
let hashes = sources
.iter()
.map(|path| {
let source = match project_root {
Some(project_root) if path.is_absolute() => {
diff_paths(path, project_root).map_or(Cow::Borrowed(*path), Cow::Owned)
}
_ => Cow::Borrowed(*path),
};
hash(
&source.to_string_lossy(),
matches!(config.pattern.segments[0], Segment::Hash),
)
})
.collect();
Self {
config,
exports_by_source_index: sources.iter().map(|_| HashMap::new()).collect(),
sources,
hashes,
references,
}
}
pub fn add_local(&mut self, exported: &str, local: &str, source_index: u32) {
self.exports_by_source_index[source_index as usize]
.entry(exported.into())
.or_insert_with(|| CssModuleExport {
name: self
.config
.pattern
.write_to_string(
String::new(),
&self.hashes[source_index as usize],
&self.sources[source_index as usize],
local,
)
.unwrap(),
composes: vec![],
is_referenced: false,
});
}
pub fn add_dashed(&mut self, local: &str, source_index: u32) {
self.exports_by_source_index[source_index as usize]
.entry(local.into())
.or_insert_with(|| CssModuleExport {
name: self
.config
.pattern
.write_to_string(
"--".into(),
&self.hashes[source_index as usize],
&self.sources[source_index as usize],
&local[2..],
)
.unwrap(),
composes: vec![],
is_referenced: false,
});
}
pub fn reference(&mut self, name: &str, source_index: u32) {
match self.exports_by_source_index[source_index as usize].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: self
.config
.pattern
.write_to_string(
String::new(),
&self.hashes[source_index as usize],
&self.sources[source_index as usize],
name,
)
.unwrap(),
composes: vec![],
is_referenced: true,
});
}
}
}
pub fn reference_dashed(&mut self, name: &str, from: &Option<Specifier>, source_index: u32) -> Option<String> {
let (reference, key) = match from {
Some(Specifier::Global) => return Some(name[2..].into()),
Some(Specifier::File(file)) => (
CssModuleReference::Dependency {
name: name.to_string(),
specifier: file.to_string(),
},
file.as_ref(),
),
Some(Specifier::SourceIndex(source_index)) => {
return Some(
self
.config
.pattern
.write_to_string(
String::new(),
&self.hashes[*source_index as usize],
&self.sources[*source_index as usize],
&name[2..],
)
.unwrap(),
)
}
None => {
match self.exports_by_source_index[source_index as usize].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: self
.config
.pattern
.write_to_string(
"--".into(),
&self.hashes[source_index as usize],
&self.sources[source_index as usize],
&name[2..],
)
.unwrap(),
composes: vec![],
is_referenced: true,
});
}
}
return None;
}
};
let hash = hash(
&format!("{}_{}_{}", self.hashes[source_index as usize], name, key),
false,
);
let name = format!("--{}", hash);
self.references.insert(name.clone(), reference);
Some(hash)
}
pub fn handle_composes(
&mut self,
selectors: &SelectorList,
composes: &Composes,
source_index: u32,
) -> 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: self
.config
.pattern
.write_to_string(
String::new(),
&self.hashes[source_index as usize],
&self.sources[source_index as usize],
name.0.as_ref(),
)
.unwrap(),
},
Some(Specifier::SourceIndex(dep_source_index)) => {
if let Some(entry) =
self.exports_by_source_index[*dep_source_index as usize].get(&name.0.as_ref().to_owned())
{
let name = entry.name.clone();
let composes = entry.composes.clone();
let export = self.exports_by_source_index[source_index as usize]
.get_mut(&id.0.as_ref().to_owned())
.unwrap();
export.composes.push(CssModuleReference::Local { name });
export.composes.extend(composes);
}
continue;
}
Some(Specifier::Global) => CssModuleReference::Global {
name: name.0.as_ref().into(),
},
Some(Specifier::File(file)) => CssModuleReference::Dependency {
name: name.0.to_string(),
specifier: file.to_string(),
},
};
let export = self.exports_by_source_index[source_index as usize]
.get_mut(&id.0.as_ref().to_owned())
.unwrap();
if !export.composes.contains(&reference) {
export.composes.push(reference);
}
}
continue;
}
_ => {}
}
}
return Err(PrinterErrorKind::InvalidComposesSelector);
}
Ok(())
}
}
pub(crate) fn hash(s: &str, at_start: bool) -> 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 at_start && matches!(hash.as_bytes()[0], b'0'..=b'9') {
format!("_{}", hash)
} else {
hash
}
}