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
use crate::error::Error;
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<(Self, String, Self::File)>, Error> {
///         if let Some(file) = self.files.get(name).map(|data| *data) {
///             Ok(Some((self.clone(), 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.
    ///
    /// 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<(Self, String, Self::File)>, Error>;
}

/// 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.
///
/// # Example
/// ```
/// use rsass::FsFileContext;
/// use std::path::PathBuf;
///
/// let base = FsFileContext::new();
/// let (base, file1) =
///     base.file(&PathBuf::from("some").join("dir").join("file.scss"));
/// // base is now a relative to file1, usefull to open files
/// // by paths mentioned in file1.
/// let (base, file2) = base.file("some/other.scss".as_ref());
/// assert_eq!(file1, PathBuf::from("some").join("dir").join("file.scss"));
/// assert_eq!(file2, PathBuf::from("some").join("dir").join("some/other.scss"));
/// ```
#[derive(Clone, Debug)]
pub struct FsFileContext {
    path: Vec<PathBuf>,
}

impl FsFileContext {
    /// Create a new FsFileContext.
    ///
    /// Files will be resolved from the current working directory.
    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());
    }

    /// Get a file from this context.
    ///
    /// Get a path and a FsFileContext from this FsFileContext and a path.
    pub fn file(&self, file: &Path) -> (Self, PathBuf) {
        let t = self.path[0].join(file);
        let mut path = vec![];
        if let Some(dir) = t.parent() {
            path.push(PathBuf::from(dir));
        }
        path.extend_from_slice(&self.path);
        (Self { path }, t)
    }
}

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

    fn find_file(
        &self,
        name: &str,
    ) -> Result<Option<(Self, String, Self::File)>, crate::Error> {
        // TODO Check docs what expansions should be tried!
        // Files with .sass extension needs another parser.
        let name = Path::new(name);
        let parent = name.parent();
        if let Some(name) = name.file_name().and_then(|n| n.to_str()) {
            for base in &self.path {
                for name in &[
                    name,
                    &format!("{}.scss", name),
                    &format!("_{}.scss", name),
                    &format!("{}/index.scss", name),
                    &format!("{}/_index.scss", name),
                ] {
                    let full = if let Some(parent) = parent {
                        base.join(parent).join(name)
                    } else {
                        base.join(name)
                    };
                    if full.is_file() {
                        let c = if let Some(parent) = parent {
                            let mut path = vec![];
                            path.push(PathBuf::from(parent));
                            path.extend_from_slice(&self.path);
                            Self { path }
                        } else {
                            self.clone()
                        };
                        let path = full.display().to_string();
                        return match Self::File::open(full) {
                            Ok(file) => Ok(Some((c, path, file))),
                            Err(e) => Err(Error::Input(path, e)),
                        };
                    }
                }
            }
        }
        Ok(None)
    }
}