use proc_macro2::Ident;
use crate::util::ident;
fn to_snake_special_case(class_name: &str) -> Option<&'static str> {
match class_name {
"JSONRPC" => Some("json_rpc"),
"OpenXRAPIExtension" => Some("open_xr_api_extension"),
"OpenXRIPBinding" => Some("open_xr_ip_binding"),
_ => None,
}
}
pub fn to_snake_case(ty_name: &str) -> String {
use heck::ToSnakeCase;
assert!(
is_valid_ident(ty_name),
"invalid identifier for snake_case conversion: {ty_name}"
);
if let Some(special_case) = to_snake_special_case(ty_name) {
return special_case.to_string();
}
ty_name
.replace("1D", "_1d") .replace("2D", "_2d")
.replace("3D", "_3d")
.replace("GDNative", "Gdnative")
.replace("GDExtension", "Gdextension")
.replace("GDScript", "Gdscript")
.replace("VSync", "Vsync")
.replace("SDFGIY", "SdfgiY")
.replace("ENet", "Enet")
.to_snake_case()
}
pub fn to_pascal_case(ty_name: &str) -> String {
use heck::ToPascalCase;
assert!(
is_valid_ident(ty_name),
"invalid identifier for PascalCase conversion: {ty_name}"
);
if let Some(snake_special) = to_snake_special_case(ty_name) {
return snake_special.to_pascal_case();
}
ty_name
.to_pascal_case()
.replace("GdExtension", "GDExtension")
.replace("GdNative", "GDNative")
.replace("GdScript", "GDScript")
.replace("Vsync", "VSync")
.replace("Sdfgiy", "SdfgiY")
}
#[allow(dead_code)] pub fn shout_to_pascal(shout_case: &str) -> String {
assert!(
is_valid_shout_ident(shout_case),
"invalid identifier for SHOUT_CASE -> PascalCase conversion: {shout_case}"
);
let mut result = String::with_capacity(shout_case.len());
let mut next_upper = true;
for ch in shout_case.chars() {
if next_upper {
assert_ne!(ch, '_'); result.push(ch); next_upper = false;
} else if ch == '_' {
next_upper = true;
} else {
result.push(ch.to_ascii_lowercase());
}
}
result
}
pub fn make_unsafe_virtual_fn_name(rust_fn_name: &str) -> String {
format!("{rust_fn_name}_rawptr")
}
pub fn make_enum_name(enum_name: &str) -> Ident {
ident(&make_enum_name_str(enum_name))
}
pub fn make_enum_name_str(enum_name: &str) -> String {
match enum_name {
"Variant.Type" => "VariantType".to_string(),
"Variant.Operator" => "VariantOperator".to_string(),
e => to_pascal_case(e),
}
}
pub fn make_enumerator_names(
godot_class_name: Option<&str>,
godot_enum_name: &str,
enumerators: Vec<&str>,
) -> Vec<Ident> {
debug_assert_eq!(
make_enum_name(godot_enum_name),
godot_enum_name,
"enum name must already be mapped"
);
shorten_enumerator_names(godot_class_name, godot_enum_name, enumerators)
.iter()
.map(|e| ident(e))
.collect()
}
pub fn shorten_enumerator_names<'e>(
godot_class_name: Option<&str>,
godot_enum_name: &str,
enumerators: Vec<&'e str>,
) -> Vec<&'e str> {
if let Some(prefixes) = reduce_hardcoded_prefix(godot_class_name, godot_enum_name) {
return enumerators
.iter()
.map(|e| try_strip_prefixes(e, prefixes))
.collect::<Vec<_>>();
}
if enumerators.len() <= 1 {
return enumerators;
}
let original = &enumerators[0];
let Some((mut longest_prefix, mut pos)) = enumerator_prefix(original, enumerators[0].len())
else {
return enumerators;
};
'outer: for e in enumerators[1..].iter() {
while !e.starts_with(longest_prefix) {
if let Some((prefix, new_pos)) = enumerator_prefix(original, pos - 1) {
longest_prefix = prefix;
pos = new_pos;
} else {
pos = 0;
break 'outer;
}
}
}
let pos = pos; let last_index = enumerators.len() - 1;
enumerators
.into_iter()
.enumerate()
.map(|(i, e)| {
if e.ends_with("_MAX") && i == last_index {
return "MAX";
}
let mut local_pos = pos;
while starts_with_invalid_char(&e[local_pos..]) {
debug_assert!(local_pos > 0, "enumerator {e} starts with digit");
local_pos = if let Some(new_pos) = e[..local_pos - 1].rfind('_') {
new_pos + 1
} else {
0
};
}
&e[local_pos..]
})
.collect()
}
fn reduce_hardcoded_prefix(
class_name: Option<&str>,
enum_name: &str,
) -> Option<&'static [&'static str]> {
let result: &[&str] = match (class_name, enum_name) {
(None, "Key") => &["KEY_"],
(Some("RenderingServer" | "Mesh"), "ArrayFormat") => &["ARRAY_FORMAT_", "ARRAY_"],
(Some("AudioServer"), "SpeakerMode") => &["SPEAKER_MODE_", "SPEAKER_"],
(Some("ENetConnection"), "HostStatistic") => &["HOST_"], (None, "MethodFlags") => &["METHOD_FLAG_", "METHOD_FLAGS_"],
(None, "KeyModifierMask") => &["KEY_MASK_", "KEY_"],
(Some("RenderingDevice"), "StorageBufferUsage") => &["STORAGE_BUFFER_USAGE_"],
(Some(_), "PathfindingAlgorithm") => &["PATHFINDING_ALGORITHM_"],
_ => return None,
};
Some(result)
}
fn try_strip_prefixes<'e>(enumerator: &'e str, prefixes: &[&str]) -> &'e str {
for prefix in prefixes {
if let Some(stripped) = enumerator.strip_prefix(prefix) {
if !starts_with_invalid_char(stripped) {
return stripped;
}
}
}
enumerator
}
fn is_valid_ident(s: &str) -> bool {
!starts_with_invalid_char(s) && s.chars().all(|c| c == '_' || c.is_ascii_alphanumeric())
}
fn is_valid_shout_ident(s: &str) -> bool {
!starts_with_invalid_char(s)
&& s.chars()
.all(|c| c == '_' || c.is_ascii_digit() || c.is_ascii_uppercase())
}
fn starts_with_invalid_char(s: &str) -> bool {
s.starts_with(|c: char| c.is_ascii_digit())
}
fn enumerator_prefix(original: &str, rfind_pos: usize) -> Option<(&str, usize)> {
assert_ne!(rfind_pos, 0);
original[..rfind_pos]
.rfind('_')
.map(|pos| (&original[..pos + 1], pos + 1))
}