diff --git a/crates/macro-sass/Cargo.toml b/crates/macro-sass/Cargo.toml new file mode 100644 index 0000000..c2f54ad --- /dev/null +++ b/crates/macro-sass/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "macro-sass" +version = { workspace = true } +rust-version = { workspace = true } +edition = { workspace = true } + +[lints] +workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { workspace = true } +quote = { workspace = true } +grass = { workspace = true } diff --git a/crates/macro-sass/src/lib.rs b/crates/macro-sass/src/lib.rs new file mode 100644 index 0000000..4d0ba08 --- /dev/null +++ b/crates/macro-sass/src/lib.rs @@ -0,0 +1,157 @@ +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) +}