1use proc_macro::TokenStream;
10use proc_macro2::Span;
11use quote::quote;
12use syn::parse::{Parse, ParseStream};
13use syn::{parse_macro_input, Expr, Ident, LitStr, Token};
14
15#[derive(Clone)]
17struct KwArg {
18 name: Ident,
19 expr: Expr,
20}
21
22impl Parse for KwArg {
23 fn parse(input: ParseStream) -> syn::Result<Self> {
24 let name = input.parse()?;
25 input.parse::<Token![=]>()?;
26 let expr = input.parse()?;
27 Ok(Self { name, expr })
28 }
29}
30
31fn trailing_kwargs(input: ParseStream) -> syn::Result<Vec<KwArg>> {
33 let mut kwargs = Vec::new();
34 while input.peek(Token![,]) {
35 input.parse::<Token![,]>()?;
36 if input.is_empty() {
37 break;
38 }
39 kwargs.push(input.parse()?);
40 }
41 Ok(kwargs)
42}
43
44struct T {
45 source: LitStr,
46 kwargs: Vec<KwArg>,
47}
48impl Parse for T {
49 fn parse(input: ParseStream) -> syn::Result<Self> {
50 let source = input.parse()?;
51 Ok(Self {
52 source,
53 kwargs: trailing_kwargs(input)?,
54 })
55 }
56}
57
58struct Tp {
59 context: LitStr,
60 source: LitStr,
61 kwargs: Vec<KwArg>,
62}
63impl Parse for Tp {
64 fn parse(input: ParseStream) -> syn::Result<Self> {
65 let context = input.parse()?;
66 input.parse::<Token![,]>()?;
67 let source = input.parse()?;
68 Ok(Self {
69 context,
70 source,
71 kwargs: trailing_kwargs(input)?,
72 })
73 }
74}
75
76struct Tn {
77 one: LitStr,
78 other: LitStr,
79 kwargs: Vec<KwArg>,
80}
81impl Parse for Tn {
82 fn parse(input: ParseStream) -> syn::Result<Self> {
83 let one = input.parse()?;
84 input.parse::<Token![,]>()?;
85 let other = input.parse()?;
86 Ok(Self {
87 one,
88 other,
89 kwargs: trailing_kwargs(input)?,
90 })
91 }
92}
93
94fn compile_error(span: Span, msg: &str) -> TokenStream {
95 syn::Error::new(span, msg).to_compile_error().into()
96}
97
98fn is_ident(s: &str) -> bool {
99 let mut chars = s.chars();
100 matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
101 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
102}
103
104fn placeholders(s: &str) -> Result<Vec<String>, String> {
107 let mut out: Vec<String> = Vec::new();
108 let mut chars = s.chars().peekable();
109 while let Some(c) = chars.next() {
110 match c {
111 '{' => {
112 if chars.peek() == Some(&'{') {
113 chars.next();
114 continue;
115 }
116 let mut name = String::new();
117 let mut closed = false;
118 for ch in chars.by_ref() {
119 if ch == '}' {
120 closed = true;
121 break;
122 }
123 name.push(ch);
124 }
125 if !closed {
126 return Err("unmatched '{' in a translation string".into());
127 }
128 if name.is_empty() {
129 return Err("positional '{}' is not allowed; name the placeholder".into());
130 }
131 if name.contains(':') {
132 return Err(format!("format spec in '{{{name}}}' is not allowed"));
133 }
134 if !is_ident(&name) {
135 return Err(format!("'{{{name}}}' is not a valid placeholder name"));
136 }
137 if !out.iter().any(|p| p == &name) {
138 out.push(name);
139 }
140 }
141 '}' => {
142 if chars.peek() == Some(&'}') {
143 chars.next();
144 } else {
145 return Err(
146 "unmatched '}' in a translation string; use '}}' for a literal".into(),
147 );
148 }
149 }
150 _ => {}
151 }
152 }
153 Ok(out)
154}
155
156fn check(placeholders: &[String], names: &[String], extra: &[&str]) -> Result<(), String> {
159 for p in placeholders {
160 if !names.iter().any(|n| n == p) {
161 return Err(format!(
162 "placeholder '{{{p}}}' has no matching argument '{p} = ...'"
163 ));
164 }
165 }
166 for n in names {
167 if !placeholders.iter().any(|p| p == n) && !extra.contains(&n.as_str()) {
168 return Err(format!(
169 "argument '{n}' is not used by any '{{{n}}}' placeholder"
170 ));
171 }
172 }
173 Ok(())
174}
175
176fn arg_names(kwargs: &[KwArg]) -> Vec<String> {
177 kwargs.iter().map(|k| k.name.to_string()).collect()
178}
179
180fn arg_calls(kwargs: &[KwArg]) -> Vec<proc_macro2::TokenStream> {
181 kwargs
182 .iter()
183 .map(|k| {
184 let key = k.name.to_string();
185 let expr = &k.expr;
186 quote! { .arg(#key, #expr) }
187 })
188 .collect()
189}
190
191#[proc_macro]
193pub fn t(input: TokenStream) -> TokenStream {
194 let T { source, kwargs } = parse_macro_input!(input as T);
195 let ph = match placeholders(&source.value()) {
196 Ok(p) => p,
197 Err(e) => return compile_error(source.span(), &e),
198 };
199 if let Err(e) = check(&ph, &arg_names(&kwargs), &[]) {
200 return compile_error(source.span(), &e);
201 }
202 let args = arg_calls(&kwargs);
203 quote! { ::laterite_core::i18n::Text::new(#source) #(#args)* }.into()
204}
205
206#[proc_macro]
208pub fn tp(input: TokenStream) -> TokenStream {
209 let Tp {
210 context,
211 source,
212 kwargs,
213 } = parse_macro_input!(input as Tp);
214 let ph = match placeholders(&source.value()) {
215 Ok(p) => p,
216 Err(e) => return compile_error(source.span(), &e),
217 };
218 if let Err(e) = check(&ph, &arg_names(&kwargs), &[]) {
219 return compile_error(source.span(), &e);
220 }
221 let args = arg_calls(&kwargs);
222 quote! { ::laterite_core::i18n::Text::new(#source).with_context(#context) #(#args)* }.into()
223}
224
225#[proc_macro]
228pub fn tn(input: TokenStream) -> TokenStream {
229 let Tn { one, other, kwargs } = parse_macro_input!(input as Tn);
230 let count = match kwargs.iter().find(|k| k.name == "n") {
231 Some(k) => k.expr.clone(),
232 None => return compile_error(one.span(), "tn! needs a count argument, 'n = ...'"),
233 };
234 let mut ph = match placeholders(&one.value()) {
235 Ok(p) => p,
236 Err(e) => return compile_error(one.span(), &e),
237 };
238 match placeholders(&other.value()) {
239 Ok(more) => {
240 for p in more {
241 if !ph.iter().any(|x| x == &p) {
242 ph.push(p);
243 }
244 }
245 }
246 Err(e) => return compile_error(other.span(), &e),
247 }
248 if let Err(e) = check(&ph, &arg_names(&kwargs), &["n"]) {
249 return compile_error(one.span(), &e);
250 }
251 let others: Vec<KwArg> = kwargs.into_iter().filter(|k| k.name != "n").collect();
252 let others = arg_calls(&others);
253 quote! {
254 {
255 let __n: i64 = (#count) as i64;
256 ::laterite_core::i18n::Text::new(#one)
257 .with_plural(#other, __n)
258 .arg("n", __n)
259 #(#others)*
260 }
261 }
262 .into()
263}