grapheme_macro 4.0.1

Macros for the `grapheme` crate
Documentation
#![doc = include_str!("../README.md")]

use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote;
use syn::{Ident, Lit, LitStr, Path, punctuated::Punctuated, token::PathSep};
use unicode_normalization::UnicodeNormalization;
use unicode_segmentation::UnicodeSegmentation;

#[derive(Debug)]
enum Nf {
    Unnormalized,
    Nfd,
    Nfc,
    Nfkd,
    Nfkc,
}

impl Nf {
    fn from_suffix(suffix: &str, span: Span) -> syn::Result<Self> {
        match suffix {
            "" => Ok(Nf::Unnormalized),
            "u" => Ok(Nf::Unnormalized),
            "d" => Ok(Nf::Nfd),
            "c" => Ok(Nf::Nfc),
            "kd" => Ok(Nf::Nfkd),
            "kc" => Ok(Nf::Nfkc),
            _ => {
                let message = "suffix can only be 'u', 'd', 'c', 'kd' or 'kc'";
                Err(syn::Error::new(span, message))
            }
        }
    }

    fn to_path(&self) -> Path {
        let new_segment = |ident| syn::PathSegment {
            ident: Ident::new(ident, Span::call_site()),
            arguments: syn::PathArguments::None,
        };

        let name = format!("{:?}", self);
        let mut segments = Punctuated::new();
        segments.push(new_segment("grapheme"));
        segments.push(new_segment("prelude"));
        segments.push(new_segment(&name));

        let spans = [Span::call_site(); 2];
        Path {
            leading_colon: Some(PathSep { spans }),
            segments,
        }
    }

    fn normalize(&self, s: String) -> String {
        match self {
            Nf::Unnormalized => s,
            Nf::Nfd => s.chars().nfd().collect(),
            Nf::Nfc => s.chars().nfc().collect(),
            Nf::Nfkd => s.chars().nfkd().collect(),
            Nf::Nfkc => s.chars().nfkc().collect(),
        }
    }
}

enum GCount {
    More,
    One,
    Zero,
}

fn categorize_str(s: &str) -> GCount {
    let mut iter = s.graphemes(true);
    match (iter.next(), iter.next()) {
        (Some(_), Some(_)) => GCount::More,
        (Some(_), None) => GCount::One,
        _ => GCount::Zero,
    }
}

fn validate_grapheme(
    string: &str,
    inner: &str,
) -> impl FnOnce(&'static str, &'static str, &'static str, &'static str) -> Result<(), &'static str>
{
    move |mm, mo, zz, zo| match (categorize_str(inner), categorize_str(string)) {
        (GCount::More, GCount::More) => Err(mm),
        (GCount::More, _) => Err(mo),
        (GCount::One, _) => Ok(()),
        (GCount::Zero, GCount::Zero) => Err(zz),
        (GCount::Zero, _) => Err(zo),
    }
}

#[inline]
fn grapheme_macro(literal: Lit) -> syn::Result<TokenStream2> {
    let span = literal.span();
    let nf = Nf::from_suffix(literal.suffix(), span)?;
    let path = nf.to_path();
    let inner = match literal {
        Lit::Str(lit_str) => {
            let string = lit_str.value();
            let inner = nf.normalize(string.clone());

            validate_grapheme(&string, &inner)(
                "string must not contain more than one grapheme cluster",
                "is this normalization from, a grapheme cluster splits into several clusters, string must not contain more than one grapheme cluster",
                "string must not be empty",
                "is this normalization from, string becomes empty, string must not be empty",
            )
            .map(move |_| inner)
            .map_err(|message| syn::Error::new_spanned(lit_str, message))?
        }

        Lit::Char(lit_char) => {
            let string: String = std::iter::once(lit_char.value()).collect();
            let inner = nf.normalize(string.clone());

            validate_grapheme(&string, &inner)(
                "char must not contain more than one grapheme cluster",
                "is this normalization from, a grapheme cluster splits into several clusters, string must not contain more than one grapheme cluster",
                "char must not be empty",
                "is this normalization from, grapheme cluster becomes empty, grapheme cluster must not be empty",
            )
            .map(move |_| inner)
            .map_err(|message| syn::Error::new_spanned(lit_char, message))?
        }

        _ => {
            let message = "a utf8 string literal or a char literal was expected";
            return Err(syn::Error::new_spanned(literal, message));
        }
    };

    let lit_str = LitStr::new(&inner, span);
    Ok(quote! {
        unsafe { ::grapheme::Grapheme::<#path>::from_usvs_unchecked(#lit_str) }
    })
}

/// Creates a `Grapheme` from a string literal or a char literal.
///
/// One of the following suffixes can be placed after the literal:
/// - `u` - creates a `Grapheme<Unnormalized>`
/// - `d` - creates a `Grapheme<Nfd>`
/// - `c` - creates a `Grapheme<Nfc>`
/// - `kd` - creates a `Grapheme<Nfkd>`
/// - `kc` - creates a `Grapheme<Nfkc>`
///
/// If the suffix is missing, `Grapheme<Unnormalized>` will be created by default.
///
/// If the string is not normalized according to the suffix, it will be normalized
/// forcibly:
/// ```no_compile
/// let c = g!("\u{0043}\u{0327}"c);
/// assert_eq!(c.as_str(), "\u{00c7}");
/// ```
#[proc_macro]
pub fn g(input: TokenStream) -> TokenStream {
    let result = syn::parse(input).and_then(grapheme_macro);

    match result {
        Ok(tokens) => tokens.into(),
        Err(err) => err.to_compile_error().into(),
    }
}

#[inline]
fn graphemes_macro(literal: LitStr) -> syn::Result<TokenStream2> {
    let span = literal.span();
    let nf = Nf::from_suffix(literal.suffix(), span)?;
    let path = nf.to_path();
    let inner = nf.normalize(literal.value());
    let lit_str = LitStr::new(&inner, span);
    Ok(quote! {
        unsafe { ::grapheme::Graphemes::<#path>::from_usvs_unchecked(#lit_str) }
    })
}

/// Creates a `Graphemes` from a string literal.
///
/// One of the following suffixes can be placed after the literal:
/// - `u` - creates a `Graphemes<Unnormalized>`
/// - `d` - creates a `Graphemes<Nfd>`
/// - `c` - creates a `Graphemes<Nfc>`
/// - `kd` - creates a `Graphemes<Nfkd>`
/// - `kc` - creates a `Graphemes<Nfkc>`
///
/// If the suffix is missing, `Graphemes<Unnormalized>` will be created by default.
///
/// If the string is not normalized according to the suffix, it will be normalized
/// forcibly:
/// ```no_compile
/// let c = gs!("\u{0043}\u{0327}"c);
/// assert_eq!(c.as_str(), "\u{00c7}");
/// ```
#[proc_macro]
pub fn gs(input: TokenStream) -> TokenStream {
    let result = syn::parse(input).and_then(graphemes_macro);

    match result {
        Ok(tokens) => tokens.into(),
        Err(err) => err.to_compile_error().into(),
    }
}

#[cfg(test)]
mod tests {
    use crate::grapheme_macro;

    use super::*;

    #[test]
    fn grapheme_macro_test() {
        {
            let with_nf = syn::parse2(quote! { 'a' }).unwrap();
            let output = grapheme_macro(with_nf).unwrap();
            let expected = quote! { unsafe { ::grapheme::Grapheme::<::grapheme::prelude::Unnormalized>::from_usvs_unchecked("a") } };
            assert_eq!(output.to_string(), expected.to_string());
        }
        {
            let with_nf = syn::parse2(quote! { "a" }).unwrap();
            let output = grapheme_macro(with_nf).unwrap();
            let expected = quote! { unsafe { ::grapheme::Grapheme::<::grapheme::prelude::Unnormalized>::from_usvs_unchecked("a") } };
            assert_eq!(output.to_string(), expected.to_string());
        }
        {
            let with_nf = syn::parse2(quote! { "a"u }).unwrap();
            let output = grapheme_macro(with_nf).unwrap();
            let expected = quote! { unsafe { ::grapheme::Grapheme::<::grapheme::prelude::Unnormalized>::from_usvs_unchecked("a") } };
            assert_eq!(output.to_string(), expected.to_string());
        }
        {
            let with_nf = syn::parse2(quote! { "a"d }).unwrap();
            let output = grapheme_macro(with_nf).unwrap();
            let expected = quote! { unsafe { ::grapheme::Grapheme::<::grapheme::prelude::Nfd>::from_usvs_unchecked("a") } };
            assert_eq!(output.to_string(), expected.to_string());
        }
    }

    #[test]
    #[should_panic]
    fn grapheme_macro_test_not_eq() {
        let with_nf = syn::parse2(quote! { "a" }).unwrap();
        let output = grapheme_macro(with_nf).unwrap();
        let expected = quote! { unsafe { ::grapheme::Grapheme::<::grapheme::prelude::Unnormalized>::from_usvs_unchecked("b") } };
        assert_eq!(output.to_string(), expected.to_string());
    }

    #[test]
    #[should_panic]
    fn grapheme_macro_test_not_unique() {
        let with_nf = syn::parse2(quote! { "aa" }).unwrap();
        let _ = grapheme_macro(with_nf).unwrap();
    }

    #[test]
    #[should_panic]
    fn grapheme_macro_test_not_inner() {
        let with_nf = syn::parse2(quote! { 10 }).unwrap();
        let _ = grapheme_macro(with_nf).unwrap();
    }

    #[test]
    #[should_panic]
    fn grapheme_macro_test_not_nf() {
        let with_nf = syn::parse2(quote! { "a"m }).unwrap();
        let _ = grapheme_macro(with_nf).unwrap();
    }

    #[test]
    fn graphemes_macro_test() {
        {
            let with_nf = syn::parse2(quote! { "a" }).unwrap();
            let output = graphemes_macro(with_nf).unwrap();
            let expected = quote! { unsafe { ::grapheme::Graphemes::<::grapheme::prelude::Unnormalized>::from_usvs_unchecked("a") } };
            assert_eq!(output.to_string(), expected.to_string());
        }
        {
            let with_nf = syn::parse2(quote! { "Ç"u }).unwrap();
            let output = grapheme_macro(with_nf).unwrap();
            let expected = quote! { unsafe { ::grapheme::Grapheme::<::grapheme::prelude::Unnormalized>::from_usvs_unchecked("Ç") } };
            assert_eq!(output.to_string(), expected.to_string());
        }
        {
            let with_nf = syn::parse2(quote! { "C\u{327}"u }).unwrap();
            let output = grapheme_macro(with_nf).unwrap();
            let expected = quote! { unsafe { ::grapheme::Grapheme::<::grapheme::prelude::Unnormalized>::from_usvs_unchecked("C\u{327}") } };
            assert_eq!(output.to_string(), expected.to_string());
        }
        {
            let with_nf = syn::parse2(quote! { "Ç"d }).unwrap();
            let output = graphemes_macro(with_nf).unwrap();
            let expected = quote! { unsafe { ::grapheme::Graphemes::<::grapheme::prelude::Nfd>::from_usvs_unchecked("C\u{327}") } };
            assert_eq!(output.to_string(), expected.to_string());
        }
        {
            let with_nf = syn::parse2(quote! { "C\u{327}"c }).unwrap();
            let output = grapheme_macro(with_nf).unwrap();
            let expected = quote! { unsafe { ::grapheme::Grapheme::<::grapheme::prelude::Nfc>::from_usvs_unchecked("Ç") } };
            assert_eq!(output.to_string(), expected.to_string());
        }
    }

    #[test]
    #[should_panic]
    fn graphemes_macro_test_not_eq() {
        let with_nf = syn::parse2(quote! { "a" }).unwrap();
        let output = graphemes_macro(with_nf).unwrap();
        let expected = quote! { unsafe { ::grapheme::Graphemes::<::grapheme::prelude::Unnormalized>::from_usvs_unchecked("b") } };
        assert_eq!(output.to_string(), expected.to_string());
    }

    #[test]
    #[should_panic]
    fn graphemes_macro_test_not_inner() {
        let with_nf = syn::parse2(quote! { 10 }).unwrap();
        let _ = graphemes_macro(with_nf).unwrap();
    }

    #[test]
    #[should_panic]
    fn graphemes_macro_test_not_nf() {
        let with_nf = syn::parse2(quote! { "abc"m }).unwrap();
        let _ = graphemes_macro(with_nf).unwrap();
    }
}