1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
use crate::{Error, SourceFile, SourceName, SourcePos};
use std::borrow::Cow;
use std::path::{Path, PathBuf};

/// A file context manages finding and loading files.
///
/// # Example
/// ```
/// use std::collections::HashMap;
/// use rsass::{FileContext, Error};
///
/// #[derive(Clone, Debug)]
/// struct StaticFileContext<'a> {
///     files: HashMap<String, &'a[u8]>,
/// }
///
/// impl<'a> FileContext for StaticFileContext<'a> {
///     type File = &'a [u8];
///
///     fn find_file(
///         &self, name: &str
///     ) -> Result<Option<(String, Self::File)>, Error> {
///         if let Some(file) = self.files.get(name).map(|data| *data) {
///             Ok(Some((name.to_string(), file)))
///         } else {
///             Ok(None)
///         }
///     }
/// }
/// ```
pub trait FileContext: Sized + std::fmt::Debug {
    /// Anything that can be read can be a File in an implementation.
    type File: std::io::Read;

    /// Find a file for `@import`
    ///
    /// This includes "import-only" filenames, otherwise the same as [`#find_file_use`].
    #[tracing::instrument]
    fn find_file_import(
        &self,
        url: &str,
        from: SourcePos,
    ) -> Result<Option<SourceFile>, Error> {
        let names: &[&dyn Fn(&str, &str) -> String] = &[
            // base will either be empty or end with a slash.
            &|base, name| format!("{}{}.import.scss", base, name),
            &|base, name| format!("{}_{}.import.scss", base, name),
            &|base, name| format!("{}{}.scss", base, name),
            &|base, name| format!("{}_{}.scss", base, name),
            &|base, name| format!("{}{}/index.import.scss", base, name),
            &|base, name| format!("{}{}/_index.import.scss", base, name),
            &|base, name| format!("{}{}/index.scss", base, name),
            &|base, name| format!("{}{}/_index.scss", base, name),
            &|base, name| format!("{}{}.css", base, name),
            &|base, name| format!("{}_{}.css", base, name),
        ];
        // Note: Should a "full stack" of bases be used here?
        // Or is this fine?
        let url = relative(from.file_url(), url);
        if let Some((path, mut file)) = do_find_file(self, &url, names)? {
            let source = SourceName::imported(path, from);
            Ok(Some(SourceFile::read(&mut file, source)?))
        } else {
            Ok(None)
        }
    }

    /// Find a file for `@use`
    #[tracing::instrument]
    fn find_file_use(
        &self,
        url: &str,
        from: SourcePos,
    ) -> Result<Option<SourceFile>, Error> {
        let names: &[&dyn Fn(&str, &str) -> String] = &[
            // base will either be empty or end with a slash.
            &|base, name| format!("{}{}.scss", base, name),
            &|base, name| format!("{}_{}.scss", base, name),
            &|base, name| format!("{}{}/index.scss", base, name),
            &|base, name| format!("{}{}/_index.scss", base, name),
            &|base, name| format!("{}{}.css", base, name),
            &|base, name| format!("{}_{}.css", base, name),
        ];
        // Note: Should a "full stack" of bases be used here?
        // Or is this fine?
        let url = relative(from.file_url(), url);
        if let Some((path, mut file)) = do_find_file(self, &url, names)? {
            let source = SourceName::used(path, from);
            Ok(Some(SourceFile::read(&mut file, source)?))
        } else {
            Ok(None)
        }
    }

    /// Find a file.
    ///
    /// If the file is imported from another file,
    /// the argument is the exact string specified in the import declaration.
    ///
    /// The official Sass spec prescribes that files are loaded by
    /// url instead of by path to ensure universal compatibility of style sheets.
    /// This effectively mandates the use of forward slashes on all platforms.
    fn find_file(
        &self,
        url: &str,
    ) -> Result<Option<(String, Self::File)>, Error>;
}

/// Make a url relative to a given base.
fn relative<'a>(base: &str, url: &'a str) -> Cow<'a, str> {
    base.rfind('/')
        .map(|p| base.split_at(p + 1).0)
        .map(|base| format!("{}{}", base, url).into())
        .unwrap_or_else(|| url.into())
}

/// Find a file in a given filecontext matching a url over a set of
/// name rules.
fn do_find_file<FC: FileContext>(
    ctx: &FC,
    url: &str,
    names: &[&dyn Fn(&str, &str) -> String],
) -> Result<Option<(String, FC::File)>, Error> {
    if let Some(result) = ctx.find_file(url)? {
        return Ok(Some(result));
    }

    let (base, name) = url
        .rfind('/')
        .map(|p| url.split_at(p + 1))
        .unwrap_or(("", url));

    for name in names.iter().map(|f| f(base, name)) {
        if let Some(result) = ctx.find_file(&name)? {
            return Ok(Some(result));
        }
    }
    Ok(None)
}

/// A filesystem file context specifies where to find local files.
///
/// When opening an included file, an extended file context is
/// created, to find further included files relative to the file they
/// are inlcuded from.
#[derive(Clone, Debug)]
pub struct FsFileContext {
    path: Vec<PathBuf>,
}

impl FsFileContext {
    /// Create a new FsFileContext.
    ///
    /// Files will be resolved from the current working directory.
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        Self {
            path: vec![PathBuf::new()],
        }
    }

    /// Add a path to search for files.
    pub fn push_path(&mut self, path: &Path) {
        self.path.push(path.into());
    }

    /// Create a FsFilecontext and a SourceFile from a given Path.
    pub fn for_path(path: &Path) -> Result<(Self, SourceFile), Error> {
        let mut f = std::fs::File::open(&path)
            .map_err(|e| Error::Input(path.display().to_string(), e))?;
        let (path, name) = if let Some(base) = path.parent() {
            (
                vec![base.to_path_buf(), PathBuf::new()],
                path.strip_prefix(base).unwrap(),
            )
        } else {
            (vec![PathBuf::new()], path)
        };
        let ctx = Self { path };
        let source = SourceName::root(name.display().to_string());
        let source = SourceFile::read(&mut f, source)?;
        Ok((ctx, source))
    }
}

impl FileContext for FsFileContext {
    type File = std::fs::File;

    fn find_file(
        &self,
        name: &str,
    ) -> Result<Option<(String, Self::File)>, Error> {
        if !name.is_empty() {
            for base in &self.path {
                let full = base.join(name);
                if Path::new(&full).is_file() {
                    tracing::debug!(?full, "opening file");
                    return match Self::File::open(&full) {
                        Ok(file) => Ok(Some((name.to_string(), file))),
                        Err(e) => {
                            Err(Error::Input(full.display().to_string(), e))
                        }
                    };
                }
                tracing::trace!(?full, "Not found");
            }
        }
        Ok(None)
    }
}