use std::path::Path;
use syn::visit::{self, Visit};
use syn::{Expr, Item};
pub struct AssetVisitor<'a> {
assets: Vec<(String, usize, String)>, _phantom: std::marker::PhantomData<&'a ()>,
}
impl<'a> AssetVisitor<'a> {
pub fn new(_source_file: &'a Path) -> Self {
Self {
assets: Vec::new(),
_phantom: std::marker::PhantomData,
}
}
pub fn assets(&self) -> &[(String, usize, String)] {
&self.assets
}
fn check_macro(&mut self, mac: &syn::Macro) {
let path_str = quote::quote!(#mac.path).to_string();
if path_str.contains("include_bytes") || path_str.contains("include_str") {
let tokens = mac.tokens.to_string();
if let Some(file_path) = Self::extract_string_literal(&tokens) {
let method = if path_str.contains("include_bytes") {
"include_bytes!"
} else {
"include_str!"
};
self.assets.push((file_path, 0, method.to_string())); }
}
}
pub fn extract_string_literal(tokens: &str) -> Option<String> {
let cleaned = tokens.trim().trim_matches('"').trim();
if !cleaned.is_empty() {
Some(cleaned.to_string())
} else {
None
}
}
}
impl<'a> Visit<'a> for AssetVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
if let Expr::Macro(macro_expr) = expr {
self.check_macro(¯o_expr.mac);
}
visit::visit_expr(self, expr);
}
fn visit_item(&mut self, item: &'a Item) {
visit::visit_item(self, item);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_string_literal_valid_strings_extracts_content() {
assert_eq!(
AssetVisitor::extract_string_literal(r#""path/to/file.png""#),
Some("path/to/file.png".to_string())
);
assert_eq!(
AssetVisitor::extract_string_literal(r#" "file.txt" "#),
Some("file.txt".to_string())
);
}
#[test]
fn test_extract_string_literal_invalid_formats_returns_none() {
assert_eq!(AssetVisitor::extract_string_literal(""), None);
assert_eq!(AssetVisitor::extract_string_literal(r#""""#), None);
assert_eq!(AssetVisitor::extract_string_literal(" "), None);
}
}