#![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) }
})
}
#[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) }
})
}
#[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();
}
}