use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{parse_macro_input, Expr, Ident, LitStr, Token};
#[derive(Clone)]
struct KwArg {
name: Ident,
expr: Expr,
}
impl Parse for KwArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name = input.parse()?;
input.parse::<Token![=]>()?;
let expr = input.parse()?;
Ok(Self { name, expr })
}
}
fn trailing_kwargs(input: ParseStream) -> syn::Result<Vec<KwArg>> {
let mut kwargs = Vec::new();
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
kwargs.push(input.parse()?);
}
Ok(kwargs)
}
struct T {
source: LitStr,
kwargs: Vec<KwArg>,
}
impl Parse for T {
fn parse(input: ParseStream) -> syn::Result<Self> {
let source = input.parse()?;
Ok(Self {
source,
kwargs: trailing_kwargs(input)?,
})
}
}
struct Tp {
context: LitStr,
source: LitStr,
kwargs: Vec<KwArg>,
}
impl Parse for Tp {
fn parse(input: ParseStream) -> syn::Result<Self> {
let context = input.parse()?;
input.parse::<Token![,]>()?;
let source = input.parse()?;
Ok(Self {
context,
source,
kwargs: trailing_kwargs(input)?,
})
}
}
struct Tn {
one: LitStr,
other: LitStr,
kwargs: Vec<KwArg>,
}
impl Parse for Tn {
fn parse(input: ParseStream) -> syn::Result<Self> {
let one = input.parse()?;
input.parse::<Token![,]>()?;
let other = input.parse()?;
Ok(Self {
one,
other,
kwargs: trailing_kwargs(input)?,
})
}
}
fn compile_error(span: Span, msg: &str) -> TokenStream {
syn::Error::new(span, msg).to_compile_error().into()
}
fn is_ident(s: &str) -> bool {
let mut chars = s.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn placeholders(s: &str) -> Result<Vec<String>, String> {
let mut out: Vec<String> = Vec::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'{' => {
if chars.peek() == Some(&'{') {
chars.next();
continue;
}
let mut name = String::new();
let mut closed = false;
for ch in chars.by_ref() {
if ch == '}' {
closed = true;
break;
}
name.push(ch);
}
if !closed {
return Err("unmatched '{' in a translation string".into());
}
if name.is_empty() {
return Err("positional '{}' is not allowed; name the placeholder".into());
}
if name.contains(':') {
return Err(format!("format spec in '{{{name}}}' is not allowed"));
}
if !is_ident(&name) {
return Err(format!("'{{{name}}}' is not a valid placeholder name"));
}
if !out.iter().any(|p| p == &name) {
out.push(name);
}
}
'}' => {
if chars.peek() == Some(&'}') {
chars.next();
} else {
return Err(
"unmatched '}' in a translation string; use '}}' for a literal".into(),
);
}
}
_ => {}
}
}
Ok(out)
}
fn check(placeholders: &[String], names: &[String], extra: &[&str]) -> Result<(), String> {
for p in placeholders {
if !names.iter().any(|n| n == p) {
return Err(format!(
"placeholder '{{{p}}}' has no matching argument '{p} = ...'"
));
}
}
for n in names {
if !placeholders.iter().any(|p| p == n) && !extra.contains(&n.as_str()) {
return Err(format!(
"argument '{n}' is not used by any '{{{n}}}' placeholder"
));
}
}
Ok(())
}
fn arg_names(kwargs: &[KwArg]) -> Vec<String> {
kwargs.iter().map(|k| k.name.to_string()).collect()
}
fn arg_calls(kwargs: &[KwArg]) -> Vec<proc_macro2::TokenStream> {
kwargs
.iter()
.map(|k| {
let key = k.name.to_string();
let expr = &k.expr;
quote! { .arg(#key, #expr) }
})
.collect()
}
#[proc_macro]
pub fn t(input: TokenStream) -> TokenStream {
let T { source, kwargs } = parse_macro_input!(input as T);
let ph = match placeholders(&source.value()) {
Ok(p) => p,
Err(e) => return compile_error(source.span(), &e),
};
if let Err(e) = check(&ph, &arg_names(&kwargs), &[]) {
return compile_error(source.span(), &e);
}
let args = arg_calls(&kwargs);
quote! { ::laterite_core::i18n::Text::new(#source) #(#args)* }.into()
}
#[proc_macro]
pub fn tp(input: TokenStream) -> TokenStream {
let Tp {
context,
source,
kwargs,
} = parse_macro_input!(input as Tp);
let ph = match placeholders(&source.value()) {
Ok(p) => p,
Err(e) => return compile_error(source.span(), &e),
};
if let Err(e) = check(&ph, &arg_names(&kwargs), &[]) {
return compile_error(source.span(), &e);
}
let args = arg_calls(&kwargs);
quote! { ::laterite_core::i18n::Text::new(#source).with_context(#context) #(#args)* }.into()
}
#[proc_macro]
pub fn tn(input: TokenStream) -> TokenStream {
let Tn { one, other, kwargs } = parse_macro_input!(input as Tn);
let count = match kwargs.iter().find(|k| k.name == "n") {
Some(k) => k.expr.clone(),
None => return compile_error(one.span(), "tn! needs a count argument, 'n = ...'"),
};
let mut ph = match placeholders(&one.value()) {
Ok(p) => p,
Err(e) => return compile_error(one.span(), &e),
};
match placeholders(&other.value()) {
Ok(more) => {
for p in more {
if !ph.iter().any(|x| x == &p) {
ph.push(p);
}
}
}
Err(e) => return compile_error(other.span(), &e),
}
if let Err(e) = check(&ph, &arg_names(&kwargs), &["n"]) {
return compile_error(one.span(), &e);
}
let others: Vec<KwArg> = kwargs.into_iter().filter(|k| k.name != "n").collect();
let others = arg_calls(&others);
quote! {
{
let __n: i64 = (#count) as i64;
::laterite_core::i18n::Text::new(#one)
.with_plural(#other, __n)
.arg("n", __n)
#(#others)*
}
}
.into()
}