1use std::{
2 collections::HashSet,
3 env,
4 ffi::OsStr,
5 fs,
6 path::{Path, PathBuf},
7 str::FromStr,
8};
9
10use anyhow::{anyhow, Context};
11use cssparser::{Parser, ParserInput, Token};
12use derive_more::Into;
13use grass::InputSyntax;
14
15#[cfg_attr(feature = "css-transpile", path = "transpile-enabled.rs")]
16#[cfg_attr(not(feature = "css-transpile"), path = "transpile-disabled.rs")]
17mod transpile;
18
19use thiserror::Error;
20pub use transpile::Version;
21
22pub struct NameMapping {
23 pub plain: String,
24 pub mangled: String,
25}
26
27#[derive(Into)]
28pub struct CssSyntax(InputSyntax);
29
30impl Default for CssSyntax {
31 fn default() -> Self {
32 Self(InputSyntax::Css)
33 }
34}
35
36impl FromStr for CssSyntax {
37 type Err = ();
38
39 fn from_str(syntax: &str) -> Result<Self, Self::Err> {
40 let syntax = match syntax {
41 "css" => InputSyntax::Css,
42 "scss" => InputSyntax::Scss,
43 "sass" => InputSyntax::Sass,
44 _ => return Err(()),
45 };
46
47 Ok(Self(syntax))
48 }
49}
50
51impl CssSyntax {
52 fn from_path(path: impl AsRef<Path>) -> Self {
53 path.as_ref()
54 .extension()
55 .and_then(OsStr::to_str)
56 .and_then(|ext| Self::from_str(ext.to_lowercase().as_str()).ok())
57 .unwrap_or_default()
58 }
59}
60
61#[derive(Debug)]
62pub struct Css {
63 content: String,
64 dependency: Option<String>,
65}
66
67impl Css {
68 pub fn from_content(content: impl Into<String>, syntax: CssSyntax) -> Result<Self, Error> {
69 Ok(Self {
70 content: Self::css_content(content.into(), syntax)?,
71 dependency: None,
72 })
73 }
74
75 pub fn from_path(path: impl AsRef<Path>, syntax: Option<CssSyntax>) -> Result<Self, Error> {
76 let syntax = syntax.unwrap_or_else(|| CssSyntax::from_path(path.as_ref()));
77 const CARGO_MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR";
78
79 let root_dir = env::var(CARGO_MANIFEST_DIR)
80 .with_context(|| format!("Couldn't read '{CARGO_MANIFEST_DIR}' variable"))?;
81 let path = PathBuf::from(root_dir)
82 .join(path)
83 .into_os_string()
84 .into_string()
85 .map_err(|filename| anyhow!("Couldn't convert filename to string: '{filename:?}'"))?;
86
87 Ok(Self {
88 content: Self::css_content(
89 fs::read_to_string(&path)
90 .with_context(|| format!("Couldn't read file '{path}'"))?,
91 syntax,
92 )?,
93 dependency: Some(path),
94 })
95 }
96
97 fn css_content(source: String, syntax: CssSyntax) -> Result<String, Error> {
98 let syntax = syntax.into();
99
100 let css = if syntax != InputSyntax::Css {
101 grass::from_string(source, &grass::Options::default().input_syntax(syntax))
102 .context("Error parsing CSS")?
103 } else {
104 source
105 };
106
107 Ok(css)
108 }
109
110 pub fn transpile(
111 &mut self,
112 validate: bool,
113 transpile: Option<Transpile>,
114 ) -> Result<Option<Vec<NameMapping>>, TranspileError> {
115 if validate || transpile.is_some() {
116 transpile::transpile(self, validate, transpile)
117 } else {
118 Ok(None)
119 }
120 }
121
122 pub fn dependency(&self) -> Option<&str> {
123 self.dependency.as_deref()
124 }
125
126 pub fn content(&self) -> &str {
127 &self.content
128 }
129
130 pub fn class_names(&self) -> impl Iterator<Item = String> {
131 let mut parser_input = ParserInput::new(&self.content);
132 let mut input = Parser::new(&mut parser_input);
133 let mut classes = HashSet::new();
134 let mut prev_dot = false;
135
136 while let Ok(token) = input.next_including_whitespace_and_comments() {
137 if prev_dot {
138 if let Token::Ident(class) = token {
139 classes.insert(class.to_string());
140 }
141 }
142
143 prev_dot = matches!(token, Token::Delim('.'));
144 }
145
146 classes.into_iter()
147 }
148
149 pub fn variable_names(&self) -> impl Iterator<Item = String> {
150 let mut parser_input = ParserInput::new(&self.content);
151 let mut input = Parser::new(&mut parser_input);
152 let mut variables = HashSet::new();
153 let mut tokens = Vec::new();
154
155 flattened_tokens(&mut tokens, &mut input);
156
157 for token in tokens {
158 if let Token::Ident(ident) = token {
159 if let Some(var) = ident.strip_prefix("--") {
160 variables.insert(var.to_string());
161 }
162 }
163 }
164
165 variables.into_iter()
166 }
167}
168
169pub struct Transpile {
170 pub minify: bool,
171 pub pretty: bool,
172 pub modules: bool,
173 pub nesting: bool,
174 pub browsers: Option<Browsers>,
175}
176
177#[derive(Default)]
178pub struct Browsers {
179 pub android: Option<Version>,
180 pub chrome: Option<Version>,
181 pub edge: Option<Version>,
182 pub firefox: Option<Version>,
183 pub ie: Option<Version>,
184 pub ios_saf: Option<Version>,
185 pub opera: Option<Version>,
186 pub safari: Option<Version>,
187 pub samsung: Option<Version>,
188}
189
190#[derive(Error, Debug)]
191#[error(transparent)]
192pub struct Error(#[from] anyhow::Error);
193
194#[derive(Error, Debug)]
195pub enum TranspileError {
196 #[error("Transpilation requires `css-transpile` feature")]
197 Disabled,
198 #[error("Transpile failed: {0}")]
199 Failed(#[from] Error),
200}
201
202impl From<anyhow::Error> for TranspileError {
203 fn from(value: anyhow::Error) -> Self {
204 Self::Failed(value.into())
205 }
206}
207
208fn flattened_tokens<'i>(tokens: &mut Vec<Token<'i>>, input: &mut Parser<'i, '_>) {
209 while let Ok(token) = input.next_including_whitespace_and_comments() {
210 tokens.push(token.clone());
211
212 match token {
213 Token::ParenthesisBlock
214 | Token::CurlyBracketBlock
215 | Token::SquareBracketBlock
216 | Token::Function(_) => {
217 input
218 .parse_nested_block(|parser| -> Result<(), cssparser::ParseError<()>> {
219 flattened_tokens(tokens, parser);
220 Ok(())
221 })
222 .unwrap();
223 }
224 _ => (),
225 }
226 }
227}