1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use proc_macro::TokenStream;
4use proc_macro2::TokenStream as TokenStream2;
5use quote::quote;
6
7#[proc_macro]
9pub fn path(input: TokenStream) -> TokenStream {
10 if input.is_empty() {
12 return quote! { ::std::path::PathBuf::new() }.into();
13 }
14
15 let Format { expr, args } = syn::parse_macro_input!(input as Format);
17
18 let path_expr = match expr {
19 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(fmt), .. }) => {
20 if args.is_some() || fmt.value().contains(&['{', '}'][..]) {
21 quote! { ::std::path::PathBuf::from(::std::format!(#fmt #args)) }
22 } else {
23 quote! { ::std::path::PathBuf::from(#fmt) }
24 }
25 }
26 _ => quote! { ::std::path::PathBuf::from(#expr) }
27 };
28
29 quote! {{
30 let path = #path_expr;
31 if path.to_string_lossy().starts_with("/") || path.to_string_lossy().starts_with("\\") {
32 let exe_path = ::std::env::current_exe().expect("Failed to get exe path");
33 let exe_dir = exe_path.parent().expect("Failed to get exe dir");
34
35 let rel_str = path.to_str().expect("Invalid path");
36 let rel_str = if rel_str.starts_with('/') || rel_str.starts_with('\\') {
37 &rel_str[1..]
38 } else {
39 rel_str
40 };
41 exe_dir.join(rel_str)
42 } else {
43 path
44 }
45 }}.into()
46}
47
48struct Format {
50 pub expr: syn::Expr,
51 pub args: Option<TokenStream2>,
52}
53
54impl syn::parse::Parse for Format {
55 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
56 let expr = input.parse()?;
58
59 let args = if input.peek(syn::token::Comma) {
61 Some(input.parse()?)
62 } else {
63 None
64 };
65
66 Ok(Self { expr, args })
67 }
68}