oxedyne/fe2o3/fe2o3_steel/src/srv/dev/sass.rs
4.6 KiB, 13 runs
created by r1870400018:985, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! A Sass/SCSS compiler using the Grass engine. |
| 2 | //! |
| 3 | //! This module provides functionality to compile Sass/SCSS files into a single CSS output file. |
| 4 | //! It handles imports, resolves dependencies, and supports path aliases for `@use` and `@import` rules. |
| 5 | //! |
| 6 | //! # Usage Example |
| 7 | //! ```rust,no_run |
| 8 | //! use oxedyne_fe2o3_core::prelude::*; |
| 9 | //! use oxedyne_fe2o3_steel::app::dev::sass::SassBundle; |
| 10 | //! use std::path::Path; |
| 11 | //! |
| 12 | //! let mut bundler = SassBundle::new(); |
| 13 | //! |
| 14 | //! // Optional: Configure load paths for Sass imports. |
| 15 | //! bundler.add_load_path("node_modules"); |
| 16 | //! |
| 17 | //! // Compile all Sass/SCSS files. |
| 18 | //! res!(bundler.compile_directory( |
| 19 | //! // Source directory containing entry point files. |
| 20 | //! Path::new("src/styles"), |
| 21 | //! // Output CSS file. |
| 22 | //! Path::new("dist/styles.css"), |
| 23 | //! )); |
| 24 | //! ``` |
| 25 | |
| 26 | use oxedyne_fe2o3_core::prelude::*; |
| 27 | |
| 28 | use std::{ |
| 29 | collections::{ |
| 30 | HashSet, |
| 31 | }, |
| 32 | path::{ |
| 33 | Path, |
| 34 | PathBuf, |
| 35 | }, |
| 36 | fs, |
| 37 | }; |
| 38 | |
| 39 | use grass::{ |
| 40 | self, |
| 41 | Options, |
| 42 | OutputStyle, |
| 43 | }; |
| 44 | |
| 45 | |
| 46 | #[derive(Debug)] |
| 47 | #[allow(dead_code)] |
| 48 | struct StyleInfo { |
| 49 | path: PathBuf, |
| 50 | content: String, |
| 51 | } |
| 52 | |
| 53 | pub struct SassBundle { |
| 54 | load_paths: Vec<PathBuf>, |
| 55 | processed: HashSet<PathBuf>, |
| 56 | styles: Vec<StyleInfo>, |
| 57 | } |
| 58 | |
| 59 | impl SassBundle { |
| 60 | pub fn new() -> Self { |
| 61 | Self { |
| 62 | load_paths: Vec::new(), |
| 63 | processed: HashSet::new(), |
| 64 | styles: Vec::new(), |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | /// Add a path to search for Sass/SCSS imports. |
| 69 | /// This is used to resolve `@use` and `@import` rules. |
| 70 | pub fn add_load_path<P: AsRef<Path>>( |
| 71 | &mut self, |
| 72 | path: P, |
| 73 | ) { |
| 74 | self.load_paths.push(path.as_ref().to_path_buf()); |
| 75 | } |
| 76 | |
| 77 | pub fn compile_directory( |
| 78 | mut self, |
| 79 | css_paths: &(PathBuf, PathBuf), |
| 80 | ) |
| 81 | -> Outcome<()> |
| 82 | { |
| 83 | // Find entry points in input directory |
| 84 | let mut entry_files = Vec::new(); |
| 85 | res!(self.collect_sass_files(&css_paths.0, &mut entry_files)); |
| 86 | |
| 87 | // Process each entry point |
| 88 | for entry in entry_files { |
| 89 | res!(self.process_style( |
| 90 | &entry, |
| 91 | )); |
| 92 | } |
| 93 | |
| 94 | // Bundle in order of processing |
| 95 | let mut bundled = String::new(); |
| 96 | for style in &self.styles { |
| 97 | bundled.push_str(&style.content); |
| 98 | bundled.push('\n'); |
| 99 | } |
| 100 | |
| 101 | // Write bundled output |
| 102 | res!(fs::write(css_paths.1.clone(), bundled)); |
| 103 | |
| 104 | Ok(()) |
| 105 | } |
| 106 | |
| 107 | fn process_style( |
| 108 | &mut self, |
| 109 | file_path: &Path, |
| 110 | ) |
| 111 | -> Outcome<()> |
| 112 | { |
| 113 | if self.processed.contains(file_path) { |
| 114 | return Ok(()); |
| 115 | } |
| 116 | debug!("Processing {:?}", file_path); |
| 117 | let content = res!(fs::read_to_string(file_path)); |
| 118 | let mut options = Options::default().style(OutputStyle::Compressed); |
| 119 | |
| 120 | // Get directory containing the current file for import resolution. |
| 121 | let import_dir = match file_path.parent() { |
| 122 | Some(dir) => dir, |
| 123 | None => return Err(err!( |
| 124 | "Could not get parent directory of {:?}.", |
| 125 | file_path; |
| 126 | Path)), |
| 127 | }; |
| 128 | options = options.load_path(import_dir); |
| 129 | |
| 130 | // Add all configured load paths to options. |
| 131 | for load_path in &self.load_paths { |
| 132 | if let Some(path_str) = load_path.to_str() { |
| 133 | options = options.load_path(path_str); |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | let css = match grass::from_string( |
| 138 | content, |
| 139 | &options, |
| 140 | ) { |
| 141 | Ok(css) => css, |
| 142 | Err(e) => return Err(err!(e, |
| 143 | "Why trying to compile Sass file {:?}", file_path; |
| 144 | IO, Format)), |
| 145 | }; |
| 146 | |
| 147 | self.styles.push(StyleInfo { |
| 148 | path: file_path.to_path_buf(), |
| 149 | content: css, |
| 150 | }); |
| 151 | self.processed.insert(file_path.to_path_buf()); |
| 152 | debug!(" Successful processing {:?}", file_path); |
| 153 | Ok(()) |
| 154 | } |
| 155 | |
| 156 | fn collect_sass_files( |
| 157 | &self, |
| 158 | dir: &Path, |
| 159 | files: &mut Vec<PathBuf>, |
| 160 | ) |
| 161 | -> Outcome<()> |
| 162 | { |
| 163 | if dir.is_dir() { |
| 164 | for entry in res!(fs::read_dir(dir)) { |
| 165 | let entry = res!(entry); |
| 166 | let path = entry.path(); |
| 167 | if path.is_dir() { |
| 168 | res!(self.collect_sass_files(&path, files)); |
| 169 | } else if let Some(ext) = path.extension() { |
| 170 | if ext == "sass" || ext == "scss" { |
| 171 | files.push(path); |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | } |
| 176 | Ok(()) |
| 177 | } |
| 178 | } |