The lib.rs
file was a bit of a mess, so this should make the boundary cleaner in preparation for creating an actual proc macro
5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC
4XADHKM66YB7R2AWXJAYR3N7CG3EM323S5WVTJWUDILZS3TAMYRAC
O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC
BMUMO42ICN3GQW77KUE2GTJPOA77SFDXJ4NNDO5NA2VJS267OXZAC
UOMQT7LTURIIWHZT2ZHLCJG6XESYTN26EJC7IHRFR4PYJ355PNYAC
K4XW4OBW5VWRCQZJNVV624E25SKRJPZ5WUXWVYHP6U7T7NPJFMFQC
3C3CHSY5FETUIE7W2VQ5V62GJNE3MW2WUM7GJDUFWSDB4WKNFR2AC
NFHPBRB5AUJGWAN7UMUDUNFDGDOCKVUKC3AAPDTND7C7MJYISVVQC
EMXNTYAMGWISSMAFQDBVGZ67OLM2SKZFH23J4ZWC5YSEAFAFL2JQC
MIHGKLMEPO5O6KSXEBOLEPWGUUL3YBNKJC75G34J23EM55BDX2XQC
use fluent_syntax::ast::{
Expression, InlineExpression, Pattern, PatternElement, Variant, VariantKey,
};
use heck::{ToPascalCase, ToSnakeCase};
use quote::{format_ident, quote};
use syn::parse_quote;
pub(crate) fn match_fluent_pattern<'resource>(
pattern: &'resource Pattern<&'resource str>,
) -> syn::Expr {
let mut format_body = String::new();
let mut format_arguments = Vec::new();
for element in &pattern.elements {
match element {
PatternElement::TextElement { value } => format_body.push_str(value),
PatternElement::Placeable { expression } => {
let expression = match_placeable_expression(&expression);
format_body.push_str("{}");
format_arguments.push(quote!(#expression));
}
}
}
let format_body_literal = proc_macro2::Literal::string(format_body.to_string().as_str());
parse_quote!(format!(#format_body_literal, #(#format_arguments),*))
}
fn match_placeable_expression<'resource>(
expression: &'resource Expression<&'resource str>,
) -> syn::Expr {
match expression {
Expression::Select { selector, variants } => {
let match_target = match_inline_expression(selector);
let match_arms: Vec<syn::Arm> = variants.iter().map(match_variant).collect();
parse_quote! {
match plural_rules.category_for(*#match_target) {
#(#match_arms),*
}
}
}
Expression::Inline(expression) => match_inline_expression(expression),
}
}
fn match_inline_expression<'resource>(
expression: &'resource InlineExpression<&'resource str>,
) -> syn::Expr {
match expression {
InlineExpression::StringLiteral { value } => {
let string_literal = proc_macro2::Literal::string(value);
parse_quote!(#string_literal)
}
InlineExpression::NumberLiteral { value } => {
// FIXME: i128 is "good enough" for now but an incorrect representation!
// e.g. decimals not supported
let parsed_value = i128::from_str_radix(value, 10).unwrap();
let number_literal = proc_macro2::Literal::i128_unsuffixed(parsed_value);
parse_quote!(#number_literal)
}
InlineExpression::VariableReference { id } => {
let ident = format_ident!("{}", id.name.to_snake_case());
parse_quote!(#ident)
}
_ => {
dbg!(expression);
todo!()
}
}
}
fn match_variant<'resource>(variant: &Variant<&'resource str>) -> syn::Arm {
let base_pattern = match_pattern(&variant.key);
let body = match_fluent_pattern(&variant.value);
// Default patterns match anything else
// TODO: this can potentially generate unreachable patterns,
// should be replaced with a more sophisticated implementation
let pattern = if variant.default {
parse_quote!(#base_pattern | _)
} else {
base_pattern
};
parse_quote!(#pattern => #body)
}
fn match_pattern<'resource>(pattern: &VariantKey<&'resource str>) -> syn::Pat {
match pattern {
VariantKey::Identifier { name } => {
let ident = format_ident!("{}", name.to_pascal_case());
parse_quote!(::icu_plurals::PluralCategory::#ident)
}
VariantKey::NumberLiteral { value } => todo!(),
}
}
use fluent_syntax::ast::{
Entry, Expression, InlineExpression, Message, Pattern, PatternElement, Resource, Variant,
VariantKey,
};
use heck::{ToKebabCase, ToPascalCase, ToSnakeCase};
use quote::{format_ident, quote};
use syn::parse_quote;
#[derive(Debug)]
pub enum GroupEntry<'resource> {
Message(&'resource Message<&'resource str>),
}
#[derive(Debug)]
pub struct Group<'resource> {
children: Vec<GroupEntry<'resource>>,
}
impl<'resource> GroupEntry<'resource> {
fn to_syn(&self) -> syn::Expr {
match self {
Self::Message(message) => message
.value
.as_ref()
.map_or_else(|| parse_quote!(()), match_fluent_pattern),
}
}
fn id(&self) -> &'resource str {
match self {
Self::Message(message) => message.id.name,
}
}
}
impl<'resource> TryFrom<&'resource Entry<&'resource str>> for GroupEntry<'resource> {
type Error = ();
fn try_from(value: &'resource Entry<&'resource str>) -> Result<Self, Self::Error> {
match value {
Entry::Message(message) => Ok(Self::Message(message)),
_ => Err(()),
}
}
}
impl<'resource> Group<'resource> {
pub fn from_resource(resource: &'resource Resource<&'resource str>) -> Self {
let mut children = Vec::new();
for entry in &resource.body {
let matched_entry = GroupEntry::try_from(entry).unwrap();
children.push(matched_entry);
}
Self { children }
}
pub fn derive_enum(
&self,
ident: syn::Ident,
enum_data: syn::DataEnum,
) -> proc_macro2::TokenStream {
let mut idents = Vec::with_capacity(enum_data.variants.len());
let mut messages = Vec::with_capacity(enum_data.variants.len());
for variant in enum_data.variants {
let kebab_case_ident = variant.ident.to_string().to_kebab_case();
let message = self
.children
.iter()
.find(|child| child.id() == kebab_case_ident)
.unwrap();
let variant_ident = variant.ident;
idents.push(match variant.fields {
syn::Fields::Named(fields) => {
// Get the name of each field for pattern-matching
let field_idents = fields
.named
.iter()
.map(|field| field.ident.as_ref().unwrap())
.map(|ident| format_ident!("{}", ident.to_string().to_snake_case()));
quote!(#variant_ident { #(#field_idents),* })
}
syn::Fields::Unnamed(_) => todo!(),
syn::Fields::Unit => quote!(#variant_ident),
});
messages.push(message.to_syn());
}
quote! {
impl #ident {
fn localize(&self, plural_rules: &::icu_plurals::PluralRules) -> String {
match self {
#(Self::#idents => #messages),*
}
}
}
}
}
}
fn match_fluent_pattern<'resource>(pattern: &'resource Pattern<&'resource str>) -> syn::Expr {
let mut format_body = String::new();
let mut format_arguments = Vec::new();
for element in &pattern.elements {
match element {
PatternElement::TextElement { value } => format_body.push_str(value),
PatternElement::Placeable { expression } => {
let expression = match_placeable_expression(&expression);
format_body.push_str("{}");
format_arguments.push(quote!(#expression));
}
}
}
let format_body_literal = proc_macro2::Literal::string(format_body.to_string().as_str());
parse_quote!(format!(#format_body_literal, #(#format_arguments),*))
}
fn match_placeable_expression<'resource>(
expression: &'resource Expression<&'resource str>,
) -> syn::Expr {
match expression {
Expression::Select { selector, variants } => {
let match_target = match_inline_expression(selector);
let match_arms: Vec<syn::Arm> = variants.iter().map(match_variant).collect();
parse_quote! {
match plural_rules.category_for(*#match_target) {
#(#match_arms),*
}
}
}
Expression::Inline(expression) => match_inline_expression(expression),
}
}
fn match_inline_expression<'resource>(
expression: &'resource InlineExpression<&'resource str>,
) -> syn::Expr {
match expression {
InlineExpression::StringLiteral { value } => {
let string_literal = proc_macro2::Literal::string(value);
parse_quote!(#string_literal)
}
InlineExpression::NumberLiteral { value } => {
// FIXME: i128 is "good enough" for now but an incorrect representation!
// e.g. decimals not supported
let parsed_value = i128::from_str_radix(value, 10).unwrap();
let number_literal = proc_macro2::Literal::i128_unsuffixed(parsed_value);
parse_quote!(#number_literal)
}
InlineExpression::VariableReference { id } => {
let ident = format_ident!("{}", id.name.to_snake_case());
parse_quote!(#ident)
}
_ => {
dbg!(expression);
todo!()
}
}
}
mod group;
mod parse;
fn match_variant<'resource>(variant: &Variant<&'resource str>) -> syn::Arm {
let base_pattern = match_pattern(&variant.key);
let body = match_fluent_pattern(&variant.value);
// Default patterns match anything else
// TODO: this can potentially generate unreachable patterns,
// should be replaced with a more sophisticated implementation
let pattern = if variant.default {
parse_quote!(#base_pattern | _)
} else {
base_pattern
};
parse_quote!(#pattern => #body)
}
fn match_pattern<'resource>(pattern: &VariantKey<&'resource str>) -> syn::Pat {
match pattern {
VariantKey::Identifier { name } => {
let ident = format_ident!("{}", name.to_pascal_case());
parse_quote!(::icu_plurals::PluralCategory::#ident)
}
VariantKey::NumberLiteral { value } => todo!(),
}
}
pub use group::Group;
use fluent_syntax::ast::{Entry, Message, Resource};
use heck::{ToKebabCase, ToSnakeCase};
use quote::{format_ident, quote};
use syn::parse_quote;
#[derive(Debug)]
pub enum GroupEntry<'resource> {
Message(&'resource Message<&'resource str>),
}
#[derive(Debug)]
pub struct Group<'resource> {
children: Vec<GroupEntry<'resource>>,
}
impl<'resource> GroupEntry<'resource> {
fn to_syn(&self) -> syn::Expr {
match self {
Self::Message(message) => message
.value
.as_ref()
.map_or_else(|| parse_quote!(()), crate::parse::match_fluent_pattern),
}
}
fn id(&self) -> &'resource str {
match self {
Self::Message(message) => message.id.name,
}
}
}
impl<'resource> TryFrom<&'resource Entry<&'resource str>> for GroupEntry<'resource> {
type Error = ();
fn try_from(value: &'resource Entry<&'resource str>) -> Result<Self, Self::Error> {
match value {
Entry::Message(message) => Ok(Self::Message(message)),
_ => Err(()),
}
}
}
impl<'resource> Group<'resource> {
pub fn from_resource(resource: &'resource Resource<&'resource str>) -> Self {
let mut children = Vec::new();
for entry in &resource.body {
let matched_entry = GroupEntry::try_from(entry).unwrap();
children.push(matched_entry);
}
Self { children }
}
pub fn derive_enum(
&self,
ident: syn::Ident,
enum_data: syn::DataEnum,
) -> proc_macro2::TokenStream {
let mut idents = Vec::with_capacity(enum_data.variants.len());
let mut messages = Vec::with_capacity(enum_data.variants.len());
for variant in enum_data.variants {
let kebab_case_ident = variant.ident.to_string().to_kebab_case();
let message = self
.children
.iter()
.find(|child| child.id() == kebab_case_ident)
.unwrap();
let variant_ident = variant.ident;
idents.push(match variant.fields {
syn::Fields::Named(fields) => {
// Get the name of each field for pattern-matching
let field_idents = fields
.named
.iter()
.map(|field| field.ident.as_ref().unwrap())
.map(|ident| format_ident!("{}", ident.to_string().to_snake_case()));
quote!(#variant_ident { #(#field_idents),* })
}
syn::Fields::Unnamed(_) => todo!(),
syn::Fields::Unit => quote!(#variant_ident),
});
messages.push(message.to_syn());
}
quote! {
impl #ident {
fn localize(&self, plural_rules: &::icu_plurals::PluralRules) -> String {
match self {
#(Self::#idents => #messages),*
}
}
}
}
}
}