use unicode_general_category::{GeneralCategory, get_general_category};
use unicode_normalization::{UnicodeNormalization, char::canonical_combining_class};
use crate::{
args::{ArgValues, FromArgs, StrArg},
bytecode::VM,
defer_drop,
exception_private::{ExcType, RunResult, SimpleException},
heap::{Heap, HeapData, HeapId},
intern::StaticStrings,
modules::ModuleFunctions,
resource::{ResourceError, ResourceTracker},
string_builder::StringBuilder,
types::{Module, str::allocate_string},
value::Value,
};
const UNIDATA_VERSION: &str = "16.0.0";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, serde::Serialize, serde::Deserialize)]
#[strum(serialize_all = "snake_case")]
pub(crate) enum UnicodedataFunctions {
Category,
Name,
Lookup,
Combining,
Normalize,
IsNormalized,
}
const UNICODEDATA_FUNCTIONS: &[(StaticStrings, UnicodedataFunctions)] = &[
(StaticStrings::Category, UnicodedataFunctions::Category),
(StaticStrings::Name, UnicodedataFunctions::Name),
(StaticStrings::Lookup, UnicodedataFunctions::Lookup),
(StaticStrings::Combining, UnicodedataFunctions::Combining),
(StaticStrings::Normalize, UnicodedataFunctions::Normalize),
(StaticStrings::IsNormalized, UnicodedataFunctions::IsNormalized),
];
pub fn create_module(vm: &mut VM<'_, impl ResourceTracker>) -> Result<HeapId, ResourceError> {
let mut module = Module::new(StaticStrings::Unicodedata);
for (name, func) in UNICODEDATA_FUNCTIONS {
module.set_attr(*name, Value::ModuleFunction(ModuleFunctions::Unicodedata(*func)), vm);
}
let version = allocate_string(UNIDATA_VERSION, vm.heap)?;
module.set_attr(StaticStrings::UnidataVersion, version, vm);
vm.heap.allocate(HeapData::Module(module))
}
pub(super) fn call(
vm: &mut VM<'_, impl ResourceTracker>,
function: UnicodedataFunctions,
args: ArgValues,
) -> RunResult<Value> {
match function {
UnicodedataFunctions::Category => uni_category(vm, args),
UnicodedataFunctions::Name => uni_name(vm, args),
UnicodedataFunctions::Lookup => uni_lookup(vm, args),
UnicodedataFunctions::Combining => uni_combining(vm, args),
UnicodedataFunctions::Normalize => uni_normalize(vm, args),
UnicodedataFunctions::IsNormalized => uni_is_normalized(vm, args),
}
}
fn uni_category(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("category", vm.heap)?;
defer_drop!(value, vm);
let c = single_char(value, "category", None, vm)?;
Ok(allocate_string(category_abbrev(get_general_category(c)), vm.heap)?)
}
fn uni_name(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let NameArgs {
chr: chr_val,
default: default_val,
} = NameArgs::from_args(args, vm)?;
defer_drop!(chr_val, vm);
let c = match single_char(chr_val, "name", Some(1), vm) {
Ok(c) => c,
Err(e) => {
if let Some(d) = default_val {
d.drop_with_heap(vm);
}
return Err(e);
}
};
match unicode_names2::name(c) {
Some(name) => {
if let Some(d) = default_val {
d.drop_with_heap(vm);
}
Ok(allocate_string(name.to_string(), vm.heap)?)
}
None => match default_val {
Some(d) => Ok(d),
None => Err(SimpleException::new_msg(ExcType::ValueError, "no such name").into()),
},
}
}
fn uni_lookup(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("lookup", vm.heap)?;
defer_drop!(value, vm);
let name = value.to_str(vm)?;
match unicode_names2::character(name) {
Some(c) => Ok(allocate_string(c.to_string(), vm.heap)?),
None => Err(SimpleException::new_msg(ExcType::KeyError, format!("undefined character name '{name}'")).into()),
}
}
fn uni_combining(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let value = args.get_one_arg("combining", vm.heap)?;
defer_drop!(value, vm);
let c = single_char(value, "combining", None, vm)?;
Ok(Value::Int(i64::from(canonical_combining_class(c))))
}
fn uni_normalize(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let NormalizeArgs { form, unistr } = NormalizeArgs::from_args(args, vm)?;
defer_drop!(form, vm);
defer_drop!(unistr, vm);
let form = NormForm::parse(form.as_str(vm))?;
normalize_with(form, unistr.as_str(vm), vm.heap)
}
fn uni_is_normalized(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let IsNormalizedArgs { form, unistr } = IsNormalizedArgs::from_args(args, vm)?;
defer_drop!(form, vm);
defer_drop!(unistr, vm);
let normalized = match NormForm::parse(form.as_str(vm))? {
NormForm::Nfc => unicode_normalization::is_nfc(unistr.as_str(vm)),
NormForm::Nfd => unicode_normalization::is_nfd(unistr.as_str(vm)),
NormForm::Nfkc => unicode_normalization::is_nfkc(unistr.as_str(vm)),
NormForm::Nfkd => unicode_normalization::is_nfkd(unistr.as_str(vm)),
};
Ok(Value::Bool(normalized))
}
#[derive(FromArgs)]
#[from_args(name = "name", style = unpack)]
struct NameArgs {
#[from_args(pos_only)]
chr: Value,
#[from_args(pos_only, default)]
default: Option<Value>,
}
#[derive(FromArgs)]
#[from_args(name = "normalize", style = unpack, bad_arg)]
struct NormalizeArgs {
#[from_args(pos_only)]
form: StrArg,
#[from_args(pos_only)]
unistr: StrArg,
}
#[derive(FromArgs)]
#[from_args(name = "is_normalized", style = unpack, bad_arg)]
struct IsNormalizedArgs {
#[from_args(pos_only)]
form: StrArg,
#[from_args(pos_only)]
unistr: StrArg,
}
#[derive(Clone, Copy)]
enum NormForm {
Nfc,
Nfd,
Nfkc,
Nfkd,
}
impl NormForm {
fn parse(name: &str) -> RunResult<Self> {
match name {
"NFC" => Ok(Self::Nfc),
"NFD" => Ok(Self::Nfd),
"NFKC" => Ok(Self::Nfkc),
"NFKD" => Ok(Self::Nfkd),
_ => Err(SimpleException::new_msg(ExcType::ValueError, "invalid normalization form").into()),
}
}
}
fn normalize_with(form: NormForm, text: &str, heap: &Heap<impl ResourceTracker>) -> RunResult<Value> {
let mut builder = StringBuilder::new(heap.tracker());
match form {
NormForm::Nfc => {
for c in text.nfc() {
builder.push(c)?;
}
}
NormForm::Nfd => {
for c in text.nfd() {
builder.push(c)?;
}
}
NormForm::Nfkc => {
for c in text.nfkc() {
builder.push(c)?;
}
}
NormForm::Nfkd => {
for c in text.nfkd() {
builder.push(c)?;
}
}
}
builder.finish(heap)
}
fn single_char(
value: &Value,
fn_name: &str,
arg_num: Option<u32>,
vm: &VM<'_, impl ResourceTracker>,
) -> RunResult<char> {
let arg_word = match arg_num {
Some(n) => format!("argument {n}"),
None => "argument".to_string(),
};
if !value.is_str(vm.heap) {
return Err(ExcType::type_error(format!(
"{fn_name}() {arg_word} must be a unicode character, not {}",
value.py_type_name(vm)
)));
}
let s = value.to_str(vm)?;
let mut chars = s.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => Ok(c),
_ => Err(ExcType::type_error(format!(
"{fn_name}(): {arg_word} must be a unicode character, not a string of length {}",
s.chars().count()
))),
}
}
fn category_abbrev(category: GeneralCategory) -> &'static str {
match category {
GeneralCategory::UppercaseLetter => "Lu",
GeneralCategory::LowercaseLetter => "Ll",
GeneralCategory::TitlecaseLetter => "Lt",
GeneralCategory::ModifierLetter => "Lm",
GeneralCategory::OtherLetter => "Lo",
GeneralCategory::NonspacingMark => "Mn",
GeneralCategory::SpacingMark => "Mc",
GeneralCategory::EnclosingMark => "Me",
GeneralCategory::DecimalNumber => "Nd",
GeneralCategory::LetterNumber => "Nl",
GeneralCategory::OtherNumber => "No",
GeneralCategory::ConnectorPunctuation => "Pc",
GeneralCategory::DashPunctuation => "Pd",
GeneralCategory::OpenPunctuation => "Ps",
GeneralCategory::ClosePunctuation => "Pe",
GeneralCategory::InitialPunctuation => "Pi",
GeneralCategory::FinalPunctuation => "Pf",
GeneralCategory::OtherPunctuation => "Po",
GeneralCategory::MathSymbol => "Sm",
GeneralCategory::CurrencySymbol => "Sc",
GeneralCategory::ModifierSymbol => "Sk",
GeneralCategory::OtherSymbol => "So",
GeneralCategory::SpaceSeparator => "Zs",
GeneralCategory::LineSeparator => "Zl",
GeneralCategory::ParagraphSeparator => "Zp",
GeneralCategory::Control => "Cc",
GeneralCategory::Format => "Cf",
GeneralCategory::Surrogate => "Cs",
GeneralCategory::PrivateUse => "Co",
GeneralCategory::Unassigned => "Cn",
_ => "Cn",
}
}