Reorganize

This commit is contained in:
2025-11-02 11:08:51 -08:00
parent 14d8a9b00c
commit fd48f75245
75 changed files with 8 additions and 17 deletions

View File

@@ -0,0 +1,15 @@
[package]
name = "macro-assets"
version = { workspace = true }
rust-version = { workspace = true }
edition = { workspace = true }
[lints]
workspace = true
[lib]
proc-macro = true
[dependencies]
syn = { workspace = true }
quote = { workspace = true }

View File

@@ -0,0 +1,239 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Ident, LitStr, Result, Token, braced,
parse::{Parse, ParseStream},
parse_macro_input,
};
/// A macro for generating static asset handlers with compile-time embedding.
///
/// This macro generates:
/// - Individual structs for each asset that implement the `assetserver::Asset` trait
/// - Compile-time embedding of asset files using `include_bytes!`
/// - Optionally, an Axum router function that serves all assets
///
/// # Syntax
///
/// ```notrust
/// assets! {
/// prefix: "/assets"
/// router: router_function_name()
///
/// AssetName {
/// source: "path/to/file.ext",
/// target: "/public-url.ext"
/// }
///
/// AnotherAsset {
/// source: "path/to/another.ext",
/// target: "/another-url.ext"
/// }
/// }
/// ```
///
/// # Arguments
///
/// - `prefix`: The URL prefix for all assets (e.g., "/assets")
/// - `router`: (Optional) The name of a function to generate that returns `(&'static str, Router<()>)`
/// with routes for all assets
/// - Asset blocks: Each block defines an asset with:
/// - A name (identifier) for the generated struct
/// - `source`: The file system path to the asset (relative to the current file)
/// - `target`: The URL path where the asset will be served
///
/// # Generated Code
///
/// For each asset, the macro generates:
/// - A struct with the specified name
/// - An `assetserver::Asset` trait implementation containing:
/// - `URL_PREFIX`: The common prefix for all assets
/// - `URL`: The specific URL path for this asset
/// - `BYTES`: The embedded file contents as a byte slice
/// - Documentation showing the original asset definition
///
/// If `router` is specified, also generates a function that returns an Axum router
/// with all assets mounted at their target URLs.
///
/// # Example
///
/// ```notrust
/// assets! {
/// prefix: "/static"
/// router: static_router()
///
/// Logo {
/// source: "../images/logo.png",
/// target: "/logo.png"
/// }
/// }
/// ```
///
/// This generates structs implementing `assetserver::Asset` and optionally a router function:
///
/// ```notrust
/// pub fn static_router() -> (&'static str, ::axum::Router<()>) {
/// let router = ::axum::Router::new()
/// .route(Logo::URL, ::axum::routing::get(|| async {
/// (::axum::http::StatusCode::OK, Logo::BYTES)
/// }));
/// ("/static", router)
/// }
/// ```
#[proc_macro]
pub fn assets(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as AssetsInput);
let prefix = &input.prefix;
let asset_impls = input.assets.iter().map(|asset| {
let name = &asset.name;
let source = &asset.source;
let target = &asset.target;
// Generate documentation showing the original asset definition
let doc = format!(
"This is an `asset!`\n```notrust\n{} {{\n\tsource: \"{}\",\n\ttarget: \"{}\"\n}}\n```",
name, source, target
);
quote! {
#[expect(clippy::allow_attributes)]
#[allow(non_camel_case_types)]
#[doc = #doc]
pub struct #name {}
impl ::assetserver::Asset for #name {
const URL_PREFIX: &'static str = #prefix;
const URL_POSTFIX: &'static str = #target;
const URL: &'static str = concat!(#prefix, #target);
const BYTES: &'static [u8] = include_bytes!(#source);
}
}
});
// Generate the router function if specified
let router_fn = if let Some(router_name) = &input.router {
let route_definitions = input.assets.iter().map(|asset| {
let name = &asset.name;
quote! {
.route(#name::URL_POSTFIX, ::axum::routing::get(|| async {
(::axum::http::StatusCode::OK, #name::BYTES)
}))
}
});
let router_doc = format!(
"Generated router function that serves {} asset(s) with prefix \"{}\"",
input.assets.len(),
prefix
);
quote! {
#[doc = #router_doc]
pub fn #router_name() -> (&'static str, ::axum::Router<()>) {
let router = ::axum::Router::new()
#(#route_definitions)*;
(#prefix, router)
}
}
} else {
quote! {}
};
let expanded = quote! {
#(#asset_impls)*
#router_fn
};
TokenStream::from(expanded)
}
/// Represents the complete input to the `assets!` macro
struct AssetsInput {
prefix: String,
router: Option<Ident>,
assets: Vec<AssetDefinition>,
}
/// Represents a single asset definition within the macro
struct AssetDefinition {
name: Ident,
source: String,
target: String,
}
impl Parse for AssetsInput {
fn parse(input: ParseStream<'_>) -> Result<Self> {
// Parse "prefix:"
let _prefix_ident: Ident = input.parse()?;
let _colon: Token![:] = input.parse()?;
let prefix_lit: LitStr = input.parse()?;
let prefix = prefix_lit.value();
// Try to parse optional "router:" parameter
let router = if input.peek(Ident) {
let peek_ident: Ident = input.fork().parse()?;
if peek_ident == "router" {
let _router_ident: Ident = input.parse()?;
let _colon: Token![:] = input.parse()?;
let router_name: Ident = input.parse()?;
// Parse the parentheses after the function name
let _paren_content;
syn::parenthesized!(_paren_content in input);
Some(router_name)
} else {
None
}
} else {
None
};
let mut assets = Vec::new();
// Parse asset definitions until we reach the end
while !input.is_empty() {
let asset = input.parse::<AssetDefinition>()?;
assets.push(asset);
}
Ok(AssetsInput {
prefix,
router,
assets,
})
}
}
impl Parse for AssetDefinition {
fn parse(input: ParseStream<'_>) -> Result<Self> {
// Parse the asset name
let name: Ident = input.parse()?;
// Parse the braced content
let content;
braced!(content in input);
// Parse "source:"
let _source_ident: Ident = content.parse()?;
let _colon1: Token![:] = content.parse()?;
let source_lit: LitStr = content.parse()?;
let source = source_lit.value();
let _comma1: Token![,] = content.parse()?;
// Parse "target:"
let _target_ident: Ident = content.parse()?;
let _colon2: Token![:] = content.parse()?;
let target_lit: LitStr = content.parse()?;
let target = target_lit.value();
// Optional trailing comma
let _ = content.parse::<Token![,]>();
Ok(AssetDefinition {
name,
source,
target,
})
}
}

View File

@@ -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 }

View File

@@ -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)
}