1use std::{
2 collections::VecDeque,
3 env,
4 fmt::Debug,
5 fs,
6 path::{Path, PathBuf},
7 sync::Arc,
8 time::{SystemTime, UNIX_EPOCH},
9};
10
11use parking_lot::Mutex;
12use regex::Regex;
13use urlencoding::encode;
14
15use crate::{
16 Exception, Importer, ImporterOptions, ImporterResult, Result, Syntax, Url,
17};
18
19use super::{LegacyImporterResult, LegacyImporterThis, LegacyPluginThis};
20
21pub(crate) const END_OF_LOAD_PROTOCOL: &str = "sass-embedded-legacy-load-done:";
22pub(crate) const LEGACY_IMPORTER_PROTOCOL: &str = "legacy-importer:";
23
24pub trait LegacyImporter: Debug + Sync + Send {
26 fn call(
28 &self,
29 this: &LegacyImporterThis,
30 url: &str,
31 prev: &str,
32 ) -> Result<Option<LegacyImporterResult>>;
33}
34
35pub type BoxLegacyImporter = Box<dyn LegacyImporter>;
37
38impl<I: 'static + LegacyImporter> From<I> for BoxLegacyImporter {
39 fn from(importer: I) -> Self {
40 Box::new(importer)
41 }
42}
43
44#[derive(Debug)]
45pub(crate) struct LegacyImporterWrapper {
46 prev_stack: Mutex<Vec<PreviousUrl>>,
47 last_contents: Mutex<Option<String>>,
48 expecting_relative_load: Mutex<bool>,
49 callbacks: Vec<BoxLegacyImporter>,
50 this: LegacyPluginThis,
51 load_paths: Vec<PathBuf>,
52}
53
54impl LegacyImporterWrapper {
55 pub fn new(
56 this: LegacyPluginThis,
57 callbacks: Vec<BoxLegacyImporter>,
58 load_paths: Vec<PathBuf>,
59 initial_prev: &str,
60 ) -> Arc<Self> {
61 let path = initial_prev != "stdin";
62 Arc::new(Self {
63 prev_stack: Mutex::new(vec![PreviousUrl {
64 url: if path {
65 initial_prev.to_string()
66 } else {
67 "stdin".to_string()
68 },
69 path,
70 }]),
71 last_contents: Mutex::new(None),
72 expecting_relative_load: Mutex::new(true),
73 callbacks,
74 this,
75 load_paths,
76 })
77 }
78
79 fn invoke_callbacks(
80 &self,
81 url: &str,
82 prev: &str,
83 options: &ImporterOptions,
84 ) -> Result<Option<LegacyImporterResult>> {
85 assert!(!self.callbacks.is_empty());
86
87 let this = LegacyImporterThis {
88 options: self.this.options.clone(),
89 from_import: options.from_import,
90 };
91 for callback in &self.callbacks {
92 match callback.call(&this, url, prev) {
93 Ok(Some(result)) => return Ok(Some(result)),
94 Ok(None) => continue,
95 Err(e) => return Err(e),
96 }
97 }
98 Ok(None)
99 }
100}
101
102impl Importer for Arc<LegacyImporterWrapper> {
103 fn canonicalize(
104 &self,
105 url: &str,
106 options: &ImporterOptions,
107 ) -> Result<Option<url::Url>> {
108 if url.starts_with(END_OF_LOAD_PROTOCOL) {
109 return Ok(Some(Url::parse(url).unwrap()));
110 }
111
112 let mut prev_stack = self.prev_stack.lock();
113
114 let mut expecting_relative_load = self.expecting_relative_load.lock();
115 if *expecting_relative_load {
116 if url.starts_with("file:") {
117 let path = url_to_file_path_cross_platform(&Url::parse(url).unwrap());
118 let resolved = resolve_path(path, options.from_import)?;
119 if let Some(p) = resolved {
120 prev_stack.push(PreviousUrl {
121 url: p.to_string_lossy().to_string(),
122 path: true,
123 });
124 return Ok(Some(Url::from_file_path(p).unwrap()));
125 }
126 }
127 *expecting_relative_load = false;
128 return Ok(None);
129 } else {
130 *expecting_relative_load = true;
131 }
132
133 let prev = prev_stack.last().unwrap();
134 let result = match self.invoke_callbacks(url, &prev.url, options) {
135 Err(e) => Err(e),
136 Ok(None) => Ok(None),
137 Ok(Some(result)) => match result {
138 LegacyImporterResult::Contents { contents, file } => {
139 *self.last_contents.lock() = Some(contents);
140 Ok(Some(if let Some(file) = file {
141 Url::parse(&format!(
142 "{}{}",
143 LEGACY_IMPORTER_PROTOCOL,
144 encode(&file.to_string_lossy())
145 ))
146 .unwrap()
147 } else if Regex::new("^[A-Za-z+.-]+:").unwrap().is_match(url) {
148 Url::parse(url).unwrap()
149 } else {
150 Url::parse(&format!("{}{}", LEGACY_IMPORTER_PROTOCOL, encode(url)))
151 .unwrap()
152 }))
153 }
154 LegacyImporterResult::File(file) => {
155 if file.is_absolute() {
156 let resolved = resolve_path(file, options.from_import)?;
157 Ok(resolved.map(|p| Url::from_file_path(p).unwrap()))
158 } else {
159 let mut prefixes = VecDeque::from(self.load_paths.clone());
160 prefixes.push_back(PathBuf::from("."));
161 if prev.path {
162 prefixes.push_front(
163 Path::new(&prev.url).parent().unwrap().to_path_buf(),
164 );
165 }
166 let mut resolved = None;
167 for prefix in prefixes {
168 if let Some(p) = resolve_path(
169 Path::new(&prefix).join(file.clone()),
170 options.from_import,
171 )? {
172 let p = if p.is_absolute() {
173 p
174 } else {
175 env::current_dir().unwrap().join(p)
176 };
177 resolved = Some(Url::from_file_path(p).unwrap());
178 break;
179 }
180 }
181 Ok(resolved)
182 }
183 }
184 },
185 }?;
186 if let Some(result) = &result {
187 let path = result.scheme() == "file";
188 prev_stack.push(PreviousUrl {
189 url: if path {
190 url_to_file_path_cross_platform(result)
191 .to_string_lossy()
192 .to_string()
193 } else {
194 url.to_string()
195 },
196 path,
197 });
198 } else {
199 for load_path in &self.load_paths {
200 let resolved =
201 resolve_path(Path::new(&load_path).join(url), options.from_import)?;
202 if let Some(p) = resolved {
203 return Ok(Some(Url::from_file_path(p).unwrap()));
204 }
205 }
206 }
207 Ok(result)
208 }
209
210 fn load(&self, canonical_url: &Url) -> Result<Option<ImporterResult>> {
211 let protocol = format!("{}:", canonical_url.scheme());
212 if protocol == END_OF_LOAD_PROTOCOL {
213 self.prev_stack.lock().pop();
214 return Ok(Some(ImporterResult {
215 contents: String::new(),
216 source_map_url: Some(Url::parse(END_OF_LOAD_PROTOCOL).unwrap()),
217 syntax: Syntax::Scss,
218 }));
219 }
220 let timestamp = SystemTime::now()
221 .duration_since(UNIX_EPOCH)
222 .unwrap()
223 .as_micros();
224 if protocol == "file:" {
225 let syntax = if canonical_url.path().ends_with(".sass") {
226 Syntax::Indented
227 } else if canonical_url.path().ends_with(".css") {
228 Syntax::Css
229 } else {
230 Syntax::Scss
231 };
232 let mut last_contents = self.last_contents.lock();
233 let contents = last_contents.clone().unwrap_or_else(|| {
234 fs::read_to_string(url_to_file_path_cross_platform(canonical_url))
235 .unwrap()
236 });
237 *last_contents = None;
238 let contents = match syntax {
239 Syntax::Scss => {
240 format!("{contents}\n;@import \"{END_OF_LOAD_PROTOCOL}{timestamp}\"")
241 }
242 Syntax::Indented => {
243 format!("{contents}\n@import \"{END_OF_LOAD_PROTOCOL}{timestamp}\"")
244 }
245 Syntax::Css => {
246 self.prev_stack.lock().pop();
247 contents
248 }
249 };
250 return Ok(Some(ImporterResult {
251 contents,
252 syntax,
253 source_map_url: Some(canonical_url.clone()),
254 }));
255 }
256 let mut last_contents = self.last_contents.lock();
257 assert!(last_contents.is_some());
258 let contents = format!(
259 "{}\n;@import \"{END_OF_LOAD_PROTOCOL}{timestamp}\"",
260 last_contents.clone().unwrap()
261 );
262 *last_contents = None;
263 Ok(Some(ImporterResult {
264 contents,
265 syntax: Syntax::Scss,
266 source_map_url: Some(canonical_url.clone()),
267 }))
268 }
269}
270
271#[derive(Debug)]
272struct PreviousUrl {
273 url: String,
274 path: bool,
275}
276
277pub(crate) fn url_to_file_path_cross_platform(file_url: &Url) -> PathBuf {
278 let p = file_url
279 .to_file_path()
280 .unwrap()
281 .to_string_lossy()
282 .to_string();
283 if Regex::new("^/[A-Za-z]:/").unwrap().is_match(&p) {
284 PathBuf::from(&p[1..])
285 } else {
286 PathBuf::from(p)
287 }
288}
289
290fn resolve_path(path: PathBuf, from_import: bool) -> Result<Option<PathBuf>> {
291 let extension = path.extension();
292 if let Some(extension) = extension {
293 if extension == "sass" || extension == "scss" || extension == "css" {
294 if from_import {
295 if let Ok(Some(p)) = exactly_one(try_path(Path::new(&format!(
296 "{}.import.{}",
297 without_extension(&path).to_string_lossy(),
298 extension.to_string_lossy()
299 )))) {
300 return Ok(Some(p));
301 }
302 }
303 return exactly_one(try_path(&path));
304 }
305 }
306 if from_import {
307 if let Ok(Some(p)) = exactly_one(try_path_with_extensions(Path::new(
308 &format!("{}.import", path.file_stem().unwrap().to_string_lossy()),
309 ))) {
310 return Ok(Some(p));
311 }
312 }
313 if let Ok(Some(p)) = exactly_one(try_path_with_extensions(&path)) {
314 return Ok(Some(p));
315 }
316 try_path_as_directory(&path.join("index"), from_import)
317}
318
319fn exactly_one(paths: Vec<PathBuf>) -> Result<Option<PathBuf>> {
320 if paths.is_empty() {
321 Ok(None)
322 } else if paths.len() == 1 {
323 Ok(Some(paths[0].clone()))
324 } else {
325 Err(
326 Exception::new(format!(
327 "It's not clear which file to import. Found:\n{}",
328 paths
329 .iter()
330 .map(|p| format!(" {}", p.to_string_lossy()))
331 .collect::<Vec<String>>()
332 .join("\n")
333 ))
334 .into(),
335 )
336 }
337}
338
339fn dir_exists(path: &Path) -> bool {
340 path.exists() && path.is_dir()
341}
342
343fn file_exists(path: &Path) -> bool {
344 path.exists() && path.is_file()
345}
346
347fn try_path_as_directory(
348 path: &Path,
349 from_import: bool,
350) -> Result<Option<PathBuf>> {
351 if !dir_exists(path) {
352 return Ok(None);
353 }
354 if from_import {
355 if let Ok(Some(p)) =
356 exactly_one(try_path_with_extensions(&path.join("index.import")))
357 {
358 return Ok(Some(p));
359 }
360 }
361 exactly_one(try_path_with_extensions(&path.join("index")))
362}
363
364fn try_path_with_extensions(path: &Path) -> Vec<PathBuf> {
365 let result = [
366 try_path(Path::new(&format!("{}.sass", path.to_string_lossy()))),
367 try_path(Path::new(&format!("{}.scss", path.to_string_lossy()))),
368 ]
369 .concat();
370 if result.is_empty() {
371 try_path(Path::new(&format!("{}.css", path.to_string_lossy())))
372 } else {
373 result
374 }
375}
376
377fn try_path(path: &Path) -> Vec<PathBuf> {
378 let partial = path
379 .parent()
380 .unwrap()
381 .join(format!("_{}", path.file_name().unwrap().to_string_lossy()));
382 let mut result = Vec::new();
383 if file_exists(&partial) {
384 result.push(partial);
385 }
386 if file_exists(path) {
387 result.push(path.to_path_buf());
388 }
389 result
390}
391
392fn without_extension(path: &Path) -> PathBuf {
393 let mut result = path.to_path_buf();
394 result.set_extension("");
395 result
396}