use proc_macro::TokenStream; use quote::quote; use std::path::PathBuf; use syn::{LitStr, parse_macro_input}; /// A macro for parsing Sass/SCSS files at compile time. /// /// This macro takes a file path to a Sass/SCSS file, compiles it to CSS at compile time /// using the `grass` compiler, and returns the resulting CSS as a `&'static str`. /// /// # Behavior /// /// Similar to `include_str!`, this macro: /// - Reads the specified file at compile time /// - Compiles the Sass/SCSS to CSS using `grass::from_path()` with default options /// - Handles `@import` and `@use` directives, resolving imported files relative to the main file /// - Embeds the resulting CSS string in the binary /// - Returns a `&'static str` containing the compiled CSS /// /// # Syntax /// /// ```notrust /// sass!("path/to/file.scss") /// ``` /// /// # Arguments /// /// - A string literal containing the path to the Sass/SCSS file (relative to the crate root) /// /// # Example /// /// ```notrust /// // Relative to crate root: looks for src/routes/css/main.scss /// const MY_STYLES: &str = sass!("src/routes/css/main.scss"); /// /// // Use in HTML generation /// html! { /// style { (PreEscaped(MY_STYLES)) } /// } /// ``` /// /// # Import Support /// /// The macro fully supports Sass imports and uses: /// ```notrust /// // main.scss /// @import "variables"; /// @use "mixins"; /// /// .button { /// color: $primary-color; /// } /// ``` /// /// All imported files are resolved relative to the location of the main Sass file. /// /// # Compile-time vs Runtime /// /// Instead of this runtime code: /// ```notrust /// let css = grass::from_path( /// "styles.scss", /// &grass::Options::default() /// ).unwrap(); /// ``` /// /// You can use: /// ```notrust /// const CSS: &str = sass!("styles.scss"); /// ``` /// /// # Panics /// /// This macro will cause a compile error if: /// - The specified file does not exist /// - The file path is invalid /// - The Sass/SCSS file contains syntax errors /// - Any imported files cannot be found /// - The grass compiler fails for any reason /// /// # Note /// /// The file path is relative to the crate root (where `Cargo.toml` is located), determined /// by the `CARGO_MANIFEST_DIR` environment variable. This is similar to how `include!()` works /// but differs from `include_str!()` which is relative to the current file. #[proc_macro] pub fn sass(input: TokenStream) -> TokenStream { let input_lit = parse_macro_input!(input as LitStr); let file_path = match PathBuf::try_from(input_lit.value()) { Ok(x) => x, Err(e) => { return syn::Error::new(input_lit.span(), format!("Invalid path: {e}")) .to_compile_error() .into(); } }; // Not stable yet, we have to use crate-relative paths :( //let span = proc_macro::Span::call_site(); //let source_file = span.source_file(); //let path: PathBuf = source_file.path(); // Use a combination of include_str! and grass compilation // include_str! handles the relative path resolution for us // We generate code that uses include_str! at the user's call site // and compiles it at macro expansion time // First, try to read and compile the file at macro expansion time // The path is interpreted relative to CARGO_MANIFEST_DIR let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()); let full_path = std::path::Path::new(&manifest_dir).join(&file_path); let css = match grass::from_path(&full_path, &grass::Options::default()) { Ok(css) => css, Err(e) => { return syn::Error::new( input_lit.span(), format!( "Failed to compile Sass file '{}': {}", file_path.display(), e, ), ) .to_compile_error() .into(); } }; // Generate code that returns the compiled CSS as a string literal let expanded = quote! { #css }; TokenStream::from(expanded) } #[proc_macro] pub fn sass_str(input: TokenStream) -> TokenStream { let input_lit = parse_macro_input!(input as LitStr); let sass_str = input_lit.value(); let css = match grass::from_string(&sass_str, &grass::Options::default()) { Ok(css) => css, Err(e) => { return syn::Error::new( input_lit.span(), format!("Failed to compile Sass string: {e}."), ) .to_compile_error() .into(); } }; let expanded = quote! { #css }; TokenStream::from(expanded) }